Repository: hediet/vscode-drawio Branch: main Commit: 21c8a129044a Files: 88 Total size: 1.2 MB Directory structure: gitextract_vl_x46wx/ ├── .gitattributes ├── .github/ │ └── workflows/ │ ├── deploy.yml │ ├── opened-issues-triage.yml │ └── test.yml ├── .gitignore ├── .gitmodules ├── .prettierignore ├── .prettierrc.json ├── .vscode/ │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── CHANGELOG.md ├── LICENSE.md ├── NOTES.md ├── README.md ├── docs/ │ ├── code-link.md │ └── plugins.md ├── drawio-custom-plugins/ │ ├── src/ │ │ ├── drawio-types.d.ts │ │ ├── focus.ts │ │ ├── index.ts │ │ ├── linkSelectedNodeWithData.ts │ │ ├── liveshare.ts │ │ ├── menu-entries.ts │ │ ├── propertiesDialog.ts │ │ ├── styles.css │ │ ├── types.d.ts │ │ └── vscode.ts │ ├── tsconfig.json │ └── webpack.config.ts ├── examples/ │ ├── .vscode/ │ │ └── settings.json │ ├── formats/ │ │ └── README.md │ ├── linking/ │ │ ├── demo-src/ │ │ │ ├── Baz.ts │ │ │ ├── Foo.ts │ │ │ ├── test.cc │ │ │ └── tsconfig.json │ │ └── main.dio │ ├── temp/ │ │ ├── Example.drawio │ │ ├── Large.drawio │ │ └── TestLibrary.xml │ ├── tooltips-plugin.js │ └── use-cases/ │ ├── class-diagrams.dio │ ├── cloud-architecture.drawio │ ├── packages.dio │ ├── screenshots.dio │ └── wireframes.dio ├── package.json ├── scripts/ │ ├── build-and-publish.ts │ ├── changelog.ts │ ├── run-script.js │ └── tsconfig.json ├── src/ │ ├── Config.ts │ ├── DrawioClient/ │ │ ├── CustomizedDrawioClient.ts │ │ ├── DrawioClient.ts │ │ ├── DrawioClientFactory.ts │ │ ├── DrawioTypes.ts │ │ ├── html.d.ts │ │ ├── index.ts │ │ ├── simpleDrawioLibrary.ts │ │ └── webview-content.html │ ├── DrawioEditorProviderBinary.ts │ ├── DrawioEditorProviderText.ts │ ├── DrawioEditorService.ts │ ├── DrawioExtensionApi.ts │ ├── Extension.ts │ ├── features/ │ │ ├── CodeLinkFeature.ts │ │ ├── EditDiagramAsTextFeature.ts │ │ └── LiveshareFeature/ │ │ ├── CurrentViewState.ts │ │ ├── LiveshareFeature.ts │ │ ├── LiveshareSession.ts │ │ ├── SessionModel.ts │ │ ├── assets/ │ │ │ └── package.json │ │ └── index.ts │ ├── index.ts │ ├── types.d.ts │ ├── utils/ │ │ ├── SimpleTemplate.ts │ │ ├── autorunTrackDisposables.ts │ │ ├── buffer.ts │ │ ├── formatValue.ts │ │ ├── fromResource.ts │ │ ├── groupBy.ts │ │ ├── mapObject.ts │ │ ├── path.ts │ │ └── registerFailableCommand.ts │ └── vscode-utils/ │ ├── VirtualFileSystemProvider.ts │ └── VsCodeSetting.ts ├── tsconfig.json ├── tslint.json └── webpack.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf ================================================ FILE: .github/workflows/deploy.yml ================================================ name: Deploy on: push: branches: - main jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 with: submodules: true - name: Install Node.js uses: actions/setup-node@v1 with: node-version: "22" - run: yarn install --frozen-lockfile - run: yarn lint - run: yarn run-script build-and-publish env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} VSCE_PAT: ${{ secrets.MARKETPLACE_TOKEN }} GITHUB_RUN_NUMBER: ${{ github.run_number }} - name: Upload Artifacts uses: actions/upload-artifact@v4 with: name: extension path: dist/extension.vsix ================================================ FILE: .github/workflows/opened-issues-triage.yml ================================================ name: Move new issues into Triage on: issues: types: [opened] jobs: automate-project-columns: runs-on: ubuntu-latest steps: - uses: alex-page/github-project-automation-plus@v0.2.3 with: project: Backlog column: Triage repo-token: ${{ secrets.GH_TOKEN }} ================================================ FILE: .github/workflows/test.yml ================================================ name: Lint & Build on: push: branches: - main pull_request: jobs: build: strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - name: Checkout uses: actions/checkout@v2 with: submodules: true - name: Install Node.js uses: actions/setup-node@v1 with: node-version: "22" - run: yarn install --frozen-lockfile - run: yarn lint - run: yarn build ================================================ FILE: .gitignore ================================================ out dist node_modules .vscode-test/ *.vsix ================================================ FILE: .gitmodules ================================================ [submodule "drawio"] path = drawio url = https://github.com/jgraph/drawio ================================================ FILE: .prettierignore ================================================ *.d.ts ================================================ FILE: .prettierrc.json ================================================ { "trailingComma": "es5", "tabWidth": 4, "semi": true, "useTabs": true, "overrides": [ { "files": ["*.yml"], "options": { "tabWidth": 2 } } ] } ================================================ FILE: .vscode/launch.json ================================================ { "version": "0.2.0", "configurations": [ { "name": "Launch Drawio To Debug Plugins", "request": "launch", "type": "pwa-chrome", "url": "https://app.diagrams.net/", "webRoot": "${workspaceFolder}/drawio-custom-plugins/src" }, { "name": "Extension (dev)", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": [ "--disable-extensions", "--extensionDevelopmentPath=${workspaceFolder}", "${workspaceFolder}/examples" ], "outFiles": ["${workspaceFolder}/dist/**/*.js"], "preLaunchTask": "npm: dev", "env": { "DEV": "1" } }, { "name": "Extension", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": [ "--disable-extensions", "--extensionDevelopmentPath=${workspaceFolder}", "${workspaceFolder}/examples" ], "outFiles": ["${workspaceFolder}/dist/**/*.js"], "preLaunchTask": "npm: dev" } ] } ================================================ FILE: .vscode/settings.json ================================================ { "files.exclude": { "out": false }, "search.exclude": { "out": true }, "editor.formatOnSave": true, "typescript.tsc.autoDetect": "off", "workbench.editorAssociations": { "*.drawio": "default", "*.dio": "default", "*.svg": "default" }, "typescript.tsdk": "node_modules\\typescript\\lib" } ================================================ FILE: .vscode/tasks.json ================================================ // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format { "version": "2.0.0", "tasks": [ { "type": "npm", "script": "dev", "problemMatcher": { "base": "$tsc", "background": { "activeOnStart": true, "beginsPattern": "assets", "endsPattern": "compiled" } }, "isBackground": true, "presentation": { "reveal": "never" }, "group": { "kind": "build", "isDefault": true } }, { "type": "npm", "script": "dev-drawio-plugins", "problemMatcher": { "base": "$tsc", "background": { "activeOnStart": true, "beginsPattern": "assets", "endsPattern": "compiled" } }, "isBackground": true, "presentation": { "reveal": "never" }, "group": { "kind": "build", "isDefault": true } } ] } ================================================ FILE: CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [1.9.0] ### Fixed - Reverts change to automatically follow VS Code dark/light theme [#457](https://github.com/hediet/vscode-drawio/issues/457) ## [1.8.0] ### Fixed - v1.7.0 breaks themes [#456](https://github.com/hediet/vscode-drawio/issues/456) ## [1.7.0] ### Changed - Updates Draw.io to 26.0.2 - Removes donation dialog - Do not let draw.io handle cmd+p/cmd+shift+p keyboard shortcuts on Mac ### Added - Introduces setting `hediet.vscode-drawio.resizeImages` (fixes [#360](https://github.com/hediet/vscode-drawio/issues/360)) ### Fixed - Maths not rendered when exporting diagram (address [#180](https://github.com/hediet/vscode-drawio/issues/180)) - Shapes not loading (BPMN + general) (fixes [#354](https://github.com/hediet/vscode-drawio/issues/354)) ## [1.6.5] - 2022-03-16 ### Added - Two commands to allow code linking to work with hierarchical document symbols (addresses [#167](https://github.com/hediet/vscode-drawio/issues/167), [291](https://github.com/hediet/vscode-drawio/issues/291)) - `linkSymbolWithSelectedNode` to link by symbol path and document URI - `linkWsSymbolWithSelectedNode` to link solely by symbol path - Add support for settings: - `zoomFactor` to control trackpad and mouse wheel sensitivity (fixes [#301](https://github.com/hediet/vscode-drawio/issues/301)) - `globalVars` to bypass arbitrary custom plugin configuration to `Editor.globalVars` (addresses [#298](https://github.com/hediet/vscode-drawio/issues/298)) ## [1.6.4] - 2021-12-21 ### Changed - Updates Draw.io to 16.0.0 ## [1.6.3] ### Changed - Add support for settings: - `style` - `defaultVertexStyle` - `defaultEdgeStyle` - `colorNames` - `defaultColorSchemes` setting can now have a title attribute - Updates Draw.io to 14.9.9 - Migrates to memento for draw.io local-storage ## [1.6.2] ### Fixed - Removes redundant web extension kind definition. ## [1.6.1] ### Fixed - Files can be linked again. ## [1.6.0] - 2021-07-11 ### Changed - Updates Draw.io to 14.8.0 - Code Link Match filter includes characters `<`, `>` and `,` to support generic class names (fixes [#240](https://github.com/hediet/vscode-drawio/issues/240)). ### Fixed - When Draw.io applies an external change to the document, it no longer emits another change (fixes [#215](https://github.com/hediet/vscode-drawio/issues/215)). - Emits proper line breaks instead of (fixes [#209](https://github.com/hediet/vscode-drawio/issues/209)). - When execution of a command throws, a more detailed error message is shown (fixes [#239](https://github.com/hediet/vscode-drawio/issues/239)). ### Added - Uses full `zh-tw` language code (instead of just `zh`) if VS Code reports this language. - Makes the extension ui, workspace and web ready. ## [1.5.0] - 2021-05-29 ### Changed - Updates Draw.io to 14.7.3. ### Added - Add support for untrusted workspaces. - Adds support for sketch theme. ## [1.4.0] - 2021-02-14 ### Changed - Removes metadata from xml. This includes an etag, last modified date and other information. ### Added - SVG link targets are configurable now (see [#204](https://github.com/hediet/vscode-drawio/issues/204)). - Option to disable SVG 1.1 warning ### Fixed - When changing properties in the properties dialog and saving the diagram after applying the change, the diagram was saved as compressed xml (if it was opened as xml). With this fix it is always saved as uncompressed xml. ## [1.3.0] - 2021-01-17 ### Changed - Updates drawio to 14.2.4. - Implements _Properties_ dialog to configure scale and border for SVG and PNG exports. ## [1.2.0] - 2020-11-19 ### Changed - Updates drawio to 13.10.0. ## [1.1.0] - 2020-11-08 ### Added - A context menu item has been added to the explorer view to link nodes to arbitrary files (see [#169](https://github.com/hediet/vscode-drawio/issues/169)). ### Fixed - `shift+f3` (find previous) is uncovered when the find-widget is visible (see [#174](https://github.com/hediet/vscode-drawio/pull/174), by [@fbehrens](https://github.com/fbehrens)). - Fixes that code link changes didn't trigger a document change. ## [1.0.3] - 2020-10-15 ### Added - Add "Preset Colors" and "Custom Color Schemes" settings (see [#145](https://github.com/hediet/vscode-drawio/issues/145), by [@AvroraPolnareff](https://github.com/AvroraPolnareff)). - Add "New Draw.io Diagram" to the command palette (see [#145](https://github.com/hediet/vscode-drawio/issues/145)). ## [1.0.2] - 2020-10-12 ### Fixed - Fix webview error when data directory is symlink (see [#152](https://github.com/hediet/vscode-drawio/pull/152), by [@jingyu9575](https://github.com/jingyu9575)). ## [1.0.1] - 2020-10-07 ### Fixed - Fixes bug that leads to too many sponsorship dialogs. - Disables Alt+Shift+S and Ctrl+Shift+S, as everything save-related is handled by VS Code (see [#144](https://github.com/hediet/vscode-drawio/issues/144)). ## [1.0.0] - 2020-10-04 ### Added - Enhanced Liveshare support: Cursors and selections of other participants are now shown. - Code Links can now refer to arbitrary code spans, not only to symbols. - Adds export/convert/save entries to the drawio menu. - Supports custom drawio plugins. - Other VS Code extensions can provide custom drawio plugins. - Adds a status bar item to quickly change the current drawio theme. - Adds drawio-language-mode (see [#130](https://github.com/hediet/vscode-drawio/issues/130)). - Users of the Insiders Build are asked for feedback after some activity time. - Users of the Stable Build are asked for sponsorship after some activity time. ### Changed - Updates drawio to 13.6.5. - Code Link looks for `#symbol` references in the entire label, not just in the beginning. - Hides the option to convert a drawio file format to itself. - Changes Category to "Visualization". ### Fixed - Fixes loss of data when changing theme in binary drawio editor with unsaved changes. - Fixes export/convert output to wrong directory when filepath contains '.' (see [#117](https://github.com/hediet/vscode-drawio/pull/117), by [@fatalc](https://github.com/fatalc)). - Fixes color problem when using light drawio theme in dark vscode theme (see [#129](https://github.com/hediet/vscode-drawio/issues/129)). ## [0.7.2] - 2020-06-28 ### Added - Symbol Code Link Feature - "Draw.io: Change Theme" Command - Experimental Manual Code Link Feature (disabled by default) - Experimental Command "Edit Diagram as Text" (disabled by default) ### Changed - Uses `https://embed.diagrams.net/` as default URL when using the online mode. ## [0.7.1] - 2020-06-13 ### Fixed - Fixes base URL. Resolves [#53](https://github.com/hediet/vscode-drawio/issues/53) and [#74](https://github.com/hediet/vscode-drawio/issues/74). (Implemented by [Speedy37](https://github.com/Speedy37)) ## [0.7.0] - 2020-06-11 ### Added - Support for creating and editing \*.drawio.png files! ### Changed - Ctrl-P is now forwarded to VS Code (see [#77](https://github.com/hediet/vscode-drawio/issues/77)). ## [0.6.6] - 2020-05-31 ### Added - Read-only view when diffing diagrams. ### Changed - Better xml canonicalization. If only non-significant whitespace has been changed, the diagram should never reload. ### Fixed - Prevents Draw.io from marking the diagram as changed if it got reloaded from disk. ## [0.6.1] - Adds hediet.vscode-drawio.editor.customFonts to configure custom fonts. - Adds hediet.vscode-drawio.editor.customLibraries to configure custom fonts. - Encodes hediet.vscode-drawio.local-storage to make editing more difficult (other settings should be used for that). - Reloads diagram editor when the config changes. - Writes localStorage to the settings file it was read from. ## [0.6.0] - Implements a command that lets you export a diagram to svg, png or drawio. ## [0.5.2] - Implements a command that lets you convert a diagram to other editabled formats (e.g. drawio.svg). ## [0.5.1] - Fixes F1/Ctrl+Tab/Ctrl+Shift+P shortcuts. ## [0.5.0] - Reduces the size of the extension significantly. - Does not spawn an http server anymore to host Draw.io - Uses new Draw.io merge API for better Live-Share experience. ## [0.4.0] - Supports Draw.io features that required local storage: - Scratchpad - Languages - Selected Libraries - Layout Settings - Uses current VS Code locale settings for Draw.io. - Removes export options as they did not work. - Fixes bug when using VS Code remote development. - Fixes bug that caused empty drawio diagrams to be saved with xml compression. - Technical code improvements. ## [0.3.0] - Supports editing `*.drawio.svg` files. - Introduces `hediet.vscode-drawio.theme` to configure the theme used in the Draw.io editor. - Logs the drawio iframe/extension communication. - Fixes a memory leak. - Fixes a bug that resets the view/undo stack on save. ## [0.2.0] - Implements offline mode (enabled by default). - Implements config to disable offline mode. - Implements config to choose a custom drawio url. ## [0.1.3] - Treats `*.dio` files the same as `*.drawio` files. - Makes extension compatible with VS Code 1.44. ## [0.1.0] - Initial release ================================================ FILE: LICENSE.md ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and`show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: NOTES.md ================================================ Use `returnbounds` to get xml right after load: ```js // Sends the bounds of the graph to the host after parsing if (urlParams['returnbounds'] == '1' || urlParams['proto'] == 'json') ``` ================================================ FILE: README.md ================================================ # Draw.io VS Code Integration [![](https://img.shields.io/twitter/follow/hediet_dev.svg?style=social)](https://twitter.com/intent/follow?screen_name=hediet_dev) This unofficial extension integrates [Draw.io](https://app.diagrams.net/) (also known as [diagrams.net](https://www.diagrams.net/)) into VS Code. Mentioned in the official diagrams.net [blog](https://www.diagrams.net/blog/embed-diagrams-vscode). ## Features - Edit `.drawio`, `.dio`, `.drawio.svg` or `.drawio.png` files in the Draw.io editor. - To create a new diagram, simply create an empty `*.drawio`, `*.drawio.svg` or `*.drawio.png` file and open it. - `.drawio.svg` are valid `.svg` files that can be embedded in Github readme files! No export needed. - `.drawio.png` are valid `.png` files! No export needed. You should use `.svg` though whenever possible - they look much better! - To convert between different formats, use the `Draw.io: Convert To...` command. - Uses an offline version of Draw.io by default. - Multiple Draw.io themes are available. - Use Liveshare to collaboratively edit a diagram with others. - Nodes/edges can be linked with code spans. ## Demo ![](./docs/demo.gif) ## Editing .drawio.svg/.drawio.png Files You can directly edit and save `.drawio.svg` and `.drawio.png` files. These files are perfectly valid svg/png-images that contain an embedded Draw.io diagram. Whenever you edit such a file, the svg/png part of that file is kept up to date. The logo of this extension is such a `.drawio.png` file that has been created with the extension itself! ![](./docs/drawio-png.gif) If diffs are important for you, you should prefer `.drawio` and avoid `.drawio.png` diagrams. ## Collaboratively Edit Or Present Diagrams With version 1.0 of this extension, extensive support for [VS Code Liveshare](https://visualstudio.microsoft.com/de/services/live-share/) has been added. You can now edit or present your Draw.io diagrams remotely, while seeing each participant's cursor and selection! This can be used for discussing, reviewing or brainstorming diagrams. With Draw.io's freehand drawing tool and integrated LaTeX support, this extension becomes an advanced whiteboard solution that can be used for remote code interviews! ![](./docs/liveshare-demo.gif) _Internally, this extension synchronizes Draw.io diagrams with text documents. These text documents are shared by Liveshare. As Liveshare has no understanding of the text, modification conflicts might occur on simultaneous modifications._ ## Code Link Feature In the status bar, you can enable or disable the code link feature. If it is enabled and you double click on a node whose label starts with `#`, you will perform a workspace search for a symbol matching the rest of the label. If you have a node labeled `#MyClass` and a class of name `MyClass`, you will jump to its source if you double click the node! **Please note that you have to open at least one file of the project that contains the symbol.** Otherwise, VS Code will not consider this project when searching for symbols. This file itself does not have to contain the symbol though. Thanks to my latest github sponsors, this feature is open source and freely available now. _TIP_: If you open the draw.io editor to the right side (i.e. the second editor column) and navigate to a symbol, the diagram will stay visible. ![](./docs/demo-code-link.gif) ## Themes
Available Draw.io Themes
  • Theme "atlas"

    atlas
  • Theme "Kennedy"

    Kennedy
  • Theme "min"

    min
  • Theme "dark"

    dark
## Associate `.svg` Files With The Draw.io Editor By default, this extension only handles `*.drawio.svg` files. Add this to your VS Code `settings.json` file if you want to associate it with `.svg` files: ```json "workbench.editorAssociations": { "*.svg": "hediet.vscode-drawio-text", } ``` You won't be able to edit arbitrary SVG files though - only those that have been created with Draw.io or this extension! ## Editing the Diagram and its XML Side by Side You can open the same `*.drawio` file with the Draw.io editor and as xml file. They are synchronized, so you can switch between them as you like it. This is super practical if you want to use find/replace to rename text or other features of VS Code to speed up your diagram creation/edit process. Use the `View: Reopen Editor With...` command to toggle between the text or the Draw.io editor. You can open multiple editors for the same file. This does not make much sense for SVG files though, as the draw.io diagram is stored in its metadata. ![](./docs/drawio-xml.gif) ## Contributors - Henning Dieterichs, [hediet](https://github.com/hediet) on Github (Main Contributor / Author) - Vincent Rouillé, [Speedy37](https://github.com/Speedy37) on Github ## See Also / Similar Extensions - [Draw.io](https://app.diagrams.net/) - This extension relies on the giant work of Draw.io. Their embedding feature enables this extension! This extension bundles a recent version of Draw.io. - [vscode-drawio](https://github.com/eightHundreds/vscode-drawio) by eightHundreds. ## Other Cool Extensions If you like this extension, you might like [my other extensions](https://marketplace.visualstudio.com/search?term=henning%20dieterichs&target=VSCode) too: - **[Debug Visualizer](https://marketplace.visualstudio.com/items?itemName=hediet.debug-visualizer)**: An extension for visualizing data structures while debugging. - **[Real-Time Debugging](https://marketplace.visualstudio.com/items?itemName=hediet.realtime-debugging)**: This extension visualizes how your code is being executed. ================================================ FILE: docs/code-link.md ================================================ # VS Code Draw.io Integration - Code Links (Since 0.7.2) The Code Link feature lets you link Draw.io nodes and edges to source code symbols. Just name a node or edge `#MySymbol` where `MySymbol` is the name of the symbol you want to link to. When code link is enabled (see the status bar) and you double click on a node or edge whith such a label, you will jump to the symbol definition. ![](./demo-code-link.gif) Disable Code Link or select a node and press F2 if you want to change the label. This feature works with any programming language that implements the VS Code workspace symbol search. In TypeScript, symbols are functions, classes, consts, interfaces, ... Code Links also work for `*.drawio.png` and `*.drawio.svg` files which are plain `*.png` and `*.svg` files with embedded Draw.io metadata that can be put on Github. The Code Link Feature does not work on Github though. ## Link Document Symbols ![](./code-link-symbol-demo.gif) Code Link supports hierarchical document symbols. First, select the node, then navigate to the document and run the "Link Symbol With Selected Node" command. Select your symbol from the dropdown. This will link the node to a symbol as well as the current docment URI. Hierarchy levels are delimited by the dot "`.`" and you can choose to edit the paths by pressing "Ctrl+M" within the Draw.io editor. You can also run "Link Workspace Symbol With Selected Node" to link a symbol path without linking to the specific docment URI. This way, documents can be moved freely without breaking the code link. However, some symbol providers don't cooperate with exporting workspace symbols -- when this occurs, a warning will be shown. ## Link Screenshots with Symbols Since you can directly paste images into Draw.io diagrams, you can use this feature to connect screenshots of react components to their source: ![](./code-link-with-screenshots.gif) ## Applications This feature can be used in many ways: - for documentation - for quick code navigation (like visual bookmarks) - for diagram based code tours ## Thank You Thank you to Draw.io for being so open and enabling this kind of stuff, thank you to all the Contributors of this extension and thank you so much for my Sponsors on Github that really motivate me implementing features like this! ================================================ FILE: docs/plugins.md ================================================ # VS Code Draw.io Integration - plugins The plugins feature lets you load Draw.io plugins, just as you can by opening the online version of Draw.io with the `?p=svgdata` query parameter: . Draw.io has a list of [sample plugins](https://www.drawio.com/doc/faq/plugins) which can be copied, or you may create your own. ## Enabling a plugin in the Draw.io Integration Plugins currently needs to be loaded from an absolute path in the Draw.io Integration extension. Thus for compatibility reasons (e.g., in a repository shared between multiple people), the plugin likely needs to be added to the workspace folder where your diagrams are located as well. To facilitate this, the path can be specified using the `${workspaceFolder}` variable, effectively allowing you to specify a relative path within your workspace. Plugins are added using the `hediet.vscode-drawio.plugins` configuration property. Adding this to the workspace settings makes sure that the plugin is automatically loaded for anyone that edits Draw.io files inside this workspace. Example: 1. Download the Draw.io sample plugin `svgdata.js`, and place it in the root of the workspace. 1. Add the following to the workspace settings: ```json "hediet.vscode-drawio.plugins": [ { "file": "${workspaceFolder}/svgdata.js" } ], ``` 1. Open any Draw.io file 1. Accept or deny loading of the plugin If this is the first time after adding the plugin definition, or if the plugin was changed, then the Draw.io Integration will show you a dialogue box, asking you to allow or disallow loading of the given plugin. What ever action you choose, is written to the `hediet.vscode-drawio.knownPlugins` property, in the user settings (scope) by the Draw.io Integration extension. Your decision is explicitly only read and written to the user scope, to ensure that a redistributed workspace can't load a plugin without you previously having accepted the specific version of a plugin (determined through the hash of the file). Example: ```json "hediet.vscode-drawio.knownPlugins": [ { "pluginId": "file:///full/path/to/workspace/svgdata.js", "fingerprint": "", "allowed": true // or false if you disallowed it } ], ``` ================================================ FILE: drawio-custom-plugins/src/drawio-types.d.ts ================================================ declare const Draw: { loadPlugin(handler: (ui: DrawioUI) => void): void; }; declare const log: any; declare class mxCellHighlight { constructor(graph: DrawioGraph, color: string, arg: number); public highlight(arg: DrawioCellState | null): void; public destroy(): void; } declare class mxResources { static parse(value: string): void; static get(key: string): string; } declare class mxMouseEvent { public readonly graphX: number; public readonly graphY: number; } declare const mxEvent: { DOUBLE_CLICK: string; CHANGE: string; }; declare const mxUtils: { isNode(node: any): node is HTMLElement; createXmlDocument(): XMLDocument; }; declare interface DrawioUI { fileNode: Element | null; hideDialog(): void; showDialog(...args: any[]): void; editor: DrawioEditor; actions: DrawioActions; menus: DrawioMenus; importLocalFile(args: boolean): void; } interface DrawioMenus { get(name: string): any; addMenuItems(menu: any, arg: any, arg2: any): void; } interface DrawioActions { addAction(name: string, action: () => void): void; get(name: string): { funct: () => void }; } declare interface DrawioEditor { graph: DrawioGraph; } declare interface DrawioGraph { defaultThemeName: string; insertVertex(arg0: undefined, arg1: null, label: string, arg3: number, arg4: number, arg5: number, arg6: number, arg7: string): void; addListener: any; model: DrawioGraphModel; getLabel(cell: DrawioCell): string; getSelectionModel(): DrawioGraphSelectionModel; view: DrawioGraphView; addMouseListener(listener: { mouseMove?: (graph: DrawioGraph, event: mxMouseEvent) => void; mouseDown?: (graph: DrawioGraph, event: mxMouseEvent) => void mouseUp?: (graph: DrawioGraph, event: mxMouseEvent) => void; }): void; } declare interface DrawioGraphView { getState(cell: DrawioCell): DrawioCellState; canvas: SVGElement; } declare interface DrawioCellState { cell: DrawioCell; } declare interface DrawioGraphSelectionModel { addListener(event: string, handler: (...args: any[]) => void): void; cells: DrawioCell[]; } declare interface DrawioCell { id: string; style: string } declare interface DrawioGraphModel { setValue(c: DrawioCell, label: string | any): void; beginUpdate(): void; endUpdate(): void; cells: Record; setStyle(cell: DrawioCell, style: string): void; isVertex(cell: DrawioCell): boolean; } ================================================ FILE: drawio-custom-plugins/src/focus.ts ================================================ import { sendEvent } from "./vscode"; Draw.loadPlugin((ui) => { sendEvent({ event: "pluginLoaded", pluginId: "focus" }); if (document.hasFocus()) { sendEvent({ event: "focusChanged", hasFocus: true }); } else { sendEvent({ event: "focusChanged", hasFocus: false }); } window.addEventListener("focus", () => { sendEvent({ event: "focusChanged", hasFocus: true }); }); window.addEventListener("blur", () => { sendEvent({ event: "focusChanged", hasFocus: false }); }); }); ================================================ FILE: drawio-custom-plugins/src/index.ts ================================================ import "./linkSelectedNodeWithData"; import "./liveshare"; import "./focus"; import "./menu-entries"; Draw.loadPlugin((ui) => { (window as any).hediet_DbgUi = ui; }); ================================================ FILE: drawio-custom-plugins/src/linkSelectedNodeWithData.ts ================================================ import { ConservativeFlattenedEntryParser, FlattenToDictionary, JSONValue, } from "@hediet/json-to-dictionary"; import { sendEvent } from "./vscode"; Draw.loadPlugin((ui) => { sendEvent({ event: "pluginLoaded", pluginId: "linkSelectedNodeWithData" }); let nodeSelectionEnabled = false; const graph = ui.editor.graph; const highlight = new mxCellHighlight(graph, "#00ff00", 8); const model = graph.model; let activeCell: DrawioCell | undefined = undefined; graph.addListener(mxEvent.DOUBLE_CLICK, function (sender: any, evt: any) { if (!nodeSelectionEnabled) { return; } var cell: any | null = evt.getProperty("cell"); if (cell == null) return; const data = getLinkedData(cell); const label = getLabelTextOfCell(cell); if (!data && !label.match(/#([a-zA-Z0-9_]+)/)) { return; } sendEvent({ event: "nodeSelected", label, linkedData: data }); evt.consume(); }); function getLabelTextOfCell(cell: any): string { const labelHtml = graph.getLabel(cell); const el = document.createElement("html"); el.innerHTML = labelHtml; // label can be html return el.innerText; } const selectionModel = graph.getSelectionModel(); selectionModel.addListener(mxEvent.CHANGE, (sender: any, evt: any) => { // selection has changed const cells = selectionModel.cells; if (cells.length >= 1) { const selectedCell = cells[0]; activeCell = selectedCell; (window as any).hediet_Cell = selectedCell; } else { activeCell = undefined; } }); const prefix = "hedietLinkedDataV1"; const flattener = new FlattenToDictionary({ parser: new ConservativeFlattenedEntryParser({ prefix, separator: "_", }), }); function getLinkedData(cell: { value: unknown }) { if (!mxUtils.isNode(cell.value)) { return undefined; } const kvs = [...(cell.value.attributes as any)] .filter((a) => a.name.startsWith(prefix)) .map((a) => [a.name, a.value]); if (kvs.length === 0) { return undefined; } const r: Record = {}; for (const [k, v] of kvs) { r[k] = v; } return flattener.unflatten(r); } function setLinkedData(cell: any, linkedData: JSONValue) { let newNode: HTMLElement; if (!mxUtils.isNode(cell.value)) { const doc = mxUtils.createXmlDocument(); const obj = doc.createElement("object"); obj.setAttribute("label", cell.value || ""); newNode = obj; } else { newNode = cell.value.cloneNode(true); } for (const a of [ ...((newNode.attributes as any) as { name: string }[]), ]) { if (a.name.startsWith(prefix)) { newNode.attributes.removeNamedItem(a.name); } } const kvp = flattener.flatten(linkedData); for (const [k, v] of Object.entries(kvp)) { newNode.setAttribute(k, v); } // don't use cell.setValue as it does not trigger a change model.setValue(cell, newNode); } window.addEventListener("message", (evt) => { if (evt.source !== window.opener) { return; } console.log(evt); const data = JSON.parse(evt.data) as CustomDrawioAction; switch (data.action) { case "setNodeSelectionEnabled": { nodeSelectionEnabled = data.enabled; break; } case "linkSelectedNodeWithData": { if (activeCell !== undefined) { log("Set linkedData to " + data.linkedData); graph.model.beginUpdate(); try { setLinkedData(activeCell, data.linkedData); } finally { graph.model.endUpdate(); } highlight.highlight(graph.view.getState(activeCell)); setTimeout(() => { highlight.highlight(null); }, 500); } break; } case "getVertices": { const vertices = Object.values(graph.model.cells) .filter((c) => graph.model.isVertex(c)) .map((c: any) => ({ id: c.id, label: graph.getLabel(c) })); sendEvent({ event: "getVertices", message: data, vertices: vertices, }); break; } case "updateVertices": { const vertices = data.verticesToUpdate; graph.model.beginUpdate(); try { for (const v of vertices) { const c = graph.model.cells[v.id]; if (!c) { log(`Unknown cell "${v.id}"!`); continue; } if (graph.getLabel(c) !== v.label) { graph.model.setValue(c, v.label); } } } finally { graph.model.endUpdate(); } break; } case "addVertices": { // why is this called twice? log("add vertices is being called"); const vertices = data.vertices; graph.model.beginUpdate(); try { let i = 0; for (const v of vertices) { graph.insertVertex( undefined, null, v.label, i * 120, 0, 100, 50, "rectangle" ); i++; } } finally { graph.model.endUpdate(); } break; } default: { return; } } evt.preventDefault(); evt.stopPropagation(); }); }); ================================================ FILE: drawio-custom-plugins/src/liveshare.ts ================================================ import { sendEvent } from "./vscode"; import * as m from "mithril"; Draw.loadPlugin((ui) => { setTimeout(() => { sendEvent({ event: "pluginLoaded", pluginId: "LiveShare" }); const graph = ui.editor.graph; const selectionModel = graph.getSelectionModel(); selectionModel.addListener(mxEvent.CHANGE, () => { const cells = selectionModel.cells; sendEvent({ event: "selectedCellsChanged", selectedCellIds: cells.map((c) => c.id), }); }); const theme = graph.defaultThemeName === "darkTheme" ? "dark" : "light"; /* new Cursor(graph.view.canvas, "test", { color: "#2965CC", name: "Henning Dieterichs", theme, }).setPosition({ x: 1200, y: 800, }); const r = new SelectionRectangle(graph.view.canvas, "test", { color: "blue", }); r.setPositions( { x: 1250, y: 850, }, { x: 1400, y: 1000, } ); */ const cursors = new Set(); const rectangles = new Set(); const hightlights = new Highlights(graph); window.addEventListener("message", (evt) => { if (evt.source !== window.opener) { return; } const data = JSON.parse(evt.data) as CustomDrawioAction; switch (data.action) { case "updateLiveshareViewState": { for (const c of cursors) { if (!data.cursors.some((c) => c.id === c.id)) { cursors.delete(c); c.dispose(); } } for (const c of data.cursors) { const existing = [...cursors].find( (existingCursor) => existingCursor.id === c.id ) || new Cursor(graph.view.canvas, c.id, { color: c.color, name: c.label || "", theme, }); cursors.add(existing); existing.setPosition(transform(c.position)); } const highlightInfos = new Array(); for (const s of data.selectedCells) { for (const selectedCellId of s.selectedCellIds) { const cell = graph.model.cells[selectedCellId]; highlightInfos.push({ cell, color: s.color }); } } hightlights.updateHighlights(highlightInfos); for (const c of rectangles) { if ( !data.selectedRectangles.some((c) => c.id === c.id) ) { rectangles.delete(c); c.dispose(); } } for (const c of data.selectedRectangles) { const existing = [...rectangles].find( (existingRectangle) => existingRectangle.id === c.id ) || new SelectionRectangle(graph.view.canvas, c.id, { color: c.color, }); rectangles.add(existing); existing.setPositions( transform(c.rectangle.start), transform(c.rectangle.end) ); } break; } } }); function transform({ x, y }: { x: number; y: number }) { const { scale, translate } = graph.view as any; return { x: (x + translate.x) * scale, y: (y + translate.y) * scale, }; } function transformBack({ x, y }: { x: number; y: number }) { const { scale, translate } = graph.view as any; return { x: x / scale - translate.x, y: y / scale - translate.y }; } graph.addMouseListener({ mouseMove: (graph: DrawioGraph, event: mxMouseEvent) => { const pos = { x: event.graphX, y: event.graphY }; const graphPos = transformBack(pos); sendEvent({ event: "cursorChanged", position: graphPos }); }, mouseDown: () => {}, mouseUp: () => {}, }); function patchFn( clazz: any, fnName: string, fnFactory: (old: Function) => (this: any, ...args: any) => any ) { const old = clazz[fnName]; clazz[fnName] = fnFactory(old); } patchFn(mxRubberband.prototype, "update", function (old) { return function (...args: any[]) { let first = { ...this.first }; let second = { x: args[0], y: args[1] }; old.apply(this, args); if (first.x > second.x) { const temp = first.x; first.x = second.x; second.x = temp; } if (first.y > second.y) { const temp = first.y; first.y = second.y; second.y = temp; } sendEvent({ event: "selectedRectangleChanged", rect: { start: transformBack(first), end: transformBack(second), }, }); }; }); patchFn(mxRubberband.prototype, "reset", function (old) { return function (...args: any[]) { old.apply(this, args); sendEvent({ event: "selectedRectangleChanged", rect: undefined, }); }; }); }); }); declare class mxRubberband {} const svgns = "http://www.w3.org/2000/svg"; class SelectionRectangle { private readonly g = document.createElementNS(svgns, "g"); private pos1: { x: number; y: number } = { x: 0, y: 0 }; private pos2: { x: number; y: number } = { x: 0, y: 0 }; constructor( canvas: SVGElement, public readonly id: string, private readonly options: { color: string } ) { canvas.appendChild(this.g); this.g.setAttribute("pointer-events", "none"); } public setPositions( pos1: { x: number; y: number }, pos2: { x: number; y: number } ) { this.pos1 = pos1; this.pos2 = pos2; this.render(); } private render() { m.render( this.g, m( "rect", { x: this.pos1.x, y: this.pos1.y, width: this.pos2.x - this.pos1.x, height: this.pos2.y - this.pos1.y, style: { fill: this.options.color, fillOpacity: 0.08, stroke: this.options.color, strokeOpacity: 0.8, }, }, [] ) ); } public dispose(): void { this.g.remove(); } } interface CursorOptions { color: string; ///borderColor: string; name: string; theme: "dark" | "light"; } class Cursor { private readonly g = document.createElementNS(svgns, "g"); constructor( canvas: SVGElement, public readonly id: string, options: CursorOptions ) { canvas.appendChild(this.g); this.g.setAttribute("pointer-events", "none"); m.render( this.g, m("g", [ m("g", { transform: "scale(0.06,0.06)" }, [ m("path", { fill: options.color, style: { stroke: options.theme === "dark" ? "white" : "black", strokeWidth: 10, }, d: "M302.189 329.126H196.105l55.831 135.993c3.889 9.428-.555 19.999-9.444 23.999l-49.165 21.427c-9.165 4-19.443-.571-23.332-9.714l-53.053-129.136-86.664 89.138C18.729 472.71 0 463.554 0 447.977V18.299C0 1.899 19.921-6.096 30.277 5.443l284.412 292.542c11.472 11.179 3.007 31.141-12.5 31.141z", }), ]), m( "text", { x: 10, y: 45, style: { fontSize: 12, fill: options.theme === "dark" ? "white" : "gray", }, }, [options.name] ), ]) ); } public setPosition(pos: { x: number; y: number }) { this.g.setAttribute("transform", `translate(${pos.x}, ${pos.y})`); } public dispose(): void { this.g.remove(); } } interface HighlightInfo { color: string; cell: DrawioCell; } class Highlights { private readonly highlights = new Map< string, { info: HighlightInfo; instance: mxCellHighlight } >(); constructor(private readonly graph: DrawioGraph) {} private highlightInfoToStr(info: HighlightInfo): string { return JSON.stringify({ color: info.color, cell: info.cell.id }); } public updateHighlights(highlights: HighlightInfo[]): void { const set = new Set(highlights.map((h) => this.highlightInfoToStr(h))); for (const [key, h] of this.highlights) { if (!set.has(key)) { h.instance.destroy(); this.highlights.delete(key); } } for (const h of highlights) { const key = this.highlightInfoToStr(h); if (!this.highlights.has(key)) { const obj = { info: h, instance: new mxCellHighlight(this.graph, h.color, 8), }; this.highlights.set(key, obj); obj.instance.highlight(this.graph.view.getState(h.cell)); } } } } ================================================ FILE: drawio-custom-plugins/src/menu-entries.ts ================================================ import { showDialog } from "./propertiesDialog"; import { sendEvent } from "./vscode"; Draw.loadPlugin((ui) => { sendEvent({ event: "pluginLoaded", pluginId: "menu-entries" }); const importActionName = "vscode.import"; mxResources.parse(`${importActionName}=Import...`); ui.actions.addAction(importActionName, () => ui.importLocalFile(true)); const exportActionName = "vscode.export"; mxResources.parse(`${exportActionName}=Export...`); ui.actions.addAction(exportActionName, () => { sendEvent({ event: "invokeCommand", command: "export" }); }); const convertActionName = "vscode.convert"; mxResources.parse(`${convertActionName}=Convert...`); ui.actions.addAction(convertActionName, () => { sendEvent({ event: "invokeCommand", command: "convert" }); }); const saveActionName = "vscode.save"; mxResources.parse(`${saveActionName}=Save`); ui.actions.addAction(saveActionName, () => { sendEvent({ event: "invokeCommand", command: "save" }); }); const propertiesActionName = "properties"; ui.actions.addAction(propertiesActionName, () => { showDialog(ui); }); const menu = ui.menus.get("file"); const oldFunct = menu.funct; menu.funct = function (menu: any, parent: any) { oldFunct.apply(this, arguments); ui.menus.addMenuItems( menu, [ "-", propertiesActionName, "-", importActionName, exportActionName, convertActionName, "-", saveActionName, ], parent ); }; }); ================================================ FILE: drawio-custom-plugins/src/propertiesDialog.ts ================================================ import "./styles.css"; import * as m from "mithril"; export function showDialog(ui: DrawioUI) { const node = ui.fileNode; if (node == null) { return; } const initialScale = parseFloat(node.getAttribute("scale") || "1"); const initialBorder = parseFloat(node.getAttribute("border") || "0"); const initialLinkTarget = node.getAttribute("linkTarget"); const initialDisableSvgWarning = node.getAttribute("disableSvgWarning") === "true"; let scale = initialScale; let border = initialBorder; let linkTarget = initialLinkTarget; let disableSvgWarning = initialDisableSvgWarning; var div = document.createElement("div"); div.style.height = "100%"; m.render( div, m( "properties-dialog.div", { style: { fontFamily: "Segoe WPC,Segoe UI,sans-serif", display: "flex", flexDirection: "column", height: "100%", }, }, [ m( "div", { style: { display: "flex", flexDirection: "column", }, }, [ m( "h2", { style: { marginTop: "4px" } }, "Export Properties" ), m( "div", { style: { display: "flex", flexDirection: "row", paddingTop: "8px", paddingBottom: "4px", }, }, [ m("div", {}, mxResources.get("zoom") + ":"), m("div", { style: { flex: 1 } }), m("input", { value: scale * 100 + "%", oninput: (e: any) => { scale = Math.min( 20, Math.max( 0.01, parseInt(e.target.value) / 100 ) ); }, }), ] ), m( "div", { style: { display: "flex", flexDirection: "row", paddingBottom: "4px", }, }, [ m( "div", {}, mxResources.get("borderWidth") + ":" ), m("div", { style: { flex: 1 } }), m("input", { value: border, oninput: (e: any) => { border = Math.max( 0, parseInt(e.target.value) ); }, }), ] ), m( "div", { style: { display: "flex", flexDirection: "row", paddingBottom: "4px", }, }, [ m("div", {}, mxResources.get("links") + ":"), m("div", { style: { flex: 1 } }), m( "select.geBtn", { value: linkTarget || "", oninput: (e: any) => { linkTarget = e.target.value; }, }, [ m( "option", { value: "" }, mxResources.get("automatic") ), m( "option", { value: "_blank" }, mxResources.get("openInNewWindow") ), m( "option", { value: "_top" }, mxResources.get("openInThisWindow") ), ] ), ] ), m( "div", { style: { display: "flex", flexDirection: "row", paddingBottom: "4px", }, }, [ m("label", {}, [ m("input", { type: "checkbox", checked: disableSvgWarning, onchange: (e: any) => { disableSvgWarning = e.target.checked; }, }), "Disable SVG 1.1 warning", ]), ] ), ] ), m("div", { style: { flex: 1 } }), m("div", { style: { textAlign: "right" } }, [ m( "button.geBtn", { onclick: () => { ui.hideDialog(); }, }, [mxResources.get("cancel")] ), m( "button.geBtn.gePrimaryBtn", { onclick: () => { ui.hideDialog(); if ( scale === initialScale && border === initialBorder && linkTarget === initialLinkTarget && disableSvgWarning === initialDisableSvgWarning ) { return; } if (linkTarget) { node.setAttribute("linkTarget", linkTarget); } else { node.removeAttribute("linkTarget"); } node.setAttribute("scale", "" + scale); node.setAttribute("border", "" + border); if (disableSvgWarning) { node.setAttribute( "disableSvgWarning", "true" ); } else { node.removeAttribute("disableSvgWarning"); } ui.actions.get("save").funct(); }, }, [mxResources.get("apply")] ), ]), ] ) ); ui.showDialog(div, 350, 200, true, true); } ================================================ FILE: drawio-custom-plugins/src/styles.css ================================================ li { padding: 3px 0; } ================================================ FILE: drawio-custom-plugins/src/types.d.ts ================================================ declare type CustomDrawioAction = UpdateVerticesAction | AddVerticesAction | GetVerticesAction | LinkSelectedNodeWithDataAction | NodeSelectionEnabledAction | UpdateLiveshareViewState; declare type CustomDrawioEvent = NodeSelectedEvent | GetVerticesResultEvent | UpdateLocalStorage | PluginLoaded | CursorChangedEvent | SelectionChangedEvent | FocusChangedEvent | InvokeCommandEvent | SelectionRectangleChangedEvent; declare interface InvokeCommandEvent { event: "invokeCommand"; command: "export" | "save" | "convert"; } declare interface FocusChangedEvent { event: "focusChanged"; hasFocus: boolean; } declare interface NodeSelectionEnabledAction { action: "setNodeSelectionEnabled"; enabled: boolean; } declare interface UpdateVerticesAction { action: "updateVertices", verticesToUpdate: { id: string; label: string }[]; } declare interface AddVerticesAction { action: "addVertices"; vertices: { label: string }[]; } declare interface GetVerticesAction { action: "getVertices"; } declare interface LinkSelectedNodeWithDataAction { action: "linkSelectedNodeWithData"; linkedData: any; } declare interface NodeSelectedEvent { event: "nodeSelected"; linkedData: any; label: string; } declare interface GetVerticesResultEvent { event: "getVertices"; message: GetVerticesAction; vertices: { id: string; label: string }[]; } declare interface UpdateLocalStorage { event: "updateLocalStorage"; newLocalStorage: Record; } declare interface PluginLoaded { event: "pluginLoaded"; pluginId: string; } // Liveshare declare interface CursorChangedEvent { event: "cursorChanged"; position: { x: number, y: number } | undefined; } declare interface SelectionChangedEvent { event: "selectedCellsChanged"; selectedCellIds: string[]; } declare interface SelectionRectangleChangedEvent { event: "selectedRectangleChanged"; rect: Rectangle | undefined; } declare interface Rectangle { start: { x: number, y: number }, end: { x: number, y: number }, } declare interface UpdateLiveshareViewState { action: "updateLiveshareViewState"; cursors: ParticipantCursorInfo[]; selectedCells: ParticipantSelectedCellsInfo[]; selectedRectangles: ParticipantSelectedRectangleInfo[]; } declare interface ParticipantCursorInfo { id: string; position: { x: number, y: number }; label: string | undefined; color: string; } declare interface ParticipantSelectedCellsInfo { id: string; color: string; selectedCellIds: string[]; } declare interface ParticipantSelectedRectangleInfo { id: string; color: string; rectangle: Rectangle; } ================================================ FILE: drawio-custom-plugins/src/vscode.ts ================================================ export function sendEvent(data: CustomDrawioEvent) { if (window.opener) { window.opener.postMessage(JSON.stringify(data), "*"); } else { console.log("sending >>>", data); } } ================================================ FILE: drawio-custom-plugins/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es6", "outDir": "out", "lib": ["es6", "DOM"], "sourceMap": true, "rootDir": "./src", "strict": true, "experimentalDecorators": true }, "include": ["./src/**/*"] } ================================================ FILE: drawio-custom-plugins/webpack.config.ts ================================================ import * as webpack from "webpack"; import path = require("path"); import { CleanWebpackPlugin } from "clean-webpack-plugin"; const r = (file: string) => path.resolve(__dirname, file); module.exports = { target: "web", entry: r("./src/index"), output: { path: r("../dist/custom-drawio-plugins"), filename: "index.js", libraryTarget: "window", devtoolModuleFilenameTemplate: "../[resource-path]", }, devtool: "source-map", externals: { vscode: "commonjs vscode", }, resolve: { extensions: [".ts", ".js"], }, module: { rules: [ { test: /\.css$/, use: ["style-loader", "css-loader"] }, { test: /\.html$/i, loader: "raw-loader", }, { test: /\.ts$/, exclude: /node_modules/, use: [ { loader: "ts-loader", }, ], }, ], }, node: { __dirname: false, }, plugins: [new CleanWebpackPlugin()], } as webpack.Configuration; ================================================ FILE: examples/.vscode/settings.json ================================================ { "hediet.vscode-drawio.enableExperimentalFeatures": true, "hediet.vscode-drawio.offline": true, "hediet.vscode-drawio.plugins": [ { "file": "${workspaceFolder}/tooltips-plugin.js" } ], "hediet.vscode-drawio.defaultVertexStyle": { "fontColor": "#ff0000", "fontFamily": "Courier New", "fontSize": 18, "strokeWidth": 2, "strokeColor": "#ff0000" }, "hediet.vscode-drawio.defaultEdgeStyle": { "fontColor": "#0000ff", "fontFamily": "Courier New", "fontSize": 18, "strokeWidth": 2, "strokeColor": "#0000ff", "endArrow": "none", "startArrow": "none", "edgeStyle": "orthogonalEdgeStyle", "orthogonal": 1, "elbow": "vertical" }, "hediet.vscode-drawio.customColorSchemes": [ [ { "title": "Kind of pink", "fill": "#00ff00", "stroke": "none", "font": "#ffff00", "gradient": "#0000ff" }, { "fill": "#ffffba", "stroke": "none" }, { "fill": "#baffc9", "stroke": "none" }, { "fill": "#bae1ff", "stroke": "none" }, { "fill": "#eecbff", "stroke": "none" }, { "fill": "#a2798f", "stroke": "none" }, { "fill": "#8caba8", "stroke": "none" } ], [ { "fill": "#ffb3ba", "stroke": "none" }, { "fill": "#ffdfba", "stroke": "none" }, { "fill": "#ffffba", "stroke": "none" }, { "fill": "#baffc9", "stroke": "none" }, { "fill": "#bae1ff", "stroke": "none" }, { "fill": "#eecbff", "stroke": "none" }, { "fill": "#a2798f", "stroke": "none" }, { "fill": "#8caba8", "stroke": "none" } ] ], "hediet.vscode-drawio.presetColors": ["ff0000", "00ff00"], "hediet.vscode-drawio.colorNames": { "FF0000": "Red", "00FF00": "Green" }, "hediet.vscode-drawio.simpleLabels": false, "hediet.vscode-drawio.styles": [ {}, { "commonStyle": { "fontColor": "#5C5C5C", "strokeColor": "#006658", "fillColor": "#21C0A5" } }, { "commonStyle": { "fontColor": "#095C86", "strokeColor": "#AF45ED", "fillColor": "#F694C1" }, "edgeStyle": { "strokeColor": "#60E696" } }, { "commonStyle": { "fontColor": "#E4FDE1", "strokeColor": "#028090", "fillColor": "#F45B69" }, "graph": { "background": "#114B5F", "gridColor": "#0B3240" } } ] } ================================================ FILE: examples/formats/README.md ================================================ Draw.io diagrams can be embedded in markdown files! SVG Diagram: ![](./Example.dio.svg) PNG Diagram: ![](./Example.drawio.png) ================================================ FILE: examples/linking/demo-src/Baz.ts ================================================ class Baz { BazBar() { } } ================================================ FILE: examples/linking/demo-src/Foo.ts ================================================ class Foo extends Baz { test: string; } class Bar { } ================================================ FILE: examples/linking/demo-src/test.cc ================================================ void test() {} void main() {} template class MyClass { T method() {} }; template void myFunc(T a) { } ================================================ FILE: examples/linking/demo-src/tsconfig.json ================================================ { "include": [ "**/*" ] } ================================================ FILE: examples/linking/main.dio ================================================ ================================================ FILE: examples/temp/Example.drawio ================================================ ================================================ FILE: examples/temp/Large.drawio ================================================ ================================================ FILE: examples/temp/TestLibrary.xml ================================================ [{"xml":"jZFNbsMgEIVPw54YKfvGqdNNN+kJUDw2qGOPhSf+uX3HgJtmEalISMP35iHmoUzZLZdgB/dJNaAy78qUgYhT1S0lIKpC+1qZsyoKLVsV1Qv1EFU92AA9/8dQJMNk8Q6JXKEWcKIlKSOvmJVA976GzaiVOc3OM3wN9rapswwgzHEnE5wPUo4c6BtKQgrRbaq4RGk84hM/HvV2YUM9/+E6LuEWfdsLQ2g4t1W287gK+wCcgP3NijBB2Cp8y/1M25PyfKLB8jKjiHJAF6AOOKzSMvuaXc7JpBy1A9+63bZDOybQ/nofkUuRU9+Pj9+N2tPn/wA=","w":230,"h":130,"aspect":"fixed","title":"Red"},{"xml":"jZHdbsMgDIWfhnsapN6vdOlupl7sCVDjBjQnroibn7efA2RdLyrNEpL5jo+ED8rYbj5Fd/Of1AAq866MjUScu262gKgqHRpljqqqtBxV1S/UXVL1zUXo+T+GKhtGh3fI5Bxd34KwA81ZHHjBIka69w2sXq3MYfKB4evmLqs6yQ7CPHeyxHEn7cCRvsESUkxuU6cS5RoQn7i1+/3Kqec/XKcS7jC0vTCEK5ex2nUBF2EfgCNwuDgRRohrh29lnml9UllRNJhfxpRQyegE1AHHRUam0LAvUZkcpfYQWr/ZNuiGDNpf7yN1aUrw2/XxwUl7+v8f","w":230,"h":130,"aspect":"fixed","title":"Orange"},{"xml":"jZFNbsMgEIVPw56YEzROnW666ipLFI8N6thj4Yl/bt8x4CStFKlISMP35iHmoUzZLedgB/dJNaAy78qUgYhT1S0lIKpC+1qZkyoKLVsV1Qv1EFU92AA9/8dQJMNk8QaJXKSBZmFHWpI48opZDHTra9i8Wpnj7DzD12CvmzrLDMIcdzLE6SDlyIG+oSSkEN2mikuUxiP+4Xq7sKGen7iOS7hF3/bCEBrObZXtPK7CPgAnYH+1IkwQtgrfcj/T9qQ8omiwvIwpopzRGagDDqu0zL5ml6MyKUrtwLdut+3Qjgm0d+8jdSly8Pvx8cFR+/X/Pw==","w":230,"h":130,"aspect":"fixed","title":"Yellow"},{"xml":"jZHNbsMgEISfhjsBKfeY1Oklpz4BijcGdW0svPHP23cNuEkPkYpkafzNjsQOQptuuUQ7uGtoAIX+ENrEECirbjGAKJT0jdBnoZTkT6j6jXtIrhxshJ7+E1A5MFl8QCbVppSswpKtkVYsVgyPvoEtKYWuZucJvgZ729yZN2DmqOMVzgeWI8XwDSZgiCmt63TYuXvEF348GpN56OmFy3SYW/RtzwzhTmWstp3Hldkn4ATkb5aNCeKm8FTmKWxXKguyB8vbkhIqDV0gdEBx5ZHZN+RKUToXKR341u2xHdoxg/Y3++ycRal9/30+b/L+vP4P","w":230,"h":130,"aspect":"fixed","title":"Blue"},{"xml":"jZHdbsMgDIWfhnsanmClS3czadKeADVuQHXiiLj5efs5QNb1otIsIZnv+Ej4oIztlnN0g/+kBlCZd2VsJOLcdYsFRFXp0ChzUlWl5aiqfqEekqoHF6Hn/xiqbJgc3iGTr9DfhBxpydLIKxYp0r1vYHNqZY6zDwzfg7ts6iwbCPPcyQqng7QjR7qBJaSY3KZOJco1ID5xazOnnv9wnUq4w9D2whCuXMZq1wVchX0ATsDh4kSYIG4dvpV5pu1JZUHRYHkZUkIloTNQBxxXGZlDw74EZXKQ2kNo/W7boRszaH+9j8ylKbHv18f3Ju3p938A","w":230,"h":130,"aspect":"fixed","title":"Pink"},{"xml":"jZFJbsMwDEVPo71iLdpt46mbrHoCIWYsobRlyIyH25ca3KSLACUggHpfHxA/hSqHrfV6MhfXAQpVC1V65yh1w1YCoiik7YSqRFFIPqJoXqinqMpJexjpP4YiGRaNd0ik9bAzObstSTPtmCXv7mMHwSmFOq/GEnxN+hrUlSdgZmjgEaoTtzN59w2lQ+ejWzWxWLlZxCdev9dV8xa4G+mJy1jMNdp+ZIZwo/ys0YPFndkn4AJkr5qFBXzo8CO/Jxe+lAdkDbaXIUWUE2rBDUA+hLDajkwOSqUgpQHbm8N2QD0n0P96H5lzk2M/ro/1Ru3P9n8A","w":230,"h":130,"aspect":"fixed","title":"Grey"},{"xml":"jZE9bsMwDIVPo12xJo+N07hLp55AiGlLKG0aMuOf25eW5CYdApSAAOp7fID4pEzVr3Wwo/ukBlCZd2WqQMSp69cKEFWhfaPMRRWFlqOK6wv1FFU92gAD/8dQJMNs8Q6J1AFgEHSmNWkTb5i1QPehgd2qlTkvzjN8jfa2q4usIMxxLztcTtJOHOgbKkIK0W2usURpPeITL8u2Lcud08BPXMcSbtF3gzCElvPY1fYeN2EfgDOwv1kRZgh7h295nml/Ut5QNFhfphRRjqgG6oHDJiOLb9jlpExKUjvwnTtsB7RTAt2v9xG6NDn34/r436j9+f4f","w":230,"h":130,"aspect":"fixed","title":"Green"},{"xml":"zVTBTsMwDP2a3LsGTXBkHR0S4oTEPTReE+E0Vept7d/jthlb1U1wAESkSu7ze7H9LEXIzLWboGrz7DWgkA9CZsF7GiPXZoAo0sRqIdciTRP+RJpfyS6GbFKrABV9R5COgr3CHYzICDTUYQTK4Hd1pEEgaCelCl9VUJB6O9KTeQsDNdbfgHdAoWPKwWoykXEbZQZsaaLseJVqYiOf0tNAHMSZLs8nZ/M9QTcbkSesNPSSRMjVwViCl1oVffbAq2HMkOMS6wWHDQX/DplHHwa1TIbDma1FPMPzfLkccV/RJb5CW1aMIWwp0nLlLHaMPQLugWyhONHbzhHeR76zWvetr65u5dz99Ev3737J/JuZ+a9D+Ff29+e/2t9Orf/5bfDv6RkZcpNX5gM=","w":180,"h":20,"aspect":"fixed","title":"Key-Value"}] ================================================ FILE: examples/tooltips-plugin.js ================================================ /** * Sample plugin. */ Draw.loadPlugin(function (ui) { var graph = ui.editor.graph; function updateOverlays(cell) { var tooltip = cell.getAttribute("tooltip"); if (tooltip != null && tooltip.length > 0) { var overlays = graph.getCellOverlays(cell); if (overlays == null) { // Creates a new overlay with an image var overlay = new mxCellOverlay( new mxImage( "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAJGUlEQVR42u1ZCXAT1xn+38qWkGVL8iXbWNjGNhiDiQl13KFMuVqImbRpQ+iEpiVt2lyTtmkLYShJmzadkEmPaWiT4hDaaUNnGujBMSHU9ZQESDldsAEbgw3G8i350n2sVvv6v9VhCUuy8RjszGTH67fa/fft9/3v+//3v10CH/ONTDWATwhMNYDpQYAQqSupGbNLCpT628DBFBAgHIJFwIly0BSWgSZ/HlHlFoEqOw8UmnQi12QQwnFJaCkP3MHj7vQMmyhvM1OHsQMc3W1gNVyl5huXQPS4kUuQ0O2RGj8BBhiBJ+uLIWfJWqJbvALSSiu0nHxGJV75FPa0AI2KsdVjm453JATQBJ8h4M9BPNOF7XVsm3Aszote/py5tcFsunAMes/UUGtbE/IQxz06YxNA0DK5AvQrHob8qq+R1JKKfCLjvoJXvoiE7gt4mT2NC9xBY/Qbfj4cHY+A67B9j4ri3603Gg2G2r/Sjv/sBcHlQEtxggTQ41yiAmZ/4XEoXv9dboZWV4XAv4cXVuFV2S33TkiKTIaUhlzNWh82H1Cf+AbvMNfcOPi22La/GgS3I+aIRH8wx0Hmvcth4dOvEpTMGtTzdjx7L3siie3hSdnoSJTX48i86DJ11zb94SXac/IwgDh6NMitP5nXSx//Mcx+8IlsjpP9DkGvY+K/2/nWn6ikYNiP0nqu8+jf+hrfegFlZY8YjRFc6Ft5Shos3voWyVy0bCVwsr/gxayAjT9HTkLai/TXmH1KaQn/GUH0bTS3Xvyw7pVvUvdgb+g+EuSRmKKFJdv/QdRFZY9h4O4iLDjJNJnnECxlqZjSpx09bXtObVtHPQESEkImm4qfvMNSIwO/G4HfGqTSj0n2/+32yWLDh/uT5pb6PWdeXE+ZnAgL2DkbNsHcDZtX4PG/MEzlMH1LDJa0cCTEtYaaPccaq18AkjxrDizdUauVKZIuIupZU41wXCwAOkXBW3522zozKX++msxc9uVfoed/IE21sbdpoKLQJmJm2jF4+eQWsmZ/R5YsUd6KJ1UwHumw6A9mj7GCPNwmin0I/Rh20XrG3YFymkOqDvU8hzf8hgTTZQxrtjntLui6aQKP2wva9GTIzddhtuVCYMJtPS4eOm8aweVwQ4pWBfqCLEhIlEW1FXhBsrVZnaBKVsKswiyQKxIjbKO4ht2+iRE4ggdV8Viz2d4yZIf//feK9DCpsMP707NTYfGSecCmORK4n9m6HB44e7wRSXj8EPBPrVFB5bIFIEMS4bZe7O8c2tptruBwgFI1Az69vAwUSnnINioFgBpy/6GebjzIGUsKdR9dgaF+a+QsiMDvqZwLWblpEaCazt+AbkP/iC3xi6XkngIoKM6JkMv15i640dzpJxpmn1ecDfPQnsSXUy9Zc6BLYPNsPOGJIoXjR86D1+Mdda2wVA9FuIcIoO2ZY5fBNuwYZZuTlwllFUURZC+eaQFTz9Ao29QMNVR8dj7ELmKk2Y2S1f80CCEhxxwBgDMfNKBGPZFTP/ZdWp4P+sLIAWw41Qz9RuuoMqFwbjYUlRVEnLvW0AYdbabI5yHBnFwtlFWWxIWF5YVIPr+3pQ/kSh0ZIwaMhj5orG/HYxLKQqqkBKhcuRDXC/IIr5pNg3DhdCuO3IitPJGgbRnMUCVF2DqtNqg73gxeYSS7yTDWK5aWQEq6NqaEpCpc8JjIqncaakly2udIHAY0sNQb6OiDznYj8LwPNBolzJ6fDwqVKuA0EmYLYO4zgaG1D9wou2QVritQZklabVRb+9AwtF/tAoeDB6UyEQrmzgS1LiPCNgomSt22o2R59Yktsoy81/xLxjgkArIDUWDTCC5pEqR1Q0BJMWx9/hqeuZTNkVL2imHLKmefz98n6xviTEqBVZBo7vsRWfp6jT4hNfsal5w6g9ztGmiCJbq0/Hc73D5Lfwkp2biNZFau3pmQmfckkSWQWGM2beoI5nzchcHu3eam08+SpOx8WLT59zpZsuaSLF2v8ztmehajwZgRrQMmn324/NIbm4yEaXPW6g0kb+03HuBUmgOcRiebjiRooEaiTqvPZzY91Hvi4PttB6qpHyUGTem3fkpS59/3fYyFX3PqTKkuilzRTMmScoQAs3TZqGgxPm+72fzbxp1bqci7R4JWpkyBBc+8QpLz5v6QJGl+wUYilDOmajTCyIn2YZ9oH9zqMna83rhzG/VaBiVaYcgIyJJSoGTjVqIpWfwQkSv/yGmzNRjYNG5+vVPY/QQoFX1EtJgs1G3/tt1w7cC1P2+nvGUgaBElbaKc8tY+RmaueHg2lo67ZOr0lUSpDr25nVQiUSREgw2eF50WKtqGjuHc85Tp7L9vth/chbLxQHjuio4HA1tdVAYF657llLq8R3A0fs6p0wuxlTIfGXkVPUnupiOQmNddVhAd5jbq5V/ih4372g+9LQ43nY36mjEOCvb2ORF0lffDzFXr5XJt5qNEnvQml5qtJP4FwOQADrqTd7PSAKjLXo+yeVOwmd/t/egg33fqPent9e29Woyw8L+tK/rqZi5t4WeOcmm5y4giaXzoI5eKNJibKavPBR6wPgfKu0TKO1uwjDiMNvsc3dfr+8/V0v56VI7HNWaWGh8QlJS+6utk5qpHjnDpuWuYlGJW6YF3pxh0lHocNLRQYQsF0eehPsGImr6O55rw5HnK8yftnS0GS2sDDF0+Td0DXQH7yXq9zjYssEqfeY1Lnj2/WZZZUIyBHpmXwiUh8KJoG3BRj/Nln9N22DMs1fqoAbALLrtVcFgEfniAuvu7wNlnoM5eLNEFL0z0i824JJRSXA4lT7xchp5v4NJnkVuKPv9jeRfFrCGAx7EXNfyzwYYThs73/0QFuzlieCLGaRKqq/gEEKp6ziIoenRLQoJKfYCodQ8QZYqUhSiThNcN6GnAyrALfN53Efhuy9XzbT1H91FH1/UxP05MxhaXgEo/B+Z955d6TpawA5edXyIKJcEAdIHg7QTRewUdWIf+/9BpNFwwN53zDdYfQ2l0T+hb1x0hoMjIgfwHn2If89Tg/+YlUJzQvfZh6hkygqvXAI6uFv/MeBuBd9cI+C1ive+aPB3fWQLTfPuEwFRvH3sC/weRVVoCD7mx+AAAAABJRU5ErkJggg==", 62, 62 ), null, mxConstants.ALIGN_RIGHT, mxConstants.ALIGN_TOP, null, "default" ); // Sets the overlay for the cell in the graph graph.addCellOverlay(cell, overlay); } } else { graph.removeCellOverlays(cell); } } function refresh() { var cells = graph.model.cells; for (var id in cells) { updateOverlays(cells[id]); } } graph.addListener(mxEvent.SIZE, refresh); refresh(); }); ================================================ FILE: examples/use-cases/class-diagrams.dio ================================================ ================================================ FILE: examples/use-cases/cloud-architecture.drawio ================================================ ================================================ FILE: examples/use-cases/packages.dio ================================================ ================================================ FILE: examples/use-cases/screenshots.dio ================================================ ================================================ FILE: examples/use-cases/wireframes.dio ================================================ ================================================ FILE: package.json ================================================ { "name": "vscode-drawio", "private": true, "displayName": "Draw.io Integration", "description": "This unofficial extension integrates Draw.io into VS Code.", "version": "1.9.0", "preRelease": false, "license": "GPL-3.0", "publisher": "hediet", "keywords": [ "drawio", "diagram", "diagrams.net", "visio", "architecture", "uml", "code link" ], "author": { "email": "henning.dieterichs@live.de", "name": "Henning Dieterichs" }, "readme": "./README.md", "icon": "docs/logo.drawio.png", "extensionKind": [ "ui", "workspace" ], "engines": { "vscode": "^1.70.0" }, "categories": [ "Visualization" ], "activationEvents": [], "repository": { "url": "https://github.com/hediet/vscode-drawio.git" }, "browser": "./dist/extension/index", "main": "./dist/extension/index", "capabilities": { "untrustedWorkspaces": { "supported": true } }, "contributes": { "languages": [ { "id": "drawio", "extensions": [ ".drawio", ".dio", ".dio.svg", ".drawio.svg", ".drawio.png", ".dio.png" ], "aliases": [ "Draw.io", "drawio" ] } ], "customEditors": [ { "viewType": "hediet.vscode-drawio", "displayName": "Draw.io (Binary)", "selector": [ { "filenamePattern": "*.drawio.png" }, { "filenamePattern": "*.dio.png" } ], "priority": "default" }, { "viewType": "hediet.vscode-drawio-text", "displayName": "Draw.io", "selector": [ { "filenamePattern": "*.drawio" }, { "filenamePattern": "*.dio" }, { "filenamePattern": "*.dio.svg" }, { "filenamePattern": "*.drawio.svg" } ], "priority": "default" } ], "commands": [ { "command": "hediet.vscode-drawio.convert", "title": "Convert To...", "category": "Draw.io" }, { "command": "hediet.vscode-drawio.export", "title": "Export To...", "category": "Draw.io" }, { "command": "hediet.vscode-drawio.toggleCodeLinkActivation", "title": "Toggle Code Link Activation", "category": "Draw.io" }, { "command": "hediet.vscode-drawio.changeTheme", "title": "Change Theme", "category": "Draw.io" }, { "command": "hediet.vscode-drawio.editDiagramAsText", "title": "Edit Diagram As Text (Experimental)", "enablement": "hediet.vscode-drawio.experimentalFeaturesEnabled", "category": "Draw.io" }, { "command": "hediet.vscode-drawio.linkCodeWithSelectedNode", "title": "Link Code with Selected Node", "category": "Draw.io" }, { "command": "hediet.vscode-drawio.newDiagram", "title": "New Draw.io Diagram", "category": "Draw.io" }, { "command": "hediet.vscode-drawio.linkFileWithSelectedNode", "title": "Link File With Selected Node", "category": "Draw.io" }, { "command": "hediet.vscode-drawio.linkSymbolWithSelectedNode", "title": "Link Symbol With Selected Node", "category": "Draw.io" }, { "command": "hediet.vscode-drawio.linkWsSymbolWithSelectedNode", "title": "Link Workspace Symbol With Selected Node", "category": "Draw.io" } ], "keybindings": [ { "command": "hediet.vscode-drawio.linkCodeWithSelectedNode", "key": "shift+F3", "when": "!findWidgetVisible" } ], "configuration": [ { "title": "General", "order": 10, "properties": { "hediet.vscode-drawio.offline": { "type": "boolean", "default": true, "title": "Use Offline Mode", "description": "When enabled, the bundled instance of Draw.io is used.", "order": 10 }, "hediet.vscode-drawio.online-url": { "type": "string", "default": "https://embed.diagrams.net/", "title": "Online URL", "description": "The app to use when offline mode is disabled.", "order": 11 }, "hediet.vscode-drawio.codeLinkActivated": { "type": "boolean", "default": false, "title": "Code Link Enabled", "description": "When activated, selecting a node will navigate to an associated code section.", "order": 20 }, "hediet.vscode-drawio.local-storage": { "anyOf": [ { "type": "object" }, { "type": "string" } ], "default": {}, "readOnly": true, "title": "Draw.io Local Storage", "description": "Only change this property if you know what you are doing. Manual changes to this property are not supported!", "order": 30 }, "hediet.vscode-drawio.simpleLabels": { "type": "boolean", "default": false, "title": "Use SimpleLabels", "description": "When enabled, no ForeignObjects are used in the svg.", "order": 40 }, "hediet.vscode-drawio.zoomFactor": { "type": "number", "default": 1.2, "title": "Draw.io zoom factor", "description": "Defines the zoom factor for mouse wheel and trackpad zoom.", "order": 50 }, "hediet.vscode-drawio.globalVars": { "type": "object", "title": "Draw.io global variables.", "description": "Defines global variables for system-wide placeholders using a JSON structure with key, value pairs. Keep the number of global variables small.", "order": 60 }, "hediet.vscode-drawio.resizeImages": { "type": [ "boolean", "null" ], "title": "Draw.io resize images.", "description": "If set to true, images are resized automatically on paste. If not defined, the user will be asked to confirm the resize.", "default": null, "order": 70 } } }, { "title": "Plugins", "order": 20, "properties": { "hediet.vscode-drawio.plugins": { "type": "array", "items": { "anyOf": [ { "type": "object", "properties": { "file": { "type": "string", "description": "The file path to the library. Must be absolute. You can use `${workspaceFolder}`." } } } ] }, "default": [], "title": "Plugins", "markdownDescription": "Loads Draw.io plugins from the local filesystem. See description of the `file` property. See [plugins documentation](https://github.com/hediet/vscode-drawio/blob/master/docs/plugins.md).", "order": 10 }, "hediet.vscode-drawio.knownPlugins": { "type": "array", "items": { "type": "object", "properties": { "pluginId": { "type": "string" }, "fingerprint": { "type": "string" }, "allowed": { "type": "boolean" } } }, "default": [], "title": "Known Plugins", "markdownDescription": "List of allowed or denied plugins. The extension will read and write to this list based on what the used decides when loading specific plugins. See [plugins documentation](https://github.com/hediet/vscode-drawio/blob/master/docs/plugins.md).", "scope": "application", "order": 15 }, "hediet.vscode-drawio.customLibraries": { "type": "array", "items": { "type": "object", "properties": { "entryId": { "type": "string", "description": "The id of the entry. A specfic entry can be enabled or deactivated in the editor." }, "libName": { "type": "string", "description": "The name of the library in the shape overview." } }, "anyOf": [ { "type": "object", "properties": { "url": { "type": "string" } } }, { "type": "object", "properties": { "xml": { "type": "string" } } }, { "type": "object", "properties": { "json": {} } }, { "type": "object", "properties": { "file": { "type": "string", "description": "The file path to the library. Must be absolute. You can use ${workspaceFolder}." } } } ] }, "default": [], "title": "Custom Libraries", "description": "Configures the Draw.io editor to use custom libraries.", "order": 50 } } }, { "title": "Theme and styles", "order": 30, "properties": { "hediet.vscode-drawio.styles": { "title": "Styles", "description": "Defines an array of objects that contain the colours (fontColor, fillColor, strokeColor and gradientColor) for the Style tab of the format panel if the selection is empty. These objects can have a commonStyle (which is applied to both vertices and edges), vertexStyle (applied to vertices) and edgeStyle (applied to edges), and a graph with background and gridColor. An empty object means apply the default colors", "type": "array", "items": { "type": "object", "properties": { "commonStyle": { "type": "object", "properties": { "fontColor": { "type": "string" }, "strokeColor": { "type": "string" }, "fillColor": { "type": "string" } } }, "graph": { "type": "object", "properties": { "background": { "type": "string" }, "gridColor": { "type": "string" } } } } } }, "hediet.vscode-drawio.defaultVertexStyle": { "title": "Default Vertex Style", "description": "Default styling of vertices (shapes).", "type": "object" }, "hediet.vscode-drawio.defaultEdgeStyle": { "title": "Default Edge Style", "description": "Default styling of edges.", "type": "object" }, "hediet.vscode-drawio.colorNames": { "title": "Color Names", "description": "Names for colors, eg. {‘FFFFFF’: ‘White’, ‘000000’: ‘Black’} that are used as tooltips (uppercase, no leading # for the colour codes)", "type": "object" }, "hediet.vscode-drawio.presetColors": { "title": "Preset Colors", "description": "Color codes for the upper palette in the color dialog.", "type": "array", "items": { "type": "string", "description": "Use hex codes without # at the beginning only (FFFFFF for absolute white, for example)." } }, "hediet.vscode-drawio.customColorSchemes": { "title": "Custom Color Schemes", "markdownDescription": "Available color schemes in the style section at the top of the format panel. See example [here](https://www.diagrams.net/doc/faq/custom-colours-confluence-cloud#default-colour-schemes---format-panel)", "type": "array", "items": { "type": "array", "description": "Represents a page of color schemes.", "items": { "type": "object", "properties": { "title": { "type": "string", "description": "Title of the color used in tooltips." }, "fill": { "type": "string", "description": "Use hex codes with # at the beginning (#FFFFFF for absolute white, for example)." }, "stroke": { "type": "string", "description": "Use hex codes with # at the beginning (#FFFFFF for absolute white, for example)." }, "gradient": { "type": "string", "description": "Use hex codes with # at the beginning (#FFFFFF for absolute white, for example)." }, "font": { "type": "string", "description": "Use hex codes with # at the beginning (#FFFFFF for absolute white, for example)." } } } } }, "hediet.vscode-drawio.customFonts": { "type": "array", "items": { "type": "string" }, "default": [], "title": "Custom Fonts", "description": "Configures the Draw.io editor to use custom fonts." }, "hediet.vscode-drawio.theme": { "title": "Draw.io Theme", "type": "string", "default": "kennedy", "enum": [ "kennedy", "min" ], "description": "The theme to use for the Draw.io editor. Use \"automatic\" to automatically choose a Draw.io theme that matches your current VS Code theme." }, "hediet.vscode-drawio.appearance": { "title": "Draw.io Appearance", "type": "string", "default": "light", "enum": [ "automatic", "light", "dark", "high-contrast-light", "high-contrast" ], "description": "The appearance to use for the Draw.io editor. Use \"automatic\" to automatically choose a Draw.io theme that matches your current VS Code theme." } } } ], "menus": { "explorer/context": [ { "when": "hediet.vscode-drawio.active", "command": "hediet.vscode-drawio.linkFileWithSelectedNode", "group": "Draw.io" } ], "commandPalette": [ { "when": "false", "command": "hediet.vscode-drawio.linkFileWithSelectedNode" } ] } }, "scripts": { "run-script": "node ./scripts/run-script", "lint": "echo 'add linting'", "_lint": "yarn run-script check-version && prettier --check ./src", "build": "yarn build-extension && yarn build-plugins && yarn package-extension", "build-extension": "webpack --mode production", "build-plugins": "webpack --mode production --config ./drawio-custom-plugins/webpack.config.ts", "package-extension": "yarn package-extension-preRelease", "package-extension-stable": "vsce package --yarn --out ./dist/extension.vsix", "package-extension-preRelease": "vsce package --yarn --out ./dist/extension.vsix --pre-release", "dev": "webpack --mode development --watch", "dev-drawio-plugins": "webpack --mode development --watch --config ./drawio-custom-plugins/webpack.config.ts", "dev-drawio-plugins-web": "webpack-dev-server --hot --config ./drawio-custom-plugins/webpack.config.ts" }, "files": [ "dist/custom-drawio-plugins/**/*", "dist/extension/**/*", "package.json", "docs/**/*", "drawio/LICENSE", "drawio/VERSION", "drawio/src/main/webapp/js/**/*", "drawio/src/main/webapp/connect/**/*", "drawio/src/main/webapp/images/**/*", "drawio/src/main/webapp/img/**/*", "drawio/src/main/webapp/math/**/*", "drawio/src/main/webapp/mxgraph/**/*", "drawio/src/main/webapp/plugins/**/*", "drawio/src/main/webapp/resources/**/*", "drawio/src/main/webapp/styles/**/*", "drawio/src/main/webapp/templates/**/*", "LICENSE.md" ], "devDependencies": { "@actions/exec": "^1.0.4", "@actions/github": "^2.2.0", "@hediet/semver": "^0.2.1", "@types/copy-webpack-plugin": "^8.0.1", "@types/mithril": "^2.0.4", "@types/node": "^22.10.10", "@types/vscode": "1.70.0", "@types/xml-formatter": "^1.1.0", "@vscode/vsce": "^3.2.1", "clean-webpack-plugin": "^4.0.0-alpha.0", "copy-webpack-plugin": "^9.0.1", "css-loader": "^3.4.2", "ovsx": "^0.1.0-next.e000fdb", "prettier": "^2.3.2", "raw-loader": "^4.0.1", "style-loader": "^1.1.3", "ts-loader": "^9.2.3", "ts-node": "^10.1.0", "tslint": "^6.1.2", "typescript": "^5.7.3", "webpack": "^5.44.0", "webpack-cli": "^4.7.2", "webpack-dev-server": "^3.11.2" }, "dependencies": { "@hediet/json-to-dictionary": "^0.2.1", "@hediet/std": "0.6.0", "@knuddels/mobx-logger": "^1.1.1", "buffer": "^6.0.3", "js-sha256": "^0.9.0", "mithril": "^2.0.4", "mobx": "5.15.4", "mobx-utils": "5.5.7", "path-browserify": "^1.0.1", "vsls": "^1.0.3015", "xml-formatter": "^2.0.1", "xml-parser-xo": "^3.0.0" } } ================================================ FILE: scripts/build-and-publish.ts ================================================ import { readFile, writeFile } from "fs/promises"; import { join, resolve } from "path"; import { SemanticVersion } from "@hediet/semver"; import { context } from "@actions/github"; import { exec } from "@actions/exec"; import { readFileSync } from "fs"; import { Changelog } from "./changelog"; const packageJsonPath = resolve(__dirname, "../package.json"); export async function run(): Promise { const changeLog = getChangelog(); const version = changeLog.latestVersion; const packageJson = await readJsonFile<{ version: string; preRelease?: boolean }>(packageJsonPath); const stableVersion = SemanticVersion.parse(packageJson.version); if (version.kind !== 'released' || version.version.toString() !== stableVersion.toString()) { throw new Error("Version in package.json does not match latest version in changelog."); } if (stableVersion.patch !== 0) { throw new Error("Patch version must be 0."); } const gh = new GitHubClient(); const isPreRelease = !!packageJson.preRelease; const stableTag = `v${stableVersion}`; const stableGhTagExists = await gh.tagExists(context.repo, stableTag); if (!isPreRelease && !stableGhTagExists) { await buildAndPublish('stable', stableVersion); await gh.createTag(context.repo, stableTag, context.sha); } else { if (stableGhTagExists) { console.log(`GitHub tag for stable version ${stableTag} exists, skipping publish.`); } if (isPreRelease) { console.log("Pre-release version detected, skipping stable"); } } const runNumber = process.env.GITHUB_RUN_NUMBER; const preReleaseNumber = getPreReleasePatchNumber(Number(runNumber)); const previewVersion = stableVersion.with({ patch: preReleaseNumber }); const previewTag = `v${previewVersion}`; const previewGhTagExists = await gh.tagExists(context.repo, previewTag); if (!previewGhTagExists) { await buildAndPublish('preRelease', previewVersion); await gh.createTag(context.repo, previewTag, context.sha); } else { console.log(`GitHub tag for preview version ${previewTag} exists, skipping publish.`); } } async function buildAndPublish(releaseType: 'stable' | 'preRelease', version: SemanticVersion) { console.log(`Publishing ${releaseType} version ${version}...`); const packageJson = await readJsonFile(packageJsonPath); packageJson.version = version.toString(); packageJson.scripts["package-extension"] = releaseType === 'preRelease' ? "yarn package-extension-preRelease" : "yarn package-extension-stable"; await writeJsonFile(packageJsonPath, packageJson); await exec("yarn", ["build"]); await exec("yarn", [ "vsce", "publish", "--pat", process.env.VSCE_PAT!, ...(releaseType === 'preRelease' ? ['--pre-release'] : [])] ); } function getPreReleasePatchNumber(runNumber: number): number { return Number(`${formatDate(new Date())}${padN(Number(runNumber) % 1000, 3)}`); } function formatDate(date: Date): string { const year = date.getUTCFullYear() % 100; const month = date.getUTCMonth() + 1; const day = date.getUTCDate(); return `${year}${padN(month, 2)}${padN(day, 2)}`; } function padN(num: number, n: number): string { return num.toString().padStart(n, '0'); } interface IRepo { owner: string; repo: string; } class GitHubClient { private readonly token: string = process.env.GH_TOKEN!; private async request(endpoint: string, options: RequestInit = {}) { const response = await fetch(`https://api.github.com${endpoint}`, { ...options, headers: { ...options.headers, Authorization: `token ${this.token}`, 'Content-Type': 'application/json', }, }); if (!response.ok) { throw new Error(`GitHub API request failed: ${response.statusText}, ${await response.text()}`); } return response.json(); } async tagExists(repo: IRepo, tag: string): Promise { try { await this.request(`/repos/${repo.owner}/${repo.repo}/git/refs/tags/${tag}`); return true; } catch { return false; } } async createTag(repo: IRepo, tag: string, sha: string): Promise { await this.request(`/repos/${repo.owner}/${repo.repo}/git/refs`, { method: 'POST', body: JSON.stringify({ ref: `refs/tags/${tag}`, sha, }), }); } } async function readJsonFile(path: string): Promise { const content = await readFile(path, 'utf-8'); return JSON.parse(content); } async function writeJsonFile(path: string, data: unknown): Promise { const content = JSON.stringify(data, null, '\t'); await writeFile(path, content, 'utf-8'); } function readTextFileSync(fileName: string): string { return readFileSync(fileName, { encoding: "utf-8" }); } function getChangelog(): Changelog { return new Changelog(readTextFileSync(join(__dirname, "../CHANGELOG.md"))); } ================================================ FILE: scripts/changelog.ts ================================================ import { SemanticVersion } from "@hediet/semver"; export class Changelog { private readonly regex = /## \[(.*?)\]([ \t]*-[ \t]*(.*))?/; public get latestVersion(): { kind: "released"; version: SemanticVersion; releaseDate: Date | undefined; } | { kind: "unreleased"; } { const result = this.regex.exec(this.src); if (!result) { throw new Error("Invalid changelog"); } if (result[1].toLowerCase() === "unreleased") { return { kind: "unreleased" }; } let date: Date | undefined; if (result[3]) { date = new Date(result[3].trim()); } return { kind: "released", version: SemanticVersion.parse(result[1]), releaseDate: date, }; } constructor(private src: string) { } public setLatestVersion( newVersion: SemanticVersion, releaseDate: Date | undefined ): void { let dateStr = ""; if (releaseDate !== undefined) { const year = releaseDate.getUTCFullYear(); const day = releaseDate.getUTCDate(); const month = releaseDate.getUTCMonth() + 1; dateStr = ` - ${pad(year, 4)}-${pad(month, 2)}-${pad(day, 2)}`; } this.src = this.src.replace( this.regex, `## [${newVersion.toString()}]${dateStr}` ); } public toString(): string { return this.src; } } function pad(num: number, size: number): string { let s = num + ""; while (s.length < size) s = "0" + s; return s; } ================================================ FILE: scripts/run-script.js ================================================ require("ts-node").register({ transpileOnly: true }); const argv = process.argv.slice(2); require(`./${argv[0]}`) .run(argv.slice(1)) .catch((e) => { console.error(e); process.exit(1); }); ================================================ FILE: scripts/tsconfig.json ================================================ { "compilerOptions": { "target": "esnext", "module": "commonjs", "strict": true, "noEmit": true, "experimentalDecorators": true }, "include": ["./**/*"] } ================================================ FILE: src/Config.ts ================================================ import { autorun, computed, observable } from "mobx"; import { ColorTheme, ColorThemeKind, commands, ConfigurationTarget, env, Memento, Uri, window, workspace, } from "vscode"; import { Style, ColorScheme, DrawioLibraryData } from "./DrawioClient"; import { BufferImpl } from "./utils/buffer"; import { SimpleTemplate } from "./utils/SimpleTemplate"; import { serializerWithDefault, VsCodeSetting, } from "./vscode-utils/VsCodeSetting"; import * as packageJson from "../package.json"; const extensionId = "hediet.vscode-drawio"; const experimentalFeaturesEnabled = "vscode-drawio.experimentalFeaturesEnabled"; export async function setContext( key: string, value: string | boolean ): Promise { return (await commands.executeCommand("setContext", key, value)) as any; } export class Config { public readonly packageJson: { version: string; versionName?: string; name: string; feedbackUrl?: string; } = packageJson; public get feedbackUrl(): Uri | undefined { if (this.packageJson.feedbackUrl) { return Uri.parse(this.packageJson.feedbackUrl); } return undefined; } public get isInsiders() { return ( this.packageJson.name === "vscode-drawio-insiders-build" || process.env.DEV === "1" ); } @observable.ref private _vscodeTheme: ColorTheme; public get vscodeTheme(): ColorTheme { return this._vscodeTheme; } constructor(private readonly globalState: Memento) { autorun(() => { setContext( experimentalFeaturesEnabled, this.experimentalFeaturesEnabled ); }); this._vscodeTheme = window.activeColorTheme; window.onDidChangeActiveColorTheme((theme) => { this._vscodeTheme = theme; }); } public getDiagramConfig(uri: Uri): DiagramConfig { return new DiagramConfig(uri, this, this.globalState); } private readonly _experimentalFeatures = new VsCodeSetting( `${extensionId}.enableExperimentalFeatures`, { serializer: serializerWithDefault(false), } ); public get experimentalFeaturesEnabled(): boolean { return this._experimentalFeatures.get(); } public get canAskForFeedback(): boolean { if ( this.getInternalConfig().versionLastAskedForFeedback === this.packageJson.version ) { return false; } const secondsIn20Minutes = 60 * 20; if ( this.getInternalConfig().thisVersionUsageTimeInSeconds < secondsIn20Minutes ) { return false; } return true; } public async markAskedToTest(): Promise { await this.updateInternalConfig((config) => ({ ...config, versionLastAskedForFeedback: this.packageJson.version, })); } private readonly _knownPlugins = new VsCodeSetting< { pluginId: string; fingerprint: string; allowed: boolean }[] >(`${extensionId}.knownPlugins`, { serializer: serializerWithDefault([]), // Don't use workspace settings here! target: ConfigurationTarget.Global, }); public isPluginAllowed( pluginId: string, fingerprint: string ): boolean | undefined { const data = this._knownPlugins.get(); const entry = data.find( (d) => d.pluginId === pluginId && d.fingerprint === fingerprint ); if (!entry) { return undefined; } return entry.allowed; } public async addKnownPlugin( pluginId: string, fingerprint: string, allowed: boolean ): Promise { const plugins = [...this._knownPlugins.get()].filter( (p) => p.pluginId !== pluginId || p.fingerprint !== fingerprint ); plugins.push({ pluginId, fingerprint, allowed }); await this._knownPlugins.set(plugins); } public getUsageTimeInSeconds(): number { return this.getInternalConfig().totalUsageTimeInSeconds; } public getUsageTimeOfThisVersionInSeconds(): number { return this.getInternalConfig().thisVersionUsageTimeInSeconds; } public addUsageTime10Seconds(): void { this.updateInternalConfig((config) => { if (config.currentVersion !== this.packageJson.version) { config.currentVersion = this.packageJson.version; config.thisVersionUsageTimeInSeconds = 0; } return { ...config, totalUsageTimeInSeconds: config.totalUsageTimeInSeconds + 10, thisVersionUsageTimeInSeconds: config.thisVersionUsageTimeInSeconds + 10, }; }); } public markAskedForSponsorship(): void { this.updateInternalConfig((c) => ({ ...c, dateTimeLastAskedForSponsorship: new Date().toDateString(), totalUsageTimeLastAskedForSponsorshipInSeconds: c.totalUsageTimeInSeconds, })); } public get canAskForSponsorship(): boolean { const c = this.getInternalConfig(); if (c.dateTimeLastAskedForSponsorship) { const d = new Date(c.dateTimeLastAskedForSponsorship); const msPerDay = 1000 * 60 * 60 * 24; const minTimeBetweenAskingMs = 180 * msPerDay; if (new Date().getTime() - d.getTime() < minTimeBetweenAskingMs) { return false; } } let usageTimeSinceLastAskedForSponsorship = c.totalUsageTimeInSeconds; if (c.totalUsageTimeLastAskedForSponsorshipInSeconds !== undefined) { usageTimeSinceLastAskedForSponsorship -= c.totalUsageTimeLastAskedForSponsorshipInSeconds; } const secondsIn1Hr = 60 * 60; const minUsageTime = secondsIn1Hr; if (usageTimeSinceLastAskedForSponsorship < minUsageTime) { return false; } return true; } private getInternalConfig(): InternalConfig { return ( this.globalState.get("config") || { totalUsageTimeInSeconds: 0, thisVersionUsageTimeInSeconds: 0, versionLastAskedForFeedback: undefined, dateTimeLastAskedForSponsorship: undefined, currentVersion: this.packageJson.version, totalUsageTimeLastAskedForSponsorshipInSeconds: 0, } ); } private async setInternalConfig(config: InternalConfig): Promise { await this.globalState.update("config", config); } private async updateInternalConfig( update: (oldConfig: InternalConfig) => InternalConfig ): Promise { const config = this.getInternalConfig(); const updated = update(config); await this.setInternalConfig(updated); } } interface InternalConfig { totalUsageTimeInSeconds: number; thisVersionUsageTimeInSeconds: number; currentVersion: string; versionLastAskedForFeedback: string | undefined; dateTimeLastAskedForSponsorship: string | undefined; totalUsageTimeLastAskedForSponsorshipInSeconds: number | undefined; } export class DiagramConfig { //#region Styles private readonly _styles = new VsCodeSetting(`${extensionId}.styles`, { scope: this.uri, serializer: serializerWithDefault([]), }); @computed public get styles(): Style[] { return this._styles.get(); } //#endregion //#region Custom Color Schemes private readonly _customColorSchemes = new VsCodeSetting( `${extensionId}.customColorSchemes`, { scope: this.uri, serializer: serializerWithDefault([]), } ); @computed public get customColorSchemes(): ColorScheme[][] { return this._customColorSchemes.get(); } //#endregion //#region Default Vertex Style private readonly _defaultVertexStyle = new VsCodeSetting( `${extensionId}.defaultVertexStyle`, { scope: this.uri, serializer: serializerWithDefault>({}), } ); @computed public get defaultVertexStyle(): Record { return this._defaultVertexStyle.get(); } //#endregion //#region Default Edge Style private readonly _defaultEdgeStyle = new VsCodeSetting( `${extensionId}.defaultEdgeStyle`, { scope: this.uri, serializer: serializerWithDefault>({}), } ); @computed public get defaultEdgeStyle(): Record { return this._defaultEdgeStyle.get(); } //#endregion //#region Color Names private readonly _colorNames = new VsCodeSetting( `${extensionId}.colorNames`, { scope: this.uri, serializer: serializerWithDefault>({}), } ); @computed public get colorNames(): Record { return this._colorNames.get(); } //#endregion //#region Simple Labels private readonly _simpleLabels = new VsCodeSetting( `${extensionId}.simpleLabels`, { scope: this.uri, serializer: serializerWithDefault(false), } ); @computed public get simpleLabels(): boolean { return this._simpleLabels.get(); } //#endregion //#region Preset Colors private readonly _presetColors = new VsCodeSetting( `${extensionId}.presetColors`, { scope: this.uri, serializer: serializerWithDefault([]), } ); @computed public get presetColors(): string[] { return this._presetColors.get(); } //#endregion // #region Theme private readonly _theme = new VsCodeSetting(`${extensionId}.theme`, { scope: this.uri, serializer: serializerWithDefault("kennedy"), }); private readonly _appearance = new VsCodeSetting(`${extensionId}.appearance`, { scope: this.uri, serializer: serializerWithDefault< "automatic" | "light" | "dark" | "highContrastLight" | "highContrast" >("light"), }); public get resolvedTheme(): ResolvedDrawioTheme { const themeName = this._theme.get().toLowerCase(); // handle 'dark' and 'automatic' for backwards compat if (themeName === 'dark') { return new ResolvedDrawioTheme('kennedy', ColorThemeKind.Dark); } if (themeName === 'automatic') { return new ResolvedDrawioTheme('kennedy', this.config.vscodeTheme.kind); } const appearance = this._appearance.get().toLowerCase(); let resolvedAppearance = themeKindFromString(appearance); if (!resolvedAppearance) { resolvedAppearance = this.config.vscodeTheme.kind; } return new ResolvedDrawioTheme(themeName, resolvedAppearance); } public getVsCodeAppearance(): string { return themeKindToString(this.config.vscodeTheme.kind); } @computed public get theme(): string { const t = this._theme.get().toLowerCase(); if (t === 'dark' || t === 'automatic') { return 'kennedy'; } return t; } @computed public get appearance(): string { return this._appearance.get().toLowerCase(); } public async setTheme(themeName: string): Promise { await this._theme.set(themeName); } public async setAppearance(appearance: string): Promise { await this._appearance.set(appearance as any); } // #endregion // #region Mode private readonly _useOfflineMode = new VsCodeSetting( `${extensionId}.offline`, { scope: this.uri, serializer: serializerWithDefault(true), } ); private readonly _onlineUrl = new VsCodeSetting( `${extensionId}.online-url`, { scope: this.uri, serializer: serializerWithDefault("https://embed.diagrams.net/"), } ); @computed public get mode(): { kind: "offline" } | { kind: "online"; url: string } { if (this._useOfflineMode.get()) { return { kind: "offline" }; } else { return { kind: "online", url: this._onlineUrl.get() }; } } // #endregion // #region Code Link Activated private readonly _codeLinkActivated = new VsCodeSetting( `${extensionId}.codeLinkActivated`, { scope: this.uri, serializer: serializerWithDefault(false), } ); public get codeLinkActivated(): boolean { return this._codeLinkActivated.get(); } public setCodeLinkActivated(value: boolean): Promise { return this._codeLinkActivated.set(value); } // #endregion // #region resizeImages private readonly _resizeImages = new VsCodeSetting( `${extensionId}.resizeImages`, { scope: this.uri, serializer: serializerWithDefault(undefined), } ); // This is a hack to prevent a reload when we update the setting from local storage public isResizeImageUpdating = false; public get resizeImages(): boolean | undefined { const result = this._resizeImages.get(); if (result === null) { return undefined; } return result; } public setResizeImages(value: boolean | undefined): Promise { return this._resizeImages.set(value); } // #endregion // #region Local Storage public get localStorage(): Record { const localStorage = this.memento.get>( `${extensionId}.local-storage`, {} ); const resizeImages = this.resizeImages; try { const drawioConfig = JSON.parse(localStorage[".drawio-config"]); drawioConfig.resizeImages = resizeImages; localStorage[".drawio-config"] = JSON.stringify(drawioConfig); } catch (e) { console.error(e); } return localStorage; } public setLocalStorage(value: Record): void { try { const drawioConfig = JSON.parse(value[".drawio-config"]) as { resizeImages?: boolean; }; if (drawioConfig.resizeImages !== this.resizeImages) { this.isResizeImageUpdating = true; this.setResizeImages(drawioConfig.resizeImages); } } catch (e) { console.error(e); } this.memento.update(`${extensionId}.local-storage`, value); } //#endregion private readonly _plugins = new VsCodeSetting<{ file: string }[]>( `${extensionId}.plugins`, { scope: this.uri, serializer: serializerWithDefault([]), } ); public get plugins(): { file: Uri }[] { return this._plugins.get().map((entry) => { const fullFilePath = this.evaluateTemplate(entry.file, "plugins"); return { file: Uri.file(fullFilePath) }; }); } // #region Custom Libraries private readonly _customLibraries = new VsCodeSetting< DrawioCustomLibrary[] >(`${extensionId}.customLibraries`, { scope: this.uri, serializer: serializerWithDefault([]), }); @computed public get customLibraries(): Promise { const normalizeLib = async ( lib: DrawioCustomLibrary ): Promise => { function parseJson(json: string): unknown { return JSON.parse(json); } function parseXml(xml: string): unknown { const parse = require("xml-parser-xo"); const parsedXml = parse(xml); return JSON.parse(parsedXml.root.children[0].content); } let data: DrawioLibraryData["data"]; if ("json" in lib) { data = { kind: "value", value: parseJson(lib.json) }; } else if ("xml" in lib) { data = { kind: "value", value: parseXml(lib.xml), }; } else if ("file" in lib) { const file = this.evaluateTemplate( lib.file, "custom libraries" ); const buffer = await workspace.fs.readFile(Uri.file(file)); const content = BufferImpl.from(buffer).toString("utf-8"); if (file.endsWith(".json")) { data = { kind: "value", value: parseJson(content), }; } else { data = { kind: "value", value: parseXml(content), }; } } else { data = { kind: "url", url: lib.url }; } return { libName: lib.libName, entryId: lib.entryId, data, }; }; return Promise.all( this._customLibraries.get().map((lib) => normalizeLib(lib)) ); } private evaluateTemplate(template: string, context: string): string { const tpl = new SimpleTemplate(template); return tpl.render({ workspaceFolder: () => { const workspaceFolder = workspace.getWorkspaceFolder(this.uri); if (!workspaceFolder) { throw new Error( `Cannot get workspace folder of opened diagram - '${template}' cannot be evaluated to load ${context}!` ); } return workspaceFolder.uri.fsPath; }, }); } // #endregion // #region Custom Fonts private readonly _customFonts = new VsCodeSetting( `${extensionId}.customFonts`, { scope: this.uri, serializer: serializerWithDefault([]), } ); @computed public get customFonts(): string[] { return this._customFonts.get(); } // #endregion // #region Zoom Factor private readonly _zoomFactor = new VsCodeSetting( `${extensionId}.zoomFactor`, { scope: this.uri, serializer: serializerWithDefault(1.2), } ); @computed public get zoomFactor(): number { return this._zoomFactor.get(); } // #endregion // #region Global Variables private readonly _globalVars = new VsCodeSetting( `${extensionId}.globalVars`, { scope: this.uri, serializer: serializerWithDefault(null), } ); @computed public get globalVars(): object | null { return this._globalVars.get(); } // #endregion constructor( public readonly uri: Uri, private readonly config: Config, private readonly memento: Memento ) { } @computed public get drawioLanguage(): string { if (env.language.toLowerCase() === "zh-tw") { // See https://github.com/hediet/vscode-drawio/issues/231. // Seems to be an exception, all other language codes are just the language, not the country. return "zh-tw"; } const lang = env.language.split("-")[0].toLowerCase(); return lang; } } type DrawioCustomLibrary = ( | { xml: string; } | { url: string; } | { json: string; } | { file: string; } ) & { libName: string; entryId: string }; export class ResolvedDrawioTheme { public static getThemeNames(): string[] { return [ "min", "kennedy", ]; } constructor( public readonly themeName: string, public readonly appearance: ColorThemeKind, ) { } getAppearanceDrawioValue(): string { return { [ColorThemeKind.Light]: "0", [ColorThemeKind.Dark]: "1", [ColorThemeKind.HighContrastLight]: "2", [ColorThemeKind.HighContrast]: "3" }[this.appearance]; } getAppearanceStringValue(): string { return themeKindToString(this.appearance); } toString(): string { if (this.appearance === ColorThemeKind.Light) { return this.themeName; } return `${this.themeName} - ${this.getAppearanceStringValue()}`; } } function themeKindToString(themeKind: ColorThemeKind): string { return { [ColorThemeKind.Light]: "light", [ColorThemeKind.Dark]: "dark", [ColorThemeKind.HighContrastLight]: "high-contrast-light", [ColorThemeKind.HighContrast]: "high-contrast" }[themeKind]; } function themeKindFromString(themeKind: string): ColorThemeKind | undefined { return { "light": ColorThemeKind.Light, "dark": ColorThemeKind.Dark, "high-contrast-light": ColorThemeKind.HighContrastLight, "high-contrast": ColorThemeKind.HighContrast }[themeKind]; } ================================================ FILE: src/DrawioClient/CustomizedDrawioClient.ts ================================================ import { EventEmitter } from "@hediet/std/events"; import { DrawioClient } from "./DrawioClient"; /** * Enhances the drawio client with custom events and methods. * They require modifications of the official drawio source or plugins. */ export class CustomizedDrawioClient extends DrawioClient< CustomDrawioAction, CustomDrawioEvent > { private readonly onNodeSelectedEmitter = new EventEmitter<{ label: string; linkedData: unknown; }>(); public readonly onNodeSelected = this.onNodeSelectedEmitter.asEvent(); private readonly onCustomPluginLoadedEmitter = new EventEmitter<{ pluginId: string; }>(); public readonly onCustomPluginLoaded = this.onCustomPluginLoadedEmitter.asEvent(); private readonly onCursorChangeEmitter = new EventEmitter<{ newPosition: Point | undefined; }>(); public readonly onCursorChanged = this.onCursorChangeEmitter.asEvent(); private readonly onSelectedCellsChangedEmitter = new EventEmitter<{ selectedCellIds: string[]; }>(); public readonly onSelectedCellsChanged = this.onSelectedCellsChangedEmitter.asEvent(); private readonly onSelectedRectangleChangedEmitter = new EventEmitter<{ rectangle: { start: Point; end: Point } | undefined; }>(); public readonly onSelectedRectangleChanged = this.onSelectedRectangleChangedEmitter.asEvent(); private readonly onFocusChangedEmitter = new EventEmitter<{ hasFocus: boolean; }>(); public readonly onFocusChanged = this.onFocusChangedEmitter.asEvent(); private readonly onInvokeCommandEmitter = new EventEmitter<{ command: InvokeCommandEvent["command"]; }>(); public readonly onInvokeCommand = this.onInvokeCommandEmitter.asEvent(); public linkSelectedNodeWithData(linkedData: unknown) { this.sendCustomAction({ action: "linkSelectedNodeWithData", linkedData, }); } public async getVertices(): Promise<{ id: string; label: string }[]> { const response = await this.sendCustomActionExpectResponse({ action: "getVertices", }); if (response.event !== "getVertices") { throw new Error("Invalid Response"); } return response.vertices; } public setNodeSelectionEnabled(enabled: boolean): void { this.sendCustomAction({ action: "setNodeSelectionEnabled", enabled, }); } public updateVertices(verticesToUpdate: { id: string; label: string }[]) { this.sendCustomAction({ action: "updateVertices", verticesToUpdate, }); } public addVertices(vertices: { label: string }[]) { this.sendCustomAction({ action: "addVertices", vertices, }); } public updateLiveshareViewState(update: { cursors: ParticipantCursorInfo[]; selectedCells: ParticipantSelectedCellsInfo[]; selectedRectangles: ParticipantSelectedRectangleInfo[]; }) { this.sendCustomAction({ action: "updateLiveshareViewState", ...update, }); } protected async handleEvent(evt: CustomDrawioEvent): Promise { if (evt.event === "nodeSelected") { this.onNodeSelectedEmitter.emit({ label: evt.label, linkedData: evt.linkedData, }); } else if (evt.event === "pluginLoaded") { this.onCustomPluginLoadedEmitter.emit({ pluginId: evt.pluginId }); } else if (evt.event === "focusChanged") { this.onFocusChangedEmitter.emit({ hasFocus: evt.hasFocus }); } else if (evt.event === "cursorChanged") { this.onCursorChangeEmitter.emit({ newPosition: evt.position }); } else if (evt.event === "selectedCellsChanged") { this.onSelectedCellsChangedEmitter.emit({ selectedCellIds: evt.selectedCellIds, }); } else if (evt.event === "invokeCommand") { this.onInvokeCommandEmitter.emit({ command: evt.command }); } else if (evt.event === "selectedRectangleChanged") { this.onSelectedRectangleChangedEmitter.emit({ rectangle: evt.rect, }); } else { await super.handleEvent(evt); } } } interface Point { x: number; y: number; } ================================================ FILE: src/DrawioClient/DrawioClient.ts ================================================ import { EventEmitter } from "@hediet/std/events"; import { Disposable } from "@hediet/std/disposable"; import { DrawioConfig, DrawioEvent, DrawioAction } from "./DrawioTypes"; import { BufferImpl } from "../utils/buffer"; /** * Represents a connection to an drawio iframe. */ export class DrawioClient< TCustomAction extends {} = never, TCustomEvent extends {} = never > { public readonly dispose = Disposable.fn(); private readonly onInitEmitter = new EventEmitter(); public readonly onInit = this.onInitEmitter.asEvent(); protected readonly onChangeEmitter = new EventEmitter(); public readonly onChange = this.onChangeEmitter.asEvent(); private readonly onSaveEmitter = new EventEmitter(); public readonly onSave = this.onSaveEmitter.asEvent(); private readonly onUnknownMessageEmitter = new EventEmitter<{ message: TCustomEvent; }>(); public readonly onUnknownMessage = this.onUnknownMessageEmitter.asEvent(); // This is always up to date, except directly after calling load. private currentXml: string | undefined = undefined; private isMerging = false; constructor( private readonly messageStream: MessageStream, private readonly getConfig: () => Promise, public readonly reloadWebview: () => void ) { this.dispose.track( messageStream.registerMessageHandler((msg) => this.handleEvent(JSON.parse(msg as string) as DrawioEvent) ) ); } private currentActionId = 0; private responseHandlers = new Map< string, { resolve: (response: DrawioEvent) => void; reject: () => void } >(); protected sendCustomAction(action: TCustomAction): void { this.sendAction(action); } protected sendCustomActionExpectResponse( action: TCustomAction ): Promise { return this.sendActionWaitForResponse(action); } private sendAction(action: DrawioAction | TCustomAction) { this.messageStream.sendMessage(JSON.stringify(action)); } private sendActionWaitForResponse( action: DrawioAction ): Promise; private sendActionWaitForResponse( action: TCustomAction ): Promise; private sendActionWaitForResponse( action: DrawioAction | TCustomAction ): Promise { return new Promise((resolve, reject) => { const actionId = (this.currentActionId++).toString(); this.responseHandlers.set(actionId, { resolve: (response) => { this.responseHandlers.delete(actionId); resolve(response); }, reject, }); this.messageStream.sendMessage( JSON.stringify(Object.assign(action, { actionId })) ); }); } protected async handleEvent(evt: { event: string }): Promise { const drawioEvt = evt as DrawioEvent; if ("message" in drawioEvt) { const actionId = (drawioEvt.message as any).actionId as | string | undefined; if (actionId) { const responseHandler = this.responseHandlers.get(actionId); this.responseHandlers.delete(actionId); if (responseHandler) { responseHandler.resolve(drawioEvt); } } } else if (drawioEvt.event === "init") { this.onInitEmitter.emit(); } else if (drawioEvt.event === "autosave") { const oldXml = this.currentXml; if (oldXml !== drawioEvt.xml) { this.currentXml = drawioEvt.xml; // Don't emit a change event if we're merging some changes in. if (!this.isMerging) { this.onChangeEmitter.emit({ newXml: this.currentXml, oldXml, }); } } } else if (drawioEvt.event === "save") { const oldXml = this.currentXml; this.currentXml = drawioEvt.xml; if (oldXml != this.currentXml) { // a little bit hacky. // If "save" does trigger a change, // treat save as autosave and don't actually save the file. this.onChangeEmitter.emit({ newXml: this.currentXml, oldXml }); } else { // Otherwise, the change has already // been reported by autosave. this.onSaveEmitter.emit(); } } else if (drawioEvt.event === "export") { // sometimes, message is not included :( // this is a hack to find the request to resolve const vals = [...this.responseHandlers.values()]; this.responseHandlers.clear(); if (vals.length !== 1) { for (const val of vals) { val.reject(); } } else { vals[0].resolve(drawioEvt); } } else if (drawioEvt.event === "configure") { const config = await this.getConfig(); this.sendAction({ action: "configure", config, }); } else { this.onUnknownMessageEmitter.emit({ message: drawioEvt }); } } public async mergeXmlLike(xmlLike: string): Promise { const promise = this.sendActionWaitForResponse({ action: "merge", xml: xmlLike, }); this.isMerging = true; try { const evt = await promise; if (evt.event !== "merge") { throw new Error("Invalid response"); } if (evt.error) { throw new Error(evt.error); } } finally { this.isMerging = false; } } /** * This loads an xml or svg+xml Draw.io diagram. */ public async loadXmlLike(xmlLike: string): Promise { this.currentXml = undefined; this.sendAction({ action: "load", xml: xmlLike, autosave: 1, }); // We request the xml to detect if an autosave is a real change. await this.getXml(); } public async loadPngWithEmbeddedXml(png: Uint8Array): Promise { let str = BufferImpl.from(png).toString("base64"); this.loadXmlLike("data:image/png;base64," + str); } public async export(extension: string): Promise { if (extension.endsWith(".png")) { return await this.exportAsPngWithEmbeddedXml(); } else if ( extension.endsWith(".drawio") || extension.endsWith(".dio") ) { const xml = await this.getXml(); return BufferImpl.from(xml, "utf-8"); } else if (extension.endsWith(".svg")) { return await this.exportAsSvgWithEmbeddedXml(); } else { throw new Error( `Invalid file extension "${extension}"! Only ".png", ".svg" and ".drawio" are supported.` ); } } private async getXmlUncached(): Promise { const response = await this.sendActionWaitForResponse({ action: "export", format: "xml", }); if (response.event !== "export") { throw new Error("Unexpected response"); } return response.xml; } public async getXml(): Promise { if (!this.currentXml) { const xml = await this.getXmlUncached(); if (!this.currentXml) { // It might have been changed in the meantime. // Always trust autosave. this.currentXml = xml; } } return this.currentXml; } public async exportAsPngWithEmbeddedXml(): Promise { const response = await this.sendActionWaitForResponse({ action: "export", format: "xmlpng", }); if (response.event !== "export") { throw new Error("Unexpected response"); } const start = "data:image/png;base64,"; if (!response.data.startsWith(start)) { throw new Error("Invalid data"); } const base64Data = response.data.substr(start.length); return BufferImpl.from(base64Data, "base64"); } public async exportAsSvgWithEmbeddedXml(): Promise { const response = await this.sendActionWaitForResponse({ action: "export", format: "xmlsvg", }); if (response.event !== "export") { throw new Error("Unexpected response"); } const start = "data:image/svg+xml;base64,"; if (!response.data.startsWith(start)) { throw new Error("Invalid data"); } const base64Data = response.data.substr(start.length); return BufferImpl.from(base64Data, "base64"); } public triggerOnSave(): void { this.onSaveEmitter.emit(); } } export interface DrawioDocumentChange { oldXml: string | undefined; newXml: string; } export interface MessageStream { registerMessageHandler(handler: (message: unknown) => void): Disposable; sendMessage(message: unknown): void; } ================================================ FILE: src/DrawioClient/DrawioClientFactory.ts ================================================ import { Webview, OutputChannel, Uri, window, WebviewPanel, workspace, } from "vscode"; import { CustomizedDrawioClient, simpleDrawioLibrary } from "."; import { Config, DiagramConfig } from "../Config"; import html from "./webview-content.html"; import { formatValue } from "../utils/formatValue"; import { autorun, observable, runInAction, untracked } from "mobx"; import { sha256 } from "js-sha256"; import { getDrawioExtensions } from "../DrawioExtensionApi"; import { BufferImpl } from "../utils/buffer"; export class DrawioClientFactory { constructor( private readonly config: Config, private readonly log: OutputChannel, private readonly extensionUri: Uri ) { } public async createDrawioClientInWebview( uri: Uri, webviewPanel: WebviewPanel, options: DrawioClientOptions ): Promise { const config = this.config.getDiagramConfig(uri); const plugins = await this.getPlugins(config); const webview = webviewPanel.webview; webview.options = { enableScripts: true, }; const reloadId = observable({ id: 0 }); let i = 0; const disposeAutorun = autorun( () => { reloadId.id; // these getters triggers a reload on change config.customLibraries; config.customFonts; config.presetColors; config.customColorSchemes; config.styles; config.defaultVertexStyle; config.defaultEdgeStyle; config.colorNames; config.simpleLabels; config.zoomFactor; config.globalVars; config.resizeImages; const html = this.getHtml(config, options, webview, plugins) + " ".repeat(i++); if (config.isResizeImageUpdating) { config.isResizeImageUpdating = false; } else { webview.html = html; } }, { name: "Update Webview Html" } ); const drawioClient = new CustomizedDrawioClient( { sendMessage: (msg) => { this.log.appendLine("vscode -> drawio: " + prettify(msg)); webview.postMessage(msg); }, registerMessageHandler: (handler) => { return webview.onDidReceiveMessage((msg) => { this.log.appendLine( "vscode <- drawio: " + prettify(msg) ); handler(msg); }); }, }, async () => { const libs = await config.customLibraries; return { compressXml: false, customFonts: config.customFonts, presetColors: config.presetColors, customColorSchemes: config.customColorSchemes, styles: config.styles, defaultVertexStyle: config.defaultVertexStyle, defaultEdgeStyle: config.defaultEdgeStyle, colorNames: config.colorNames, simpleLabels: config.simpleLabels, defaultLibraries: "general", libraries: simpleDrawioLibrary(libs), zoomFactor: config.zoomFactor, globalVars: config.globalVars, }; }, () => { runInAction("Force reload", () => { reloadId.id++; }); } ); drawioClient.onUnknownMessage.sub(({ message }) => { if (message.event === "updateLocalStorage") { const newLocalStorage = message.newLocalStorage; config.setLocalStorage(newLocalStorage); } }); webviewPanel.onDidDispose(() => { disposeAutorun(); drawioClient.dispose(); }); return drawioClient; } private async getPlugins( config: DiagramConfig ): Promise<{ jsCode: string }[]> { const pluginsToLoad = new Array<{ jsCode: string }>(); const promises = new Array>(); for (const ext of getDrawioExtensions()) { promises.push( (async () => { pluginsToLoad.push( ...(await ext.getDrawioPlugins({ uri: config.uri })) ); })() ); } for (const p of config.plugins) { let jsCode: string; try { jsCode = BufferImpl.from( await workspace.fs.readFile(p.file) ).toString("utf-8"); } catch (e) { window.showErrorMessage( `Could not read plugin file "${p.file}"!` ); continue; } const fingerprint = sha256.hex(jsCode); const pluginId = p.file.toString(); const isAllowed = this.config.isPluginAllowed( pluginId, fingerprint ); if (isAllowed) { pluginsToLoad.push({ jsCode }); } else if (isAllowed === undefined) { promises.push( (async () => { const result = await window.showWarningMessage( `Found unknown plugin "${pluginId}" with fingerprint "${fingerprint}"`, {}, { title: "Allow", action: async () => { pluginsToLoad.push({ jsCode }); await this.config.addKnownPlugin( pluginId, fingerprint, true ); }, }, { title: "Disallow", action: async () => { await this.config.addKnownPlugin( pluginId, fingerprint, false ); }, } ); if (result) { await result.action(); } })() ); } } await Promise.all(promises); return pluginsToLoad; } private getHtml( config: DiagramConfig, options: DrawioClientOptions, webview: Webview, plugins: { jsCode: string }[] ): string { if (config.mode.kind === "offline") { return this.getOfflineHtml(config, options, webview, plugins); } else { return this.getOnlineHtml(config, config.mode.url); } } private getOfflineHtml( config: DiagramConfig, options: DrawioClientOptions, webview: Webview, plugins: { jsCode: string }[] ): string { const vsuri = webview.asWebviewUri( Uri.joinPath(this.extensionUri, "drawio/src/main/webapp") ); const customPluginsPath = webview.asWebviewUri( // See webpack configuration. Uri.joinPath( this.extensionUri, "dist/custom-drawio-plugins/index.js" ) ); const localStorage = untracked(() => config.localStorage); // TODO use template engine // Prevent injection attacks by using JSON.stringify. const patchedHtml = html .replace(/\$\$literal-vsuri\$\$/g, vsuri.toString()) .replace("$$theme$$", JSON.stringify(config.resolvedTheme.themeName)) .replace("$$appearance$$", JSON.stringify(config.resolvedTheme.getAppearanceDrawioValue())) .replace("$$lang$$", JSON.stringify(config.drawioLanguage)) .replace("$$simpleLabels$$", JSON.stringify(config.simpleLabels)) .replace( "$$chrome$$", JSON.stringify(options.isReadOnly ? "0" : "1") ) .replace( "$$customPluginPaths$$", JSON.stringify([customPluginsPath.toString()]) ) .replace("$$localStorage$$", JSON.stringify(localStorage)) .replace( "$$additionalCode$$", JSON.stringify(plugins.map((p) => p.jsCode)) ); return patchedHtml; } private getOnlineHtml(config: DiagramConfig, drawioUrl: string): string { return ` `; } } export interface DrawioClientOptions { isReadOnly: boolean; } function prettify(msg: unknown): string { try { if (typeof msg === "string") { const obj = JSON.parse(msg as string); return formatValue(obj, process.env.DEV === "1" ? 500 : 80); } return formatValue(msg, process.env.DEV === "1" ? 500 : 80); } catch { } return "" + msg; } ================================================ FILE: src/DrawioClient/DrawioTypes.ts ================================================ export type DrawioEvent = | { event: "merge"; error: string; message: DrawioEvent; } | { event: "init"; } | { event: "autosave"; xml: string; } | { event: "save"; xml: string; } | { event: "export"; data: string; format: DrawioFormat; xml: string; message?: DrawioEvent; } | { event: "configure"; }; export type DrawioAction = | { action: "load"; xml: string; autosave?: 1; } | { action: "merge"; xml: string } | { action: "prompt"; } | { action: "template"; } | { action: "draft"; } | { action: "export"; format: DrawioFormat; } | { action: "configure"; config: DrawioConfig; }; // See https://desk.draw.io/support/solutions/articles/16000058316-how-to-configure-draw-io- export interface DrawioConfig { /** * An array of font family names in the format panel font drop-down list. */ defaultFonts?: string[]; /** * An array of font family names to be added before defaultFonts (9.2.4 and later). * Note: Fonts must be installed on the server and all client devices, or be added using the fontCss option. (6.5.4 and later). */ customFonts?: string[]; /** * Colour codes for the upper palette in the colour dialog (no leading # for the colour codes). */ presetColors?: string[]; /** * Colour codes to be added before presetColors (no leading # for the colour codes) (9.2.5 and later). */ customPresetColors?: string[]; /** * Available colour schemes in the style section at the top of the format panel (use leading # for the colour codes). * Possible colour keys are fill, stroke, gradient and font (font is ignored for connectors). */ defaultColorSchemes?: string[]; /** * Colour schemes to be added before defaultColorSchemes (9.2.4 and later). */ customColorSchemes?: ColorScheme[][]; /** * Config for the style tab in the format panel */ styles?: Style[]; /** * Defines the initial default styles for vertices and edges (connectors). * Note that the styles defined here are copied to the styles of new cells, for each cell. * This means that these values override everything else that is inherited from other styles or themes * (which may be supported at a later time). * Therefore, it is recommended to use a minimal set of values for the default styles. * To find the key/value pairs to be used, set the style in the application and find the key and value via Edit Style (Ctrl+E) (6.5.2 and later). * For example, to assign a default fontFamily of Courier New to all edges and vertices (and override all other default styles), * use * ```json * { * "defaultVertexStyle": {"fontFamily": "Courier New"}, * "defaultEdgeStyle": {"fontFamily": "Courier New"} * } * ``` * (6.5.2 and later). */ defaultVertexStyle?: Record; /** * See `defaultVertexStyle`. */ defaultEdgeStyle?: Record; /** * Names for colors, eg. {‘FFFFFF’: ‘White’, ‘000000’: ‘Black’} that are used as tooltips (uppercase, no leading # for the colour codes). */ colorNames?: Record; /** * A boolean flag to allow usage of ForeignObjects in svg output (simpleLabels=false, default) or not. */ simpleLabels?: boolean; /** * Defines a string with CSS rules to be used to configure the diagrams.net user interface. * For example, to change the background colour of the menu bar, use the following: * ```css * .geMenubarContainer { background-color: #c0c0c0 !important; } * .geMenubar { background-color: #c0c0c0 !important; } * ``` * (6.5.2 and later). */ css?: string; /** * Defines a string with CSS rules for web fonts to be used in diagrams. */ fontCss?: string; /** * Defines a semicolon-separated list of library keys (unique names) * in a string to be initially displayed in the left panel (e.g. "general;uml;company-graphics"). * Possible keys include custom entry IDs from the libraries field, * or keys for the libs URL parameter (6.5.2 and later). * The default value is `"general;uml;er;bpmn;flowchart;basic;arrows2"`. */ defaultLibraries?: string; /** * Defines an array of objects that list additional libraries and sections * in the left panel and the More Shapes dialog. */ libraries?: DrawioLibrarySection[]; /** * Defines the XML for blank diagrams and libraries (6.5.4 and later). */ emptyDiagramXml?: string; /** * Specifies if the XML output should be compressed. The default is true. */ compressXml?: boolean; } export interface ColorScheme { title?: string; fill?: string; stroke?: string; gradient?: string; font?: string; } export interface CommonStyle { fontColor?: string; strokeColor?: string; fillColor?: string; } export interface Graph { background?: string; gridColor?: string; } export interface Style { commonStyle?: CommonStyle; graph?: Graph; } export interface DrawioLibrarySection { title: DrawioResource; entries: { id: string; preview?: string; title: DrawioResource; desc?: DrawioResource; libs: ({ title: DrawioResource; tags?: string; } & ({ data: unknown } | { url: string }))[]; }[]; } export interface DrawioLibraryData { entryId: string; libName: string; data: { kind: "value"; value: unknown } | { kind: "url"; url: string }; } export function res(name: string): DrawioResource { return { main: name, }; } export interface DrawioResource { main: string; } export type DrawioFormat = "html" | "xmlpng" | "png" | "xml" | "xmlsvg"; ================================================ FILE: src/DrawioClient/html.d.ts ================================================ declare module "*.html" { const content: string; export default content; } ================================================ FILE: src/DrawioClient/index.ts ================================================ export * from "./DrawioClient"; export * from "./CustomizedDrawioClient"; export * from "./DrawioTypes"; export * from "./simpleDrawioLibrary"; export * from "./DrawioClientFactory"; ================================================ FILE: src/DrawioClient/simpleDrawioLibrary.ts ================================================ import { groupBy } from "../utils/groupBy"; import { DrawioLibraryData, DrawioLibrarySection, res } from "./DrawioTypes"; export function simpleDrawioLibrary( libs: DrawioLibraryData[] ): DrawioLibrarySection[] { function mapLib(lib: DrawioLibraryData) { return lib.data.kind === "value" ? { title: res(lib.libName), data: lib.data.value, } : { title: res(lib.libName), url: lib.data.url, }; } const groupedLibs = groupBy(libs, (l) => l.entryId); return [ { title: res("Custom Libraries"), entries: [...groupedLibs.values()].map((group) => ({ title: res(group.key), id: group.key, libs: group.items.map(mapLib), })), }, ]; } ================================================ FILE: src/DrawioClient/webview-content.html ================================================

Flowchart Maker and Online Diagram Software

diagrams.net (formerly draw.io) is free online diagram software. You can use it as a flowchart maker, network diagram software, to create UML online, as an ER diagram tool, to design database schema, to build BPMN online, as a circuit diagram maker, and more. draw.io can import .vsdx, Gliffy™ and Lucidchart™ files .

Loading...

================================================ FILE: src/DrawioEditorProviderBinary.ts ================================================ import { CustomEditorProvider, EventEmitter, CustomDocument, CancellationToken, Uri, CustomDocumentBackupContext, CustomDocumentBackup, CustomDocumentOpenContext, WebviewPanel, CustomDocumentContentChangeEvent, workspace, commands, window, } from "vscode"; import { CustomizedDrawioClient } from "./DrawioClient"; import { extname } from "path"; import { DrawioEditorService } from "./DrawioEditorService"; import { BufferImpl } from "./utils/buffer"; export class DrawioEditorProviderBinary implements CustomEditorProvider { private readonly onDidChangeCustomDocumentEmitter = new EventEmitter< CustomDocumentContentChangeEvent >(); public readonly onDidChangeCustomDocument = this.onDidChangeCustomDocumentEmitter.event; public constructor( private readonly drawioEditorService: DrawioEditorService ) {} public saveCustomDocument( document: DrawioBinaryDocument, cancellation: CancellationToken ): Promise { return document.save(); } public saveCustomDocumentAs( document: DrawioBinaryDocument, destination: Uri, cancellation: CancellationToken ): Promise { return document.saveAs(destination); } public revertCustomDocument( document: DrawioBinaryDocument, cancellation: CancellationToken ): Promise { return document.loadFromDisk(); } public async backupCustomDocument( document: DrawioBinaryDocument, context: CustomDocumentBackupContext, cancellation: CancellationToken ): Promise { return document.backup(context.destination); } public async openCustomDocument( uri: Uri, openContext: CustomDocumentOpenContext, token: CancellationToken ): Promise { const document = new DrawioBinaryDocument(uri, openContext.backupId); document.onChange(() => { this.onDidChangeCustomDocumentEmitter.fire({ document, }); }); document.onInstanceSave(() => { commands.executeCommand("workbench.action.files.save"); }); return document; } public async resolveCustomEditor( document: DrawioBinaryDocument, webviewPanel: WebviewPanel, token: CancellationToken ): Promise { try { const editor = await this.drawioEditorService.createDrawioEditorInWebview( webviewPanel, { kind: "drawio", document }, { isReadOnly: false } ); document.setDrawioClient(editor.drawioClient); } catch (e) { window.showErrorMessage(`Failed to open diagram: ${e}`); throw e; } } } export class DrawioBinaryDocument implements CustomDocument { private readonly onChangeEmitter = new EventEmitter(); public readonly onChange = this.onChangeEmitter.event; private readonly onInstanceSaveEmitter = new EventEmitter(); public readonly onInstanceSave = this.onInstanceSaveEmitter.event; private _drawioClient: CustomizedDrawioClient | undefined; private get drawioClient(): CustomizedDrawioClient { return this._drawioClient!; } private _isDirty = false; public get isDirty() { return this._isDirty; } private currentXml: string | undefined; public constructor( public readonly uri: Uri, public readonly backupId: string | undefined ) {} public setDrawioClient(drawioClient: CustomizedDrawioClient): void { if (this._drawioClient) { throw new Error("Client already set!"); } this._drawioClient = drawioClient; drawioClient.onInit.sub(async () => { if (this.currentXml) { this.drawioClient.loadXmlLike(this.currentXml); } else if (this.backupId) { const backupFile = Uri.parse(this.backupId); const content = await workspace.fs.readFile(backupFile); const xml = BufferImpl.from(content).toString("utf-8"); await this.drawioClient.loadXmlLike(xml); this._isDirty = true; // because of backup } else { this.loadFromDisk(); } }); drawioClient.onChange.sub((change) => { this.currentXml = change.newXml; this._isDirty = true; this.onChangeEmitter.fire(); }); drawioClient.onSave.sub((change) => { this.onInstanceSaveEmitter.fire(); }); } public async loadFromDisk(): Promise { this._isDirty = false; if (this.uri.fsPath.endsWith(".png")) { const buffer = await workspace.fs.readFile(this.uri); await this.drawioClient.loadPngWithEmbeddedXml(buffer); } else { throw new Error("Invalid file extension"); } } public save(): Promise { this._isDirty = false; return this.saveAs(this.uri); } public async saveAs(target: Uri): Promise { const buffer = await this.drawioClient.export(extname(target.path)); await workspace.fs.writeFile(target, buffer); } public async backup(destination: Uri): Promise { const xml = await this.drawioClient.getXml(); await workspace.fs.writeFile( destination, BufferImpl.from(xml, "utf-8") ); return { id: destination.toString(), delete: async () => { try { await workspace.fs.delete(destination); } catch { // no op } }, }; } public dispose(): void { // no op } } ================================================ FILE: src/DrawioEditorProviderText.ts ================================================ import { CancellationToken, CustomTextEditorProvider, Range, TextDocument, WebviewPanel, window, workspace, WorkspaceEdit, } from "vscode"; import formatter = require("xml-formatter"); import { DrawioEditorService } from "./DrawioEditorService"; export class DrawioEditorProviderText implements CustomTextEditorProvider { constructor(private readonly drawioEditorService: DrawioEditorService) {} public async resolveCustomTextEditor( document: TextDocument, webviewPanel: WebviewPanel, token: CancellationToken ): Promise { try { const readonlySchemes = new Set(["git", "conflictResolution"]); const isReadOnly = readonlySchemes.has(document.uri.scheme); const editor = await this.drawioEditorService.createDrawioEditorInWebview( webviewPanel, { kind: "text", document, }, { isReadOnly } ); const drawioClient = editor.drawioClient; interface NormalizedDocument { equals(other: this): boolean; } function getNormalizedDocument(src: string): NormalizedDocument { const result = { src, equals: (o: any) => o.src === src, }; return result; } let lastDocument = getNormalizedDocument(document.getText()); let isThisEditorSaving = false; workspace.onDidChangeTextDocument(async (evt) => { if (evt.document !== document) { return; } if (isThisEditorSaving) { // We don't want to process our own changes. return; } if (evt.contentChanges.length === 0) { // Sometimes VS Code reports a document change without a change. return; } const newText = evt.document.getText(); const newDocument = getNormalizedDocument(newText); if (newDocument.equals(lastDocument)) { return; } lastDocument = newDocument; await drawioClient.mergeXmlLike(newText); }); drawioClient.onChange.sub(async ({ oldXml, newXml }) => { // We format the xml so that it can be easily edited in a second text editor. async function getOutput(): Promise { if (document.uri.path.endsWith(".svg")) { const svg = await drawioClient.exportAsSvgWithEmbeddedXml(); newXml = svg.toString("utf-8"); // This adds a host to track which files are created by this extension and which by draw.io desktop. newXml = newXml.replace( /^ ` ` `/, () => `` ); } return formatter( // This normalizes the host newXml ); } } const output = await getOutput(); const newDocument = getNormalizedDocument(output); if (newDocument.equals(lastDocument)) { return; } lastDocument = newDocument; const workspaceEdit = new WorkspaceEdit(); // TODO diff the new document with the old document and only edit the changes. workspaceEdit.replace( document.uri, new Range(0, 0, document.lineCount, 0), output ); isThisEditorSaving = true; try { if (!(await workspace.applyEdit(workspaceEdit))) { window.showErrorMessage( "Could not apply Draw.io document changes to the underlying document. Try to save again!" ); } } finally { isThisEditorSaving = false; } }); drawioClient.onSave.sub(async () => { await document.save(); }); drawioClient.onInit.sub(async () => { drawioClient.loadXmlLike(document.getText()); }); } catch (e) { window.showErrorMessage(`Failed to open diagram: ${e}`); throw e; } } } ================================================ FILE: src/DrawioEditorService.ts ================================================ import { Disposable } from "@hediet/std/disposable"; import { EventEmitter } from "@hediet/std/events"; import { autorun, computed, observable, ObservableSet } from "mobx"; import { extname } from "path"; import { commands, QuickPickItem, QuickPickItemKind, StatusBarAlignment, TextDocument, Uri, WebviewPanel, window, workspace } from "vscode"; import { Config, DiagramConfig, ResolvedDrawioTheme } from "./Config"; import { CustomizedDrawioClient, DrawioClientFactory, DrawioClientOptions, } from "./DrawioClient"; import { DrawioBinaryDocument } from "./DrawioEditorProviderBinary"; import { registerFailableCommand } from "./utils/registerFailableCommand"; const drawioChangeThemeCommand = "hediet.vscode-drawio.changeTheme"; export class DrawioEditorService { public readonly dispose = Disposable.fn(); private readonly onEditorOpenedEmitter = new EventEmitter<{ editor: DrawioEditor; }>(); public readonly onEditorOpened = this.onEditorOpenedEmitter.asEvent(); public readonly openedEditors = new ObservableSet(); @computed get activeDrawioEditor(): DrawioEditor | undefined { return [...this.openedEditors].find((e) => e.isActive); } @observable private _lastActiveDrawioEditor: DrawioEditor | undefined; get lastActiveDrawioEditor(): DrawioEditor | undefined { return this._lastActiveDrawioEditor; } private readonly statusBar = this.dispose.track( window.createStatusBarItem(StatusBarAlignment.Right) ); constructor( private readonly config: Config, private readonly drawioClientFactory: DrawioClientFactory ) { autorun(() => { const a = this.activeDrawioEditor; if (a) { this._lastActiveDrawioEditor = a; } commands.executeCommand( "setContext", "hediet.vscode-drawio.active", !!a ); }); this.dispose.track( registerFailableCommand(drawioChangeThemeCommand, () => { const activeDrawioEditor = this.activeDrawioEditor; if (!activeDrawioEditor) { return; } activeDrawioEditor.handleChangeThemeCommand(); }) ); this.dispose.track( registerFailableCommand("hediet.vscode-drawio.convert", () => { const activeDrawioEditor = this.activeDrawioEditor; if (!activeDrawioEditor) { return; } activeDrawioEditor.handleConvertCommand(); }) ); this.dispose.track( registerFailableCommand( "hediet.vscode-drawio.reload-webview", () => { for (const e of this.openedEditors) { e.drawioClient.reloadWebview(); } } ) ); this.dispose.track( registerFailableCommand("hediet.vscode-drawio.export", () => { const activeDrawioEditor = this.activeDrawioEditor; if (!activeDrawioEditor) { return; } activeDrawioEditor.handleExportCommand(); }) ); this.dispose.track({ dispose: autorun( () => { const activeEditor = this.activeDrawioEditor; this.statusBar.command = drawioChangeThemeCommand; if (activeEditor) { this.statusBar.text = `Theme: ${activeEditor.config.resolvedTheme.toString()}`; this.statusBar.show(); } else { this.statusBar.hide(); } }, { name: "Update UI" } ), }); } public async createDrawioEditorInWebview( webviewPanel: WebviewPanel, document: | { kind: "text"; document: TextDocument } | { kind: "drawio"; document: DrawioBinaryDocument }, options: DrawioClientOptions ): Promise { const instance = await this.drawioClientFactory.createDrawioClientInWebview( document.document.uri, webviewPanel, options ); const config = this.config.getDiagramConfig(document.document.uri); const editor = new DrawioEditor( PrivateSymbol, webviewPanel, instance, document, config ); this.openedEditors.add(editor); this.onEditorOpenedEmitter.emit({ editor }); editor.webviewPanel.onDidDispose(() => { this.openedEditors.delete(editor); }); return editor; } } const PrivateSymbol = Symbol(); /** * Represents a drawio editor in VS Code. * Wraps a `CustomizedDrawioClient` and a webview. */ export class DrawioEditor { public readonly dispose = Disposable.fn(); @observable private _isActive = false; @observable private _hasFocus = false; private readonly knownDrawioFileExtensions: ReadonlyArray = [ ".drawio", ".dio", ".drawio.svg", ".drawio.png", ".dio.svg", ".dio.png", ]; public get fileExtension(): string { const currentFilePath = this.uri.path; // Just in case an extension is the prefix of another, // we want to return the longest. const sortedExtensionsByLengthDesc = this.knownDrawioFileExtensions .slice() .sort((a, b) => b.length - a.length); return ( sortedExtensionsByLengthDesc.find((ext) => currentFilePath.endsWith(ext) ) || extname(currentFilePath) ); } constructor( _constructorGuard: typeof PrivateSymbol, public readonly webviewPanel: WebviewPanel, public readonly drawioClient: CustomizedDrawioClient, public readonly document: | { kind: "text"; document: TextDocument } | { kind: "drawio"; document: DrawioBinaryDocument }, public readonly config: DiagramConfig ) { this._isActive = webviewPanel.active; this.dispose.track( webviewPanel.onDidChangeViewState(() => { this._isActive = webviewPanel.active; }) ); this.dispose.track( drawioClient.onFocusChanged.sub(({ hasFocus }) => { this._hasFocus = hasFocus; }) ); drawioClient.onInvokeCommand.sub(({ command }) => { if (command === "convert") { this.handleConvertCommand(); } else if (command === "export") { this.handleExportCommand(); } else if (command === "save") { this.drawioClient.triggerOnSave(); } }); } public get isActive(): boolean { return this._isActive; } public get hasFocus(): boolean { return this._hasFocus; } public get uri(): Uri { return this.document.document.uri; } /** * Supports `.drawio`, `.dio`, `.drawio.svg` `.drawio.png` and other extensions. * * @param newExtension Must start with a dot. */ public getUriWithExtension(newExtension: string): Uri { return this.uri.with({ path: removeEnd(this.uri.path, this.fileExtension) + newExtension, }); } public async convertTo(targetExtension: string): Promise { if (this.document.document.isDirty) { await window.showErrorMessage("Save your diagram first!"); return; } const targetUri = this.getUriWithExtension(targetExtension); if (await fileExists(targetUri)) { await window.showErrorMessage( `File "${targetUri.toString()}" already exists!` ); return; } const buffer = await this.drawioClient.export(targetExtension); const sourceUri = this.document.document.uri; const oldContent = await workspace.fs.readFile(sourceUri); await workspace.fs.writeFile(sourceUri, buffer); try { await workspace.fs.rename(sourceUri, targetUri); } catch (e) { await workspace.fs.writeFile(sourceUri, oldContent); throw e; } } public async exportTo(targetExtension: string): Promise { const buffer = await this.drawioClient.export(targetExtension); const targetUri = await window.showSaveDialog({ defaultUri: this.getUriWithExtension(targetExtension), }); if (!targetUri) { return; } await workspace.fs.writeFile(targetUri, buffer); } public async handleConvertCommand(): Promise { const result = await window.showQuickPick( [ { label: ".drawio.svg", description: "Converts the diagram to an editable SVG file", }, { label: ".drawio", description: "Converts the diagram to a drawio file", }, { label: ".drawio.png", description: "Converts the diagram to an editable png file", }, ].filter((x) => x.label !== this.fileExtension) ); if (!result) { return; } await this.convertTo(result.label); } public async handleExportCommand(): Promise { const result = await window.showQuickPick([ { label: ".svg", description: "Exports the diagram to a SVG file", }, { label: ".png", description: "Exports the diagram to a png file", }, { label: ".drawio", description: "Exports the diagram to a drawio file", }, ]); if (!result) { return; } await this.exportTo(result.label); } public async handleChangeThemeCommand(): Promise { const originalTheme = this.config.theme; const originalAppearance = this.config.appearance; const availableThemes = withFirstUnique(ResolvedDrawioTheme.getThemeNames(), originalTheme); const availableOptions: (QuickPickItem & { onSelect?: (preview: boolean) => void })[] = []; const curVsCodeAppearance = this.config.getVsCodeAppearance(); const appearances = withFirstUnique(["automatic", "light", "dark"], originalAppearance); for (const appearance of appearances) { const appearanceLabel = appearance === "automatic" ? `always match VS Code theme '${curVsCodeAppearance}'` : appearance; availableOptions.push({ kind: QuickPickItemKind.Separator, label: appearanceLabel, }); for (const theme of availableThemes) { availableOptions.push({ label: `${theme} - ${appearance}`, onSelect: () => { this.config.setTheme(theme); this.config.setAppearance(appearance); } }); } } const result = await window.showQuickPick( availableOptions, { onDidSelectItem: async (item) => { (item as any).onSelect(true); }, } ); if (!result || !result.onSelect) { await this.config.setTheme(originalTheme); await this.config.setAppearance(originalAppearance); return; } result.onSelect(false); } } function withFirstUnique(items: T[], firstItem: T): T[] { const filtered = items.filter(t => t !== firstItem); return [firstItem, ...filtered]; } async function fileExists(uri: Uri): Promise { try { await workspace.fs.stat(uri); return true; } catch (e) { return false; } } function removeEnd(value: string, end: string): string { if (!value.endsWith(end)) { throw new Error(`Value does not end with "${end}"!`); } return value.substr(0, value.length - end.length); } ================================================ FILE: src/DrawioExtensionApi.ts ================================================ import { Extension, extensions, Uri } from "vscode"; export function getDrawioExtensions(): DrawioExtension[] { return extensions.all .filter( (e) => (e.packageJSON as DrawioExtensionJsonManifest) .isDrawioExtension === true ) .map((e) => new DrawioExtension(e)); } export class DrawioExtension { constructor(private readonly api: Extension) {} public async getDrawioPlugins( context: DocumentContext ): Promise<{ jsCode: string }[]> { if (!this.api.isActive) { await this.api.activate(); } const { drawioExtensionV1 } = this.api.exports; if (drawioExtensionV1) { const { getDrawioPlugins } = drawioExtensionV1; if (getDrawioPlugins) { return await getDrawioPlugins.apply(drawioExtensionV1, [ context, ]); } } return []; } } export interface DrawioExtensionJsonManifest { // Set `"isDrawioExtension": true` in your package.json // so that your extension gets loaded when a draw.io file is opened. isDrawioExtension?: boolean; } // Implement this API in your public extension API. export interface DrawioExtensionApi { drawioExtensionV1?: { getDrawioPlugins?: ( context: DocumentContext ) => Promise<{ jsCode: string }[]>; }; } export interface DocumentContext { uri: Uri; } ================================================ FILE: src/Extension.ts ================================================ import * as vscode from "vscode"; import { Disposable } from "@hediet/std/disposable"; import { DrawioEditorProviderBinary } from "./DrawioEditorProviderBinary"; import { DrawioEditorProviderText } from "./DrawioEditorProviderText"; import { Config } from "./Config"; import { DrawioEditorService } from "./DrawioEditorService"; import { LinkCodeWithSelectedNodeService } from "./features/CodeLinkFeature"; import { EditDiagramAsTextFeature } from "./features/EditDiagramAsTextFeature"; import { LiveshareFeature } from "./features/LiveshareFeature"; import { DrawioClientFactory } from "./DrawioClient"; import { registerFailableCommand } from "./utils/registerFailableCommand"; export class Extension { public readonly dispose = Disposable.fn(); private readonly log = this.dispose.track( vscode.window.createOutputChannel("Drawio Integration Log") ); private readonly config = new Config(this.context.globalState); private readonly drawioClientFactory = new DrawioClientFactory( this.config, this.log, this.context.extensionUri ); private readonly editorService = new DrawioEditorService( this.config, this.drawioClientFactory ); private readonly linkCodeWithSelectedNodeService = this.dispose.track( new LinkCodeWithSelectedNodeService(this.editorService, this.config) ); private readonly editDiagramsAsTextFeature = this.dispose.track( new EditDiagramAsTextFeature(this.editorService, this.config) ); private readonly liveshareFeature = this.dispose.track( new LiveshareFeature(this.editorService, this.config) ); constructor(private readonly context: vscode.ExtensionContext) { this.dispose.track( vscode.window.registerCustomEditorProvider( "hediet.vscode-drawio-text", new DrawioEditorProviderText(this.editorService), { webviewOptions: { retainContextWhenHidden: true } } ) ); this.dispose.track( vscode.window.registerCustomEditorProvider( "hediet.vscode-drawio", new DrawioEditorProviderBinary(this.editorService), { supportsMultipleEditorsPerDocument: false, webviewOptions: { retainContextWhenHidden: true }, } ) ); this.dispose.track( registerFailableCommand( "hediet.vscode-drawio.newDiagram", async () => { const targetUri = await vscode.window.showSaveDialog({ saveLabel: "Create", filters: { Diagrams: ["drawio"], }, }); if (!targetUri) { return; } try { await vscode.workspace.fs.writeFile( targetUri, new Uint8Array() ); await vscode.commands.executeCommand( "vscode.openWith", targetUri, "hediet.vscode-drawio-text" ); } catch (e) { console.error("Cannot create or open file", e); await vscode.window.showErrorMessage( `Cannot create or open file "${targetUri.toString()}"!` ); } } ) ); } } ================================================ FILE: src/features/CodeLinkFeature.ts ================================================ import { Disposable } from "@hediet/std/disposable"; import { commands, window, Uri, Range, Position, ThemeColor, workspace, TextEditorRevealType, ViewColumn, TextEditorDecorationType, TextEditor, SymbolInformation, QuickPickItem, QuickPickOptions, DocumentSymbol, SymbolKind, } from "vscode"; import { wait } from "@hediet/std/timer"; import { DrawioEditorService, DrawioEditor } from "../DrawioEditorService"; import { autorun, action } from "mobx"; import { Config } from "../Config"; import { path } from "../utils/path"; import { registerFailableCommand } from "../utils/registerFailableCommand"; const toggleCodeLinkActivationCommandName = "hediet.vscode-drawio.toggleCodeLinkActivation"; const linkCodeWithSelectedNodeCommandName = "hediet.vscode-drawio.linkCodeWithSelectedNode"; const linkSymbolWithSelectedNodeCommandName = "hediet.vscode-drawio.linkSymbolWithSelectedNode"; const linkWsSymbolWithSelectedNodeCommandName = "hediet.vscode-drawio.linkWsSymbolWithSelectedNode"; const linkFileWithSelectedNodeCommandName = "hediet.vscode-drawio.linkFileWithSelectedNode"; const symbolNameMap: Record = { [SymbolKind.File]: "symbol-file", [SymbolKind.Module]: "symbol-module", [SymbolKind.Namespace]: "symbol-namespace", [SymbolKind.Package]: "symbol-package", [SymbolKind.Class]: "symbol-class", [SymbolKind.Method]: "symbol-method", [SymbolKind.Property]: "symbol-property", [SymbolKind.Field]: "symbol-field", [SymbolKind.Constructor]: "symbol-constructor", [SymbolKind.Enum]: "symbol-enum", [SymbolKind.Interface]: "symbol-interface", [SymbolKind.Function]: "symbol-function", [SymbolKind.Variable]: "symbol-variable", [SymbolKind.Constant]: "symbol-constant", [SymbolKind.String]: "symbol-string", [SymbolKind.Number]: "symbol-number", [SymbolKind.Boolean]: "symbol-boolean", [SymbolKind.Array]: "symbol-array", [SymbolKind.Object]: "symbol-object", [SymbolKind.Key]: "symbol-key", [SymbolKind.Null]: "symbol-null", [SymbolKind.EnumMember]: "symbol-enum-member", [SymbolKind.Struct]: "symbol-struct", [SymbolKind.Event]: "symbol-event", [SymbolKind.Operator]: "symbol-operator", [SymbolKind.TypeParameter]: "symbol-type-parameter", }; export class LinkCodeWithSelectedNodeService { public readonly dispose = Disposable.fn(); private readonly statusBar = window.createStatusBarItem(); private lastActiveTextEditor: TextEditor | undefined = window.activeTextEditor; constructor( private readonly editorManager: DrawioEditorService, private readonly config: Config ) { this.dispose.track([ editorManager.onEditorOpened.sub(({ editor }) => this.handleDrawioEditor(editor) ), { dispose: autorun( () => { const activeEditor = editorManager.activeDrawioEditor; this.statusBar.command = toggleCodeLinkActivationCommandName; if (activeEditor) { this.statusBar.text = `$(link) ${ activeEditor.config.codeLinkActivated ? "$(circle-filled)" : "$(circle-outline)" } Code Link`; this.statusBar.show(); } else { this.statusBar.hide(); } }, { name: "Update UI" } ), }, window.onDidChangeActiveTextEditor(() => { if (window.activeTextEditor) { this.lastActiveTextEditor = window.activeTextEditor; } }), registerFailableCommand( linkCodeWithSelectedNodeCommandName, this.linkCodeWithSelectedNode ), registerFailableCommand( toggleCodeLinkActivationCommandName, this.toggleCodeLinkEnabled ), registerFailableCommand( linkFileWithSelectedNodeCommandName, this.linkFileWithSelectedNode ), registerFailableCommand( linkSymbolWithSelectedNodeCommandName, this.linkSymbolWithSelectedNode ), registerFailableCommand( linkWsSymbolWithSelectedNodeCommandName, this.linkWsSymbolWithSelectedNode ), ]); } @action.bound private async toggleCodeLinkEnabled() { const activeEditor = this.editorManager.activeDrawioEditor; if (!activeEditor) { return; } await activeEditor.config.setCodeLinkActivated( !activeEditor.config.codeLinkActivated ); } @action.bound private linkCodeWithSelectedNode(): void { const lastActiveDrawioEditor = this.editorManager.lastActiveDrawioEditor; if (!lastActiveDrawioEditor) { window.showErrorMessage("No active drawio instance."); return; } const editor = this.lastActiveTextEditor; if (!editor) { window.showErrorMessage("No text editor active."); return; } if (!editor.selection) { window.showErrorMessage("Nothing selected."); return; } const pos = new DeserializedCodePosition( editor.document.uri, editor.selection ); lastActiveDrawioEditor.drawioClient.linkSelectedNodeWithData( pos.serialize(lastActiveDrawioEditor.uri) ); this.revealSelection(pos); } @action.bound private linkFileWithSelectedNode(file: Uri): void { const lastActiveDrawioEditor = this.editorManager.lastActiveDrawioEditor; if (!lastActiveDrawioEditor) { window.showErrorMessage("No active drawio instance."); return; } const pos = new CodePosition(file, undefined); lastActiveDrawioEditor.drawioClient.linkSelectedNodeWithData( pos.serialize(lastActiveDrawioEditor.uri) ); } @action.bound private async linkWsSymbolWithSelectedNode() { this.linkSymbolWithSelectedNode(true); } @action.bound private async linkSymbolWithSelectedNode( storeTopLevelSymbol: boolean = false ) { const lastActiveDrawioEditor = this.editorManager.lastActiveDrawioEditor; if (!lastActiveDrawioEditor) { window.showErrorMessage("No active drawio instance."); return; } const editor = window.activeTextEditor; if (editor == undefined) { window.showErrorMessage("No text editor active."); return; } const uri = editor.document.uri; const hasSelection = !editor.selection.start.isEqual( editor.selection.end ); const result = (await commands.executeCommand( "vscode.executeDocumentSymbolProvider", uri )) as DocumentSymbol[]; let items: QuickPickItem[] = []; function recurse(symb: DocumentSymbol[], path: string) { for (let x of symb) { // If there is a selection and we do not intersect it, omit the symbol let intersectSelection = true; if (hasSelection && editor) { intersectSelection = editor.selections.reduce( (prev: boolean, cur) => { return ( prev && cur.intersection(x.selectionRange) !== undefined ); }, intersectSelection ); } // Add the symbol and descend into children let curpath = path == "" ? x.name : `${path}.${x.name}`; if (intersectSelection) { items.push({ label: `$(${symbolNameMap[x.kind]}) ${x.name}`, description: x.detail, detail: curpath, }); } recurse(x.children, curpath); } } recurse(result, ""); window .showQuickPick(items, { matchOnDescription: true, matchOnDetail: true, placeHolder: `Choose symbol from ${path.basename(uri.fsPath)}`, }) .then(async (v) => { if (v == undefined) return; const pos: CodePosition = new CodePosition( storeTopLevelSymbol ? undefined : uri, v.detail ); lastActiveDrawioEditor.drawioClient.linkSelectedNodeWithData( pos.serialize(lastActiveDrawioEditor.uri) ); // Validate upon exist, as some languages do not export workspace symbols if ( storeTopLevelSymbol && v.detail && !(await resolveTopSymbol(v.detail)) ) { window.showWarningMessage( `Cannot resolve symbol ${v.detail}. This likely means workspace symbols are not supported by your language. Try "Link Symbol With Selected Node" instead.` ); } }); } private handleDrawioEditor(editor: DrawioEditor): void { const drawioInstance = editor.drawioClient; drawioInstance.onCustomPluginLoaded.sub(() => { drawioInstance.dispose.track({ dispose: autorun( () => { drawioInstance.setNodeSelectionEnabled( editor.config.codeLinkActivated ); }, { name: "Send codeLinkActivated to drawio instance" } ), }); }); drawioInstance.onNodeSelected.sub(async ({ linkedData, label }) => { if (!editor.config.codeLinkActivated) { return; } if (linkedData) { try { const pos = await CodePosition.deserialize( linkedData, editor.uri ); await this.revealSelection(pos); } catch (e) { window.showErrorMessage((e as Error).message); } } else { const match = label.match(/#([a-zA-Z0-9_<>,]+)/); if (match) { const symbolName = match[1]; const pos = await resolveWorkspaceSymbol(symbolName); if (pos) { await this.revealSelection(pos); } else { window.showErrorMessage( `No symbol found with name "${symbolName}". Maybe you need to load the symbols by opening at least one of its code files?` ); } } } }); } private lastDecorationType: TextEditorDecorationType | undefined; private async revealSelection( pos: DeserializedCodePosition ): Promise { if (pos.range) { const d = await workspace.openTextDocument(pos.uri); const e = await window.showTextDocument(d, { viewColumn: ViewColumn.One, preserveFocus: true, }); e.revealRange(pos.range, TextEditorRevealType.Default); const highlightDecorationType = window.createTextEditorDecorationType({ backgroundColor: new ThemeColor( "editor.stackFrameHighlightBackground" ), }); if (this.lastDecorationType) { e.setDecorations(this.lastDecorationType, []); } this.lastDecorationType = highlightDecorationType; e.setDecorations(highlightDecorationType, [pos.range]); wait(1000).then(() => { e.setDecorations(highlightDecorationType, []); }); } else { await commands.executeCommand("vscode.open", pos.uri, { viewColumn: ViewColumn.One, preserveFocus: true, }); } } } // CodePosition before serializing and passing to editor class CodePosition { public readonly range: Range | undefined; private readonly symbol: string | undefined; public static async deserialize( value: unknown, relativeTo: Uri ): Promise { const data = value as Data; function getPosition(pos: PositionData): Position { return new Position(pos.line, pos.col); } // If data.path is defined, then // 1. Either have explicit range (data.start) defined // 2. Or symbol path (data.symbol) defined // Otherwise, we must resolve using data.symbol only. if (data.path) { let uri: Uri = relativeTo.with({ path: Uri.file(path.join(relativeTo.path, data.path)).path, }); if ("start" in data) { let range = new Range( getPosition(data.start), getPosition(data.end) ); return new DeserializedCodePosition(uri, range); } else if ("symbol" in data) { let range = await resolveSymbol(uri, data.symbol); if (range == undefined) throw new Error( `Cannot find symbol by path: ${data.symbol}. Maybe you need to load the symbols by opening at least one of its code files?` ); return new DeserializedCodePosition(uri, range); } return new DeserializedCodePosition(uri, undefined); } else if ("symbol" in data) { let pos = await resolveTopSymbol(data.symbol); if (pos) return pos; throw new Error( `Cannot find symbol by path: ${data.symbol}. Maybe you need to load the symbols by opening at least one of its code files?` ); } // Exceptions will be very rare in this case console.error("Draw.io: Data is invalid or cannot find symbol", data); throw new Error(`Malformed symbol information. Check console log.`); } constructor( public readonly uri: Uri | undefined, private obj?: Range | string ) { if (obj instanceof Range) { this.range = obj as Range; } else if (typeof obj == "string") { this.symbol = obj as string; } } public serialize(relativeTo: Uri): Data { function toPosition(pos: Position): PositionData { return { col: pos.character, line: pos.line, }; } let rangeObj = {}; if (this.range) { rangeObj = { start: toPosition(this.range.start), end: toPosition(this.range.end), }; } else if (this.symbol) { rangeObj = { symbol: this.symbol, }; } if (this.uri) { return { path: path .relative(relativeTo.fsPath, this.uri.fsPath) .replace(/\\/g, "/"), ...rangeObj, }; } else { return { ...rangeObj, }; } } } // CodePosition after deserializing (from Data object) class DeserializedCodePosition { constructor(public readonly uri: Uri, public readonly range?: Range) {} public serialize(relativeTo: Uri): Data { return new CodePosition(this.uri, this.range).serialize(relativeTo); } } type Data = { path: string | undefined; } & ( | {} | { start: PositionData; end: PositionData; } | { symbol: string; } ); interface PositionData { line: number; col: number; } function getSorterBy(selector: (item: T) => number) { return (item1: T, item2: T) => { return selector(item2) - selector(item1); }; } async function resolveSymbol( uri: Uri, path: string ): Promise { const result = (await commands.executeCommand( "vscode.executeDocumentSymbolProvider", uri )) as DocumentSymbol[]; let treePath = path.split("."); let cur: DocumentSymbol[] | undefined = result; for (let i = 0; i < treePath.length; i++) { if (cur == undefined) break; cur = cur.filter((x) => x.name == treePath[i]); if (i < treePath.length - 1) cur = cur[0]?.children; } if (cur == undefined || cur.length == 0) return undefined; return cur[0].selectionRange; } async function resolveTopSymbol( path: string ): Promise { let res = path.split("."); if (res.length == 0) return undefined; // res.length > 0 let symb = await resolveWorkspaceSymbol(res[0]); if (!symb) return undefined; if (res.length == 2) { const range = await resolveSymbol(symb.uri, path); if (range) symb = new DeserializedCodePosition(symb.uri, range); } return symb; } async function resolveWorkspaceSymbol( symbolName: string ): Promise { const result = (await commands.executeCommand( "vscode.executeWorkspaceSymbolProvider", symbolName )) as SymbolInformation[]; for (let x of result) console.log(x.name); const filtered = result .filter((r) => r.name === symbolName) .sort( getSorterBy((matchedSymbol) => { let score = 0; const uriAsString = matchedSymbol.location.uri.toString(); const idx = window.visibleTextEditors.findIndex( (e) => e.document.uri.toString() === uriAsString ); if (idx !== -1) { score += (window.visibleTextEditors.length - idx) / window.visibleTextEditors.length; } if (matchedSymbol.containerName === "") { score += 10; } return score; }) ); const symbolInfo = filtered[0]; if (symbolInfo) { return new DeserializedCodePosition( symbolInfo.location.uri, symbolInfo.location.range ); } return undefined; } ================================================ FILE: src/features/EditDiagramAsTextFeature.ts ================================================ import { Disposable } from "@hediet/std/disposable"; import { Config } from "../Config"; import { workspace, commands, window, ViewColumn, TextDocument } from "vscode"; import { DrawioEditorService, DrawioEditor } from "../DrawioEditorService"; import { DrawioFileSystemController } from "../vscode-utils/VirtualFileSystemProvider"; import { registerFailableCommand } from "../utils/registerFailableCommand"; export class EditDiagramAsTextFeature { public readonly dispose = Disposable.fn(); private readonly drawioFsController = this.dispose.track( new DrawioFileSystemController() ); private readonly trackedDocuments = new Map(); constructor( private readonly editorManager: DrawioEditorService, config: Config ) { if (!config.experimentalFeaturesEnabled) { return; } this.dispose.track([ workspace.onDidChangeTextDocument((e) => { const drawioEditor = this.trackedDocuments.get(e.document); if (!drawioEditor) { return; } const doc = DiagramAsTextDocument.parse(e.document.getText()); drawioEditor.drawioClient.updateVertices(doc.vertexUpdates); }), workspace.onDidCloseTextDocument((e) => { this.trackedDocuments.delete(e); }), ]); let isUpdating = false; this.dispose.track( registerFailableCommand( "hediet.vscode-drawio.editDiagramAsText", async () => { const activeDrawioEditor = this.editorManager.activeDrawioEditor; if (!activeDrawioEditor) { return; } const { didFileExist, file } = this.drawioFsController.getOrCreateFileForUri( activeDrawioEditor.uri.with({ scheme: this.drawioFsController.scheme, path: activeDrawioEditor.uri.path + ".drawio-txt", }) ); const updateFile = async () => { const nodes = await activeDrawioEditor.drawioClient.getVertices(); isUpdating = true; try { const doc = new DiagramAsTextDocument(nodes, []); file.writeString(doc.toString()); } finally { isUpdating = false; } }; await updateFile(); if (!didFileExist) { file.onDidChangeFile(async () => { if (isUpdating) { return; } const doc = DiagramAsTextDocument.parse( file.readString() ); doc.removeDuplicates(); activeDrawioEditor.drawioClient.addVertices( doc.newVertices ); await updateFile(); }); } const doc = await workspace.openTextDocument(file.uri); this.trackedDocuments.set(doc, activeDrawioEditor); const editor = await window.showTextDocument(doc, { viewColumn: ViewColumn.Beside, }); } ) ); } } class DiagramAsTextDocument { public static parse(src: string): DiagramAsTextDocument { const lines = src.split("\n"); const vertexUpdates = new Array<{ id: string; label: string; }>(); const newVertices = new Array<{ label: string }>(); for (const line of lines) { const m = line.match(/(.*):(.*)/); if (!m) { newVertices.push({ label: line }); } else { vertexUpdates.push({ id: m[1], label: m[2] }); } } return new DiagramAsTextDocument(vertexUpdates, newVertices); } constructor( public vertexUpdates: { id: string; label: string; }[], public newVertices: { label: string }[] ) {} public toString(): string { return ( this.vertexUpdates.map((n) => `${n.id}:${n.label}`).join("\n") + this.newVertices.map((v) => v.label).join("\n") ); } public removeDuplicates(): void { this.newVertices = this.newVertices.filter( (v) => !this.vertexUpdates.some( (existing) => existing.label === v.label ) ); } } ================================================ FILE: src/features/LiveshareFeature/CurrentViewState.ts ================================================ import { Disposable, Disposer } from "@hediet/std/disposable"; import { computed } from "mobx"; import { Uri } from "vscode"; import { DrawioEditor, DrawioEditorService } from "../../DrawioEditorService"; import { CustomizedDrawioClient } from "../../DrawioClient"; import { fromResource, IResource } from "../../utils/fromResource"; import { Point, ViewState, NormalizedUri } from "./SessionModel"; export class CurrentViewState { @computed private get _state(): | { editor: DrawioEditor; cursorPos: IResource; selectedCellIds: IResource; selectedRectangle: IResource; } | undefined { const activeDrawioEditor = this.editorManager.activeDrawioEditor; if (!activeDrawioEditor) { return undefined; } return { editor: activeDrawioEditor, cursorPos: getCursorPositionResource( activeDrawioEditor.drawioClient ), selectedCellIds: getSelectedCellsResource( activeDrawioEditor.drawioClient ), selectedRectangle: getSelectedRectangleResource( activeDrawioEditor.drawioClient ), }; } @computed get state(): ViewState { const state = this._state; if (!state) { return undefined; } const activeUri = this.normalizeUri(state.editor.uri); return { activeUri, currentCursor: state.cursorPos.current(), selectedCellIds: state.selectedCellIds.current(), selectedRectangle: state.selectedRectangle.current(), }; } constructor( private readonly editorManager: DrawioEditorService, private readonly normalizeUri: (uri: Uri) => NormalizedUri ) {} } function getSelectedRectangleResource( drawioInstance: CustomizedDrawioClient ): IResource { return fromResource( (sink) => Disposable.fn((track) => { let lastRect: Rectangle | undefined; let timeout: any; track( drawioInstance.onFocusChanged.sub(({ hasFocus }) => { if (!hasFocus) { sink(undefined); } }) ); track( drawioInstance.onSelectedRectangleChanged.sub( ({ rectangle }) => { lastRect = rectangle; if (!timeout) { timeout = setTimeout(() => { timeout = undefined; sink(lastRect); }, 1000 / 30); } } ) ); }), () => undefined ); } function getCursorPositionResource( drawioInstance: CustomizedDrawioClient ): IResource { return fromResource( (sink) => Disposable.fn((track) => { let lastPosition: Point | undefined; let timeout: any; track( drawioInstance.onFocusChanged.sub(({ hasFocus }) => { if (!hasFocus) { sink(undefined); } }) ); track( drawioInstance.onCursorChanged.sub(({ newPosition }) => { lastPosition = newPosition; if (!timeout) { timeout = setTimeout(() => { timeout = undefined; sink(lastPosition); }, 1000 / 30); } }) ); }), () => undefined ); } function getSelectedCellsResource( drawioInstance: CustomizedDrawioClient ): IResource { return fromResource( (sink) => Disposable.fn((track) => { track( drawioInstance.onFocusChanged.sub(({ hasFocus }) => { if (!hasFocus) { sink([]); } }) ); track( drawioInstance.onSelectedCellsChanged.sub( ({ selectedCellIds }) => { sink(selectedCellIds); } ) ); }), () => [] ); } ================================================ FILE: src/features/LiveshareFeature/LiveshareFeature.ts ================================================ import { Disposable } from "@hediet/std/disposable"; import * as vsls from "vsls"; import { Config } from "../../Config"; import { DrawioEditorService } from "../../DrawioEditorService"; import { autorunTrackDisposables } from "../../utils/autorunTrackDisposables"; import { fromResource } from "../../utils/fromResource"; import { LiveshareSession } from "./LiveshareSession"; export class LiveshareFeature { public readonly dispose = Disposable.fn(); constructor( private readonly editorManager: DrawioEditorService, private readonly config: Config ) { this.init().catch(console.error); } private async init() { const liveshare = await vsls.getApi("hediet.vscode-drawio"); if (!liveshare) { console.warn("Could not get liveshare API"); return; } this.dispose.track( new LiveshareFeatureInitialized(liveshare, this.editorManager) ); } } class LiveshareFeatureInitialized { public readonly dispose = Disposable.fn(); private session = fromResource( (sink) => { this.api.onDidChangeSession(({ session }) => { sink(normalizeSession(session)); }); }, () => normalizeSession(this.api.session) ); constructor( private readonly api: vsls.LiveShare, editorManager: DrawioEditorService ) { this.dispose.track( autorunTrackDisposables(async (track) => { const session = this.session.current(); if (!session) { return; } track(new LiveshareSession(api, session, editorManager)); }) ); } } function normalizeSession(session: vsls.Session): vsls.Session | undefined { if (session.role === vsls.Role.None) { return undefined; } return { ...session }; } ================================================ FILE: src/features/LiveshareFeature/LiveshareSession.ts ================================================ import { Disposable } from "@hediet/std/disposable"; import { EventEmitter, EventSource } from "@hediet/std/events"; import { Uri } from "vscode"; import * as vsls from "vsls"; import { DrawioEditor, DrawioEditorService } from "../../DrawioEditorService"; import { autorunTrackDisposables } from "../../utils/autorunTrackDisposables"; import { CurrentViewState } from "./CurrentViewState"; import { SessionModelUpdate, SessionModel, NormalizedUri, } from "./SessionModel"; export class LiveshareSession { public readonly dispose = Disposable.fn(); private readonly sessionModel = new SessionModel(); constructor( private readonly api: vsls.LiveShare, private readonly session: vsls.Session, private readonly editorManager: DrawioEditorService ) { this.dispose.track( autorunTrackDisposables((track) => [...editorManager.openedEditors].map((e) => track([ autorunTrackDisposables(() => this.updateLiveshareOverlaysInDrawio(e) ), ]) ) ) ); this.init(); } private getPeerIdInformation(peerId: number): { color: string; name: string | undefined; } { const peer = this.api.peers.find((p) => p.peerNumber === peerId); const colors = [ "#2965CC", "#29A634", "#D99E0B", "#D13913", "#8F398F", "#00B3A4", "#DB2C6F", "#9BBF30", "#96622D", "#7157D9", ]; const colorIdx = peer ? this.api.peers.indexOf(peer) : colors.length - 2; const color = colors[colorIdx % colors.length]; const name = peer && peer.user ? peer.user.displayName : undefined; return { color, name }; } private updateLiveshareOverlaysInDrawio(editor: DrawioEditor) { const viewStates = [ ...this.sessionModel.viewStatesByPeerId.values(), ].filter( (v) => v.peerId !== this.session.peerNumber && v.viewState && v.viewState.activeUri === this.normalizeUri(editor.document.document.uri) ); const selectedCells: Array = viewStates.map((v) => ({ id: "" + v.peerId, selectedCellIds: v.viewState!.selectedCellIds, color: this.getPeerIdInformation(v.peerId).color, })); const cursors: Array = viewStates .filter((v) => v.viewState && v.viewState.currentCursor) .map((v) => ({ id: "" + v.peerId, label: this.getPeerIdInformation(v.peerId).name, color: this.getPeerIdInformation(v.peerId).color, position: v.viewState!.currentCursor!, })); const selectedRectangles: Array = viewStates .filter((v) => v.viewState && v.viewState.selectedRectangle) .map((v) => ({ id: "" + v.peerId, color: this.getPeerIdInformation(v.peerId).color, rectangle: v.viewState!.selectedRectangle!, })); editor.drawioClient.updateLiveshareViewState({ selectedCells, cursors, selectedRectangles, }); } private async init() { let client: { sendAction(action: ServerAction): void; onEvent: EventSource<{ event: ServerEvent }>; }; if (this.session.role === vsls.Role.Host) { const svc = await this.api.shareService("drawio"); this.dispose.track({ dispose: () => this.api.unshareService("drawio"), }); if (!svc) { console.error("Could not share liveshare service"); return; } const eventEmitter = new EventEmitter<{ event: ServerEvent; }>(); client = { sendAction: (action) => { const event: ServerEvent = { event: "applyUpdate", update: action.update, }; svc.notify("event", event); eventEmitter.emit({ event }); }, onEvent: eventEmitter.asEvent(), }; svc.onNotify("action", (arg) => { client.sendAction(arg as unknown as ServerAction); }); this.dispose.track( this.api.onDidChangePeers(({ removed }) => { for (const r of removed) { client.sendAction({ action: "applyUpdate", update: { kind: "removePeer", peerId: r.peerNumber, }, }); } }) ); } else { const svc = await this.api.getSharedService("drawio"); if (!svc) { console.error("Could not get liveshare service"); return; } const eventEmitter = new EventEmitter<{ event: ServerEvent; }>(); client = { sendAction: (action) => svc.notify("action", action), onEvent: eventEmitter.asEvent(), }; svc.onNotify("event", (arg) => { eventEmitter.emit({ event: arg as ServerEvent }); }); } client.onEvent.sub(({ event }) => { this.sessionModel.apply(event.update); }); const curViewState = new CurrentViewState( this.editorManager, this.normalizeUri ); this.dispose.track( autorunTrackDisposables(() => { client.sendAction({ action: "applyUpdate", update: { kind: "updateViewState", newViewState: curViewState.state, peerId: this.session.peerNumber, }, }); }) ); } private readonly normalizeUri = (uri: Uri): NormalizedUri => { if (this.session.role === vsls.Role.Host) { return this.api.convertLocalUriToShared(uri).toString() as any; } else { return uri.toString() as any; } }; } type ServerAction = { action: "applyUpdate"; update: SessionModelUpdate }; type ServerEvent = { event: "applyUpdate"; update: SessionModelUpdate }; ================================================ FILE: src/features/LiveshareFeature/SessionModel.ts ================================================ import { action, ObservableMap } from "mobx"; export interface Point { x: number; y: number; } // is a string export type NormalizedUri = { __brand: "normalizedUri" }; export type ViewState = | { activeUri: NormalizedUri; currentCursor: Point | undefined; selectedCellIds: string[]; selectedRectangle: Rectangle | undefined; } | undefined; export class SessionModel { public readonly viewStatesByPeerId = new ObservableMap< number, { viewState: ViewState; peerId: number } >(); @action public apply(update: SessionModelUpdate): void { if (update.kind === "updateViewState") { const val = this.viewStatesByPeerId.get(update.peerId); const newVal = { peerId: update.peerId, viewState: update.newViewState, }; if (JSON.stringify(val) !== JSON.stringify(newVal)) { this.viewStatesByPeerId.set(update.peerId, newVal); } } if (update.kind === "removePeer") { this.viewStatesByPeerId.delete(update.peerId); } } } export type SessionModelUpdate = | { kind: "updateViewState"; peerId: number; newViewState: ViewState; } | { kind: "removePeer"; peerId: number; }; ================================================ FILE: src/features/LiveshareFeature/assets/package.json ================================================ { "name": "vscode-drawio", "publisher": "hediet", "description": "This file bypasses vsliveshares check", "private": "true" } ================================================ FILE: src/features/LiveshareFeature/index.ts ================================================ export * from "./LiveshareFeature"; ================================================ FILE: src/index.ts ================================================ import * as vscode from "vscode"; import { MobxConsoleLogger } from "@knuddels/mobx-logger"; import * as mobx from "mobx"; import { Extension } from "./Extension"; if (process.env.DEV === "1") { new MobxConsoleLogger(mobx); } export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(new Extension(context)); } export function deactivate() {} ================================================ FILE: src/types.d.ts ================================================ declare module "*.json"; ================================================ FILE: src/utils/SimpleTemplate.ts ================================================ export class SimpleTemplate { constructor(private readonly str: string) {} render(data: Record string>): string { return this.str.replace(/\$\{([a-zA-Z0-9]+)\}/g, (substr, grp1) => { return data[grp1](); }); } } ================================================ FILE: src/utils/autorunTrackDisposables.ts ================================================ import { Disposable, TrackFunction } from "@hediet/std/disposable"; import { autorun } from "mobx"; export function autorunTrackDisposables( reaction: (track: TrackFunction) => void ): Disposable { let lastDisposable: Disposable | undefined; return { dispose: autorun(() => { if (lastDisposable) { lastDisposable.dispose(); } lastDisposable = Disposable.fn(reaction); }), }; } ================================================ FILE: src/utils/buffer.ts ================================================ import { Buffer as Buf } from "buffer"; export type BufferImpl = Buffer; export const BufferImpl = typeof Buffer === "undefined" ? Buf : Buffer; ================================================ FILE: src/utils/formatValue.ts ================================================ // TODO make generic and improve. Copied from my mobx logger. export function formatValue(value: unknown, availableLen: number): string { switch (typeof value) { case "number": return "" + value; case "string": if (value.length + 2 <= availableLen) { return `"${value}"`; } return `"${value.substr(0, availableLen - 7)}"+...`; case "boolean": return value ? "true" : "false"; case "undefined": return "undefined"; case "object": if (value === null) { return "null"; } if (Array.isArray(value)) { return formatArray(value, availableLen); } else { return formatObject(value, availableLen); } case "symbol": return value.toString(); case "function": return `[[Function${value.name ? " " + value.name : ""}]]`; default: return "" + value; } } function formatObject(value: object, availableLen: number): string { let result = "{ "; let first = true; for (const [key, val] of Object.entries(value)) { if (!first) { result += ", "; } if (result.length - 5 > availableLen) { result += "..."; break; } first = false; result += `${key}: ${formatValue(val, availableLen - result.length)}`; } result += " }"; return result; } function formatArray(value: any[], availableLen: number): string { let result = "[ "; let first = true; for (const val of value) { if (!first) { result += ", "; } if (result.length - 5 > availableLen) { result += "..."; break; } first = false; result += `${formatValue(val, availableLen - result.length)}`; } result += " ]"; return result; } ================================================ FILE: src/utils/fromResource.ts ================================================ import { DisposableLike, dispose } from "@hediet/std/disposable"; import { createAtom, _allowStateChanges } from "mobx"; function invariant(condition: boolean, message?: string) {} export function fromResource( subscriber: (sink: (newValue: T) => void) => DisposableLike ): IResource; export function fromResource( subscriber: (sink: (newValue?: T) => void) => DisposableLike, getValue: () => T ): IResource; export function fromResource( subscriber: (sink: (newValue?: T) => void) => DisposableLike, getValue: (() => T) | undefined = undefined ): IResource { let isActive = false; let isDisposed = false; let value = getValue ? getValue() : undefined; let disposable: DisposableLike; const initializer = () => { invariant(!isActive && !isDisposed); isActive = true; disposable = subscriber((...args) => { _allowStateChanges(true, () => { if (args.length > 0) { value = args[0]; } else if (getValue) { value = getValue(); } else { throw new Error( "Either an argument or getValue must be provided" ); } atom.reportChanged(); }); }); }; const suspender = () => { if (isActive) { isActive = false; dispose(disposable); } }; const atom = createAtom("ResourceBasedObservable", initializer, suspender); return { current: () => { invariant( !isDisposed, "subscribingObservable has already been disposed" ); const isBeingTracked = atom.reportObserved(); if (!isBeingTracked && !isActive) { if (getValue) { return getValue(); } else { console.warn( "Called `get` of a subscribingObservable outside a reaction. Current value will be returned but no new subscription has started" ); } } return value; }, dispose: () => { isDisposed = true; suspender(); }, isAlive: () => isActive, }; } export interface IResource { current(): T; dispose(): void; isAlive(): boolean; } ================================================ FILE: src/utils/groupBy.ts ================================================ interface Group { key: TKey; items: TItem[]; } export function groupBy( items: ReadonlyArray, selectKey: (item: T) => TKey ): Map> { const map = new Map>(); for (const item of items) { const key = selectKey(item); let group = map.get(key); if (!group) { group = { key, items: [] }; map.set(key, group); } group.items.push(item); } return map; } ================================================ FILE: src/utils/mapObject.ts ================================================ export function mapObject, TResult>( obj: TObj, map: (item: TObj[keyof TObj], key: string) => TResult ): Record { const result: Record = {} as any; for (const [key, value] of Object.entries(obj)) { result[key as keyof TObj] = map(value as any, key); } return result; } ================================================ FILE: src/utils/path.ts ================================================ import * as path2 from "path"; function getPath(): path2.PlatformPath { try { const rq = eval("req" + "uire"); const obj = rq("path"); if ("relative" in obj) { return obj; } } catch (e) {} return path2; } export const path = getPath(); ================================================ FILE: src/utils/registerFailableCommand.ts ================================================ import { commands, window, Disposable } from "vscode"; export function registerFailableCommand( commandName: string, commandFn: (...args: any[]) => any ): Disposable { return commands.registerCommand(commandName, async (...args: any[]) => { try { return await commandFn(...args); } catch (e : any) { window.showErrorMessage("The command failed: " + e.message); return false; } }); } ================================================ FILE: src/vscode-utils/VirtualFileSystemProvider.ts ================================================ import { FileSystemProvider, Event, Uri, FileStat, FileType, FileChangeEvent, EventEmitter, workspace, FileChangeType, } from "vscode"; import { Disposable } from "@hediet/std/disposable"; import { BufferImpl } from "../utils/buffer"; export class DrawioFileSystemController { public readonly dispose = Disposable.fn(); private readonly fs = new VirtualFileSystemProvider(); public readonly scheme = "drawio"; constructor() { this.dispose.track( workspace.registerFileSystemProvider(this.scheme, this.fs, { isCaseSensitive: true, isReadonly: false, }) ); } public getOrCreateFileForUri(uri: Uri): { file: File; didFileExist: boolean; } { return this.fs.getOrCreateFile(uri); } /*public getRandomFile(extensionWithDot: string): File { const id1 = new Date().getTime(); const id2 = id++; return this.fs.getOrCreateFile(`/${id1}_${id2}${extensionWithDot}`); }*/ } export class VirtualFileSystemProvider implements FileSystemProvider { private fileChangedEmitter = new EventEmitter(); public readonly onDidChangeFile = this.fileChangedEmitter.event; private readonly files = new Map(); public getOrCreateFile(uri: Uri): { file: File; didFileExist: boolean } { const key = uri.toString(); const f = this.files.get(key); if (f) { return { file: f, didFileExist: true }; } const newFile = new File(uri, Uint8Array.from([])); newFile.onDidChangeFile(() => this.fileChangedEmitter.fire([ { type: FileChangeType.Changed, uri: newFile.uri }, ]) ); this.files.set(key, newFile); return { file: newFile, didFileExist: false }; } readFile(uri: Uri): Uint8Array | Thenable { return this.getOrCreateFile(uri).file.data; } writeFile( uri: Uri, content: Uint8Array, options: { create: boolean; overwrite: boolean } ): void | Thenable { return this.getOrCreateFile(uri).file.write(content); } stat(uri: Uri): FileStat { const f = this.getOrCreateFile(uri).file; return { type: FileType.File, ctime: 0, mtime: 0, size: f.data.length, }; } watch( uri: Uri, options: { recursive: boolean; excludes: string[] } ): Disposable { return Disposable.empty; } readDirectory( uri: Uri ): | [string, import("vscode").FileType][] | Thenable<[string, import("vscode").FileType][]> { throw new Error("Method not implemented."); } createDirectory(uri: Uri): void | Thenable { throw new Error("Method not implemented."); } delete(uri: Uri, options: { recursive: boolean }): void | Thenable { throw new Error("Method not implemented."); } rename( oldUri: Uri, newUri: Uri, options: { overwrite: boolean } ): void | Thenable { throw new Error("Method not implemented."); } } export class File { private readonly fileChangedEmitter = new EventEmitter(); public readonly onDidChangeFile = this.fileChangedEmitter.event; constructor(public readonly uri: Uri, public data: Uint8Array) {} public write(data: Uint8Array): void { this.data = data; this.fileChangedEmitter.fire(undefined); } public writeString(str: string): void { this.write(Uint8Array.from(BufferImpl.from(str, "utf-8"))); } public readString(): string { return BufferImpl.from(this.data).toString("utf-8"); } } ================================================ FILE: src/vscode-utils/VsCodeSetting.ts ================================================ import { Uri, workspace, ConfigurationTarget, Disposable } from "vscode"; import { fromResource } from "../utils/fromResource"; import { computed, runInAction } from "mobx"; import { EventEmitter } from "@hediet/std/events"; export interface Serializer { deserialize: (val: any) => T; serializer: (val: T) => any; } export function serializerWithDefault(defaultValue: T): Serializer { return { deserialize: (val) => (val === undefined ? defaultValue : val), serializer: (val) => val, }; } export class VsCodeSetting { public get T(): T { throw new Error(); } public readonly serializer: Serializer; public readonly scope: Uri | undefined; private readonly settingResource: VsCodeSettingResource; private readonly target: ConfigurationTarget | undefined; public constructor( public readonly id: string, options: { serializer?: Serializer; scope?: Uri; target?: ConfigurationTarget; } = {} ) { this.scope = options.scope; this.serializer = options.serializer || { deserialize: (val) => val, serializer: (val) => val, }; this.target = options.target; this.settingResource = new VsCodeSettingResource( this.id, this.scope, this.target ); } public get(): T { const result = this.settingResource.value; return this.serializer.deserialize(result); } public async set(value: T): Promise { const value2 = this.serializer.serializer(value); const c = workspace.getConfiguration(undefined, this.scope); let target: ConfigurationTarget; if (this.target !== undefined) { target = this.target; } else { const result = c.inspect(this.id); if ( result && [ result.workspaceFolderLanguageValue, result.workspaceFolderValue, ].some((i) => i !== undefined) ) { target = ConfigurationTarget.WorkspaceFolder; } if ( result && [result.workspaceLanguageValue, result.workspaceValue].some( (i) => i !== undefined ) ) { target = ConfigurationTarget.Workspace; } else { target = ConfigurationTarget.Global; } } await c.update(this.id, value2, target); } } class VsCodeSettingResource { public static onConfigChange = new EventEmitter(); private readonly resource = fromResource( (update) => { return VsCodeSettingResource.onConfigChange.sub(() => { update(); }); }, () => this.readValue() ); constructor( private readonly id: string, private readonly scope: Uri | undefined, private readonly target: ConfigurationTarget | undefined ) {} private readValue(): any { const config = workspace.getConfiguration(undefined, this.scope); if (this.target === undefined) { return config.get(this.id); } else { const result = config.inspect(this.id); if (!result) { return undefined; } if (this.target === ConfigurationTarget.Global) { return result.globalValue; } else if (this.target === ConfigurationTarget.Workspace) { return result.workspaceValue; } else if (this.target === ConfigurationTarget.WorkspaceFolder) { return result.workspaceFolderValue; } } } /** * This improves change detection. */ private readonly stringifiedSettingValue = computed( () => JSON.stringify(this.resource.current()), { name: `VsCodeSettingResource[${this.id}].value`, context: this, } ); public get value() { const v = this.stringifiedSettingValue.get(); if (v === undefined) { return undefined; } return JSON.parse(v); } } workspace.onDidChangeConfiguration(() => { runInAction("Update Configuration", () => { VsCodeSettingResource.onConfigChange.emit(); }); }); ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es6", "outDir": "out", "lib": ["es6"], "sourceMap": true, "rootDir": "src", "strict": true, "experimentalDecorators": true }, "include": ["./src/**/*", "./drawio-custom-plugins/src/types.d.ts"] } ================================================ FILE: tslint.json ================================================ { "rules": { "no-string-throw": true, "no-unused-expression": true, "no-duplicate-variable": true, "curly": true, "class-name": true, "semicolon": [true, "always"], "triple-equals": true }, "defaultSeverity": "warning" } ================================================ FILE: webpack.config.ts ================================================ import * as webpack from "webpack"; import path = require("path"); import { CleanWebpackPlugin } from "clean-webpack-plugin"; import * as CopyPlugin from "copy-webpack-plugin"; const r = (file: string) => path.resolve(__dirname, file); module.exports = { entry: r("./src/index"), output: { path: r("./dist/extension"), filename: "index.js", libraryTarget: "commonjs2", devtoolModuleFilenameTemplate: "../../[resource-path]", }, devtool: "source-map", externals: { vscode: "commonjs vscode", }, resolve: { extensions: [".ts", ".js"], fallback: { path: require.resolve("path-browserify"), fs: false, }, }, module: { rules: [ { test: /\.html$/i, loader: "raw-loader", }, { test: /\.ts$/, exclude: /node_modules/, use: [ { loader: "ts-loader", }, ], }, ], }, node: { __dirname: false, }, plugins: [ new CleanWebpackPlugin(), new webpack.EnvironmentPlugin({ DEV: "0", }), // Without `as any`, I get "Excessive stack depth comparing types with TS 3.2" new webpack.IgnorePlugin({ resourceRegExp: /^canvas$/ }) as any, new CopyPlugin({ patterns: [ { from: "./src/features/LiveshareFeature/assets", to: "." }, ], }), ], } as webpack.Configuration;