├── .eslintrc ├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── eslint.config.js ├── examples ├── main.js └── main.js.retypewriter ├── package.json ├── packages ├── cli │ ├── bin │ │ └── retypewriter.mjs │ ├── build.config.ts │ ├── package.json │ └── src │ │ ├── cli.ts │ │ ├── git.ts │ │ ├── index.ts │ │ └── terminal.ts ├── core │ ├── build.config.ts │ ├── package.json │ ├── src │ │ ├── animation │ │ │ ├── slicing.ts │ │ │ ├── steps.ts │ │ │ ├── timing.ts │ │ │ └── typewriter.ts │ │ ├── index.ts │ │ ├── state │ │ │ ├── parse.ts │ │ │ ├── patch.ts │ │ │ └── snaps.ts │ │ └── types.ts │ └── test │ │ ├── __snapshots__ │ │ ├── index.test.ts.snap │ │ ├── parse.test.ts.snap │ │ ├── snaps.test.ts.snap │ │ └── steps.test.ts.snap │ │ ├── fixture.ts │ │ ├── index.test.ts │ │ ├── parse.test.ts │ │ ├── slicing.test.ts │ │ ├── snaps.test.ts │ │ └── steps.test.ts └── vscode │ ├── .vscodeignore │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── res │ └── icon.png │ ├── scripts │ └── generateSyntaxes.ts │ ├── src │ ├── decoration.ts │ ├── index.ts │ ├── lens.ts │ ├── manager.ts │ ├── manipulate.ts │ ├── play.ts │ ├── record.ts │ ├── syntaxes.ts │ └── utils.ts │ └── syntaxes │ ├── default.json │ ├── html.json │ ├── javascript.json │ ├── javascriptreact.json │ ├── typescript.json │ ├── typescriptreact.json │ └── vue.json ├── playground ├── index.html ├── package.json ├── src │ ├── App.vue │ ├── auto-imports.d.ts │ ├── components.d.ts │ ├── components │ │ ├── CodeMirror.vue │ │ ├── Player.vue │ │ ├── Playground.vue │ │ ├── Snap.vue │ │ └── Snapshots.vue │ ├── logics │ │ ├── codemirror.ts │ │ ├── dark.ts │ │ └── snaps.ts │ ├── main.postcss │ └── main.ts ├── tsconfig.json ├── unocss.config.ts └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json └── vitest.config.ts /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@antfu" 3 | } 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [antfu] 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@v2.2.1 20 | 21 | - name: Set node 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: 16.x 25 | cache: pnpm 26 | 27 | - name: Setup 28 | run: npm i -g @antfu/ni 29 | 30 | - name: Install 31 | run: nci 32 | 33 | - name: Lint 34 | run: nr lint 35 | 36 | typecheck: 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v2 40 | 41 | - name: Install pnpm 42 | uses: pnpm/action-setup@v2.2.1 43 | 44 | - name: Set node 45 | uses: actions/setup-node@v2 46 | with: 47 | node-version: 16.x 48 | cache: pnpm 49 | 50 | - name: Setup 51 | run: npm i -g @antfu/ni 52 | 53 | - name: Install 54 | run: nci 55 | 56 | - name: Typecheck 57 | run: nr typecheck 58 | 59 | test: 60 | runs-on: ${{ matrix.os }} 61 | 62 | strategy: 63 | matrix: 64 | node: [16.x] 65 | os: [ubuntu-latest] 66 | fail-fast: false 67 | 68 | steps: 69 | - uses: actions/checkout@v2 70 | 71 | - name: Install pnpm 72 | uses: pnpm/action-setup@v2.2.1 73 | 74 | - name: Set node version to ${{ matrix.node }} 75 | uses: actions/setup-node@v2 76 | with: 77 | node-version: ${{ matrix.node }} 78 | cache: pnpm 79 | 80 | - name: Setup 81 | run: npm i -g @antfu/ni 82 | 83 | - name: Install 84 | run: nci 85 | 86 | - name: Build 87 | run: nr build 88 | 89 | - name: Test 90 | run: nr test 91 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Install pnpm 15 | uses: pnpm/action-setup@v2.2.1 16 | 17 | - name: Set node version to v16 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: 16.x 21 | cache: pnpm 22 | 23 | - run: npx conventional-github-releaser -p angular 24 | env: 25 | CONVENTIONAL_GITHUB_RELEASER_TOKEN: ${{secrets.GITHUB_TOKEN}} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | .idea 4 | *.log 5 | *.tgz 6 | coverage 7 | dist 8 | lib-cov 9 | logs 10 | node_modules 11 | temp 12 | *.vsix 13 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "runtimeExecutable": "${execPath}", 9 | "args": [ 10 | "--extensionDevelopmentPath=${workspaceFolder}/packages/vscode", 11 | "${workspaceFolder}/examples" 12 | ], 13 | "outFiles": [ 14 | "${workspaceFolder}/packages/vscode/dist/**/*.js" 15 | ], 16 | "preLaunchTask": "npm: vscode" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "unocss.root": "./playground/unocss.config.ts", 3 | // Enable the ESlint flat config support 4 | "eslint.experimental.useFlatConfig": true, 5 | 6 | // Disable the default formatter, use eslint instead 7 | "prettier.enable": false, 8 | "editor.formatOnSave": false, 9 | 10 | // Auto fix 11 | "editor.codeActionsOnSave": { 12 | "source.fixAll.eslint": "explicit", 13 | "source.organizeImports": "never" 14 | }, 15 | 16 | // Silent the stylistic rules in you IDE, but still auto fix them 17 | "eslint.rules.customizations": [ 18 | { "rule": "style/*", "severity": "off" }, 19 | { "rule": "format/*", "severity": "off" }, 20 | { "rule": "*-indent", "severity": "off" }, 21 | { "rule": "*-spacing", "severity": "off" }, 22 | { "rule": "*-spaces", "severity": "off" }, 23 | { "rule": "*-order", "severity": "off" }, 24 | { "rule": "*-dangle", "severity": "off" }, 25 | { "rule": "*-newline", "severity": "off" }, 26 | { "rule": "*quotes", "severity": "off" }, 27 | { "rule": "*semi", "severity": "off" } 28 | ], 29 | 30 | // Enable eslint for all supported languages 31 | "eslint.validate": [ 32 | "javascript", 33 | "javascriptreact", 34 | "typescript", 35 | "typescriptreact", 36 | "vue", 37 | "html", 38 | "markdown", 39 | "json", 40 | "jsonc", 41 | "yaml", 42 | "toml" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "vscode", 9 | "isBackground": true, 10 | "presentation": { 11 | "reveal": "never" 12 | }, 13 | "problemMatcher": [ 14 | { 15 | "base": "$ts-webpack-watch", 16 | "background": { 17 | "activeOnStart": true, 18 | "beginsPattern": "Build start", 19 | "endsPattern": "Build success" 20 | } 21 | } 22 | ], 23 | "group": "build" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Anthony Fu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | retypewriter 4 |

5 | 6 |

7 | reTypewriter 8 |

9 | 10 |

11 | Replay the steps of your changes at ease. 12 |
A diff based typing simulator. 13 |
14 |
15 | NPM version 16 |

17 | 18 |

19 | Gif Demo | 20 | Install VS Code Extension 21 |

22 | 23 |
24 | 25 | 26 | 31 | 32 |
27 |
28 | Made possible by my Sponsor Program 💖
29 | 30 |
33 |
34 | 35 | ## Sponsors 36 | 37 |

38 | 39 | 40 | 41 |

42 | 43 | ## License 44 | 45 | [MIT](./LICENSE) License © 2022 [Anthony Fu](https://github.com/antfu) 46 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | const antfu = require('@antfu/eslint-config').default 2 | 3 | module.exports = antfu({ 4 | 5 | }) 6 | -------------------------------------------------------------------------------- /examples/main.js: -------------------------------------------------------------------------------- 1 | // this code was generated using https://github.com/antfu/retypewriter 2 | import { createApp } from 'vue' 3 | import { createRouter, createWebHistory } from 'vue-router' 4 | import routes from 'virtual:generated-pages' 5 | import App from './App.vue' 6 | 7 | const app = createApp(App) 8 | const router = createRouter({ 9 | history: createWebHistory(), 10 | routes, 11 | }) 12 | app.use(router) 13 | app.mount('#app') 14 | -------------------------------------------------------------------------------- /examples/main.js.retypewriter: -------------------------------------------------------------------------------- 1 | reTypewriter Snapshots v1 2 | 3 | pause: true 4 | delay: 100 5 | --01---------- 6 | 7 | --02---------- 8 | import App from './App.vue' 9 | 10 | --03---------- 11 | import { } from 'vue' 12 | import App from './App.vue' 13 | 14 | --04---------- 15 | import { createApp } from 'vue' 16 | import App from './App.vue' 17 | 18 | const app = createApp(App) 19 | app.mount('#app') 20 | 21 | -----options-- 22 | pause: true 23 | --05---------- 24 | import { createApp } from 'vue' 25 | import { createRouter } from 'vue-router' 26 | import App from './App.vue' 27 | 28 | const app = createApp(App) 29 | const router = createRouter() 30 | app.use(router) 31 | app.mount('#app') 32 | 33 | --06---------- 34 | import { createApp } from 'vue' 35 | import { createRouter, createWebHistory } from 'vue-router' 36 | import App from './App.vue' 37 | 38 | const app = createApp(App) 39 | const router = createRouter({ 40 | history: createWebHistory(), 41 | }) 42 | app.use(router) 43 | app.mount('#app') 44 | 45 | --07---------- 46 | import { createApp } from 'vue' 47 | import { createRouter, createWebHistory } from 'vue-router' 48 | import routes from 'virtual:generated-pages' 49 | import App from './App.vue' 50 | 51 | const app = createApp(App) 52 | const router = createRouter({ 53 | history: createWebHistory(), 54 | routes, 55 | }) 56 | app.use(router) 57 | app.mount('#app') 58 | 59 | --08---------- 60 | // this code was generated using 61 | import { createApp } from 'vue' 62 | import { createRouter, createWebHistory } from 'vue-router' 63 | import routes from 'virtual:generated-pages' 64 | import App from './App.vue' 65 | 66 | const app = createApp(App) 67 | const router = createRouter({ 68 | history: createWebHistory(), 69 | routes, 70 | }) 71 | app.use(router) 72 | app.mount('#app') 73 | 74 | --09---------- 75 | // this code was generated using https://github.com/antfu/retypewriter 76 | import { createApp } from 'vue' 77 | import { createRouter, createWebHistory } from 'vue-router' 78 | import routes from 'virtual:generated-pages' 79 | import App from './App.vue' 80 | 81 | const app = createApp(App) 82 | const router = createRouter({ 83 | history: createWebHistory(), 84 | routes, 85 | }) 86 | app.use(router) 87 | app.mount('#app') 88 | 89 | -----options-- 90 | wait: 200 91 | paste: true 92 | -------------- 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@retypewriter/monorepo", 3 | "version": "0.1.6", 4 | "private": true, 5 | "packageManager": "pnpm@8.15.4", 6 | "scripts": { 7 | "build": "pnpm -r run build", 8 | "dev": "vite playground", 9 | "play": "vite playground", 10 | "stub": "pnpm -r run stub", 11 | "lint": "eslint .", 12 | "release": "bumpp package.json packages/*/package.json && pnpm -r publish --access public --no-git-checks && pnpm -r run publish", 13 | "test": "vitest", 14 | "vscode": "nr -C packages/vscode dev", 15 | "typecheck": "vue-tsc --noEmit" 16 | }, 17 | "devDependencies": { 18 | "@antfu/eslint-config": "^2.7.0", 19 | "@antfu/ni": "^0.21.12", 20 | "@babel/types": "^7.24.0", 21 | "@retypewriter/cli": "workspace:./packages/cli", 22 | "@types/node": "^20.11.24", 23 | "@vscode/vsce": "^2.24.0", 24 | "@vueuse/core": "^10.9.0", 25 | "bumpp": "^9.3.1", 26 | "c8": "^9.1.0", 27 | "diff-match-patch-es": "^0.1.0", 28 | "eslint": "^8.57.0", 29 | "esno": "^4.0.0", 30 | "jiti": "^1.21.0", 31 | "pnpm": "^8.15.4", 32 | "retypewriter": "workspace:./packages/core", 33 | "rimraf": "^5.0.5", 34 | "typescript": "^5.3.3", 35 | "unbuild": "^2.0.0", 36 | "vite": "^5.1.4", 37 | "vitest": "^1.3.1", 38 | "vue-tsc": "^2.0.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/cli/bin/retypewriter.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import('../dist/cli.mjs') 3 | -------------------------------------------------------------------------------- /packages/cli/build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from 'unbuild' 2 | 3 | export default defineBuildConfig({ 4 | entries: [ 5 | 'src/index', 6 | 'src/cli', 7 | ], 8 | declaration: true, 9 | clean: true, 10 | rollup: { 11 | emitCJS: true, 12 | }, 13 | replace: { 14 | 'import.meta.vitest': 'undefined', 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@retypewriter/cli", 3 | "version": "0.1.6", 4 | "packageManager": "pnpm@8.15.4", 5 | "description": "reTypewriter CLI", 6 | "author": "Anthony Fu ", 7 | "license": "MIT", 8 | "funding": "https://github.com/sponsors/antfu", 9 | "homepage": "https://github.com/antfu/retypewriter#readme", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/antfu/retypewriter.git", 13 | "directory": "packages/cli" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/antfu/retypewriter/issues" 17 | }, 18 | "keywords": [], 19 | "sideEffects": false, 20 | "exports": { 21 | ".": { 22 | "types": "./dist/index.d.ts", 23 | "import": "./dist/index.mjs", 24 | "require": "./dist/index.cjs" 25 | } 26 | }, 27 | "main": "./dist/index.cjs", 28 | "module": "./dist/index.mjs", 29 | "types": "./dist/index.d.ts", 30 | "bin": { 31 | "retypewriter": "./bin/retypewriter.mjs" 32 | }, 33 | "files": [ 34 | "bin", 35 | "dist" 36 | ], 37 | "scripts": { 38 | "build": "rimraf dist && unbuild", 39 | "stub": "unbuild --stub", 40 | "prepublishOnly": "nr build" 41 | }, 42 | "peerDependencies": { 43 | "retypewriter": "workspace:../core" 44 | }, 45 | "dependencies": { 46 | "cac": "^6.7.14", 47 | "cli-cursor": "^4.0.0", 48 | "cli-highlight": "^2.1.11", 49 | "log-update": "^6.0.0", 50 | "simple-git": "^3.22.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/cli/src/cli.ts: -------------------------------------------------------------------------------- 1 | import { parse as parsePath, resolve } from 'node:path' 2 | import { promises as fs } from 'node:fs' 3 | import process from 'node:process' 4 | import cac from 'cac' 5 | import { Snapshots, getSnapshotPath } from 'retypewriter' 6 | import { version } from '../package.json' 7 | import { loadFromGit } from './git' 8 | import { playInTerminal } from './terminal' 9 | 10 | const cli = cac('retypewriter') 11 | 12 | cli 13 | .version(version) 14 | .option('-r, --root ', 'root path') 15 | .option('--git', 'Read from Git history') 16 | .option('--lang ', 'language id for syntax highlighting') 17 | .help() 18 | 19 | cli 20 | .command('play ') 21 | .action(play) 22 | 23 | cli 24 | .command('') 25 | .action(() => { 26 | cli.outputHelp() 27 | }) 28 | 29 | cli.parse() 30 | 31 | export interface CliOptions { 32 | git?: string 33 | root?: string 34 | } 35 | 36 | async function play(file: string, options: CliOptions) { 37 | const { 38 | root = process.cwd(), 39 | git, 40 | } = options 41 | 42 | const absolute = resolve(root, file) 43 | const snaps = git 44 | ? await loadFromGit(absolute, root) 45 | : await loadFromFile(absolute) 46 | 47 | const { ext } = parsePath(file) 48 | playInTerminal(snaps, ext.slice(1)) 49 | } 50 | 51 | async function loadFromFile(file: string) { 52 | return Snapshots.fromString(await fs.readFile(getSnapshotPath(file), 'utf-8')) 53 | } 54 | -------------------------------------------------------------------------------- /packages/cli/src/git.ts: -------------------------------------------------------------------------------- 1 | import { relative, resolve } from 'node:path' 2 | import process from 'node:process' 3 | import { Snapshots } from 'retypewriter' 4 | import Git from 'simple-git' 5 | import type { Snapshot } from 'retypewriter' 6 | 7 | export async function loadFromGit(path: string, cwd = process.cwd()) { 8 | const git = Git(cwd) 9 | const root = (await git.raw(['rev-parse', '--show-toplevel'])).trim() 10 | const full = resolve(process.cwd(), path) 11 | const file = relative(root, full) 12 | const items = await git.log({ 13 | file, 14 | }) 15 | const snaps = await Promise.all(items.all.map(async (): Promise => { 16 | const content = await git.show(`${items.latest!.hash}:${file}`) 17 | return { 18 | content, 19 | } 20 | })) 21 | return new Snapshots(...snaps) 22 | } 23 | -------------------------------------------------------------------------------- /packages/cli/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './git' 2 | -------------------------------------------------------------------------------- /packages/cli/src/terminal.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import process from 'node:process' 3 | import type { Snapshots } from 'retypewriter' 4 | import { highlight } from 'cli-highlight' 5 | import { createLogUpdate } from 'log-update' 6 | import cliCursor from 'cli-cursor' 7 | 8 | const langMap: Record = { 9 | ts: 'typescript', 10 | } 11 | 12 | export async function playInTerminal(snaps: Snapshots, lang?: string) { 13 | console.clear() 14 | const log = createLogUpdate( 15 | process.stdout, 16 | { showCursor: true }, 17 | ) 18 | 19 | for await (const snap of snaps.typewriter()) { 20 | switch (snap.type) { 21 | case 'init': 22 | case 'insert': 23 | case 'removal': 24 | case 'snap-finish': 25 | // reset cursor 26 | cliCursor.hide() 27 | process.stdout.cursorTo(0, snap.content.split('\n').length - 1) 28 | // log 29 | log(highlight(snap.content, { 30 | language: lang ? langMap[lang] : lang, 31 | ignoreIllegals: true, 32 | })) 33 | // cursor 34 | if (snap.type !== 'init' && snap.type !== 'snap-finish') { 35 | const pre = snap.content.slice(0, snap.cursor) 36 | const lines = pre.split('\n') 37 | const char = lines[lines.length - 1].length 38 | process.stdout.cursorTo(char, lines.length - 1) 39 | cliCursor.show() 40 | } 41 | break 42 | } 43 | } 44 | 45 | log.done() 46 | } 47 | -------------------------------------------------------------------------------- /packages/core/build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from 'unbuild' 2 | 3 | export default defineBuildConfig({ 4 | entries: [ 5 | 'src/index', 6 | ], 7 | declaration: true, 8 | clean: true, 9 | rollup: { 10 | emitCJS: true, 11 | inlineDependencies: true, 12 | }, 13 | replace: { 14 | 'import.meta.vitest': 'undefined', 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "retypewriter", 3 | "version": "0.1.6", 4 | "packageManager": "pnpm@8.15.4", 5 | "description": "", 6 | "author": "Anthony Fu ", 7 | "license": "MIT", 8 | "funding": "https://github.com/sponsors/antfu", 9 | "homepage": "https://github.com/antfu/retypewriter#readme", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/antfu/retypewriter.git", 13 | "directory": "packages/core" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/antfu/retypewriter/issues" 17 | }, 18 | "keywords": [], 19 | "sideEffects": false, 20 | "exports": { 21 | ".": { 22 | "types": "./dist/index.d.ts", 23 | "import": "./dist/index.mjs", 24 | "require": "./dist/index.cjs" 25 | } 26 | }, 27 | "main": "./dist/index.cjs", 28 | "module": "./dist/index.mjs", 29 | "types": "./dist/index.d.ts", 30 | "files": [ 31 | "dist" 32 | ], 33 | "scripts": { 34 | "build": "rimraf dist && unbuild", 35 | "stub": "unbuild --stub", 36 | "prepublishOnly": "nr build" 37 | }, 38 | "dependencies": { 39 | "@retypewriter/cli": "workspace:*", 40 | "diff-match-patch-es": "^0.1.0" 41 | }, 42 | "devDependencies": { 43 | "@types/js-yaml": "^4.0.9", 44 | "js-yaml": "^4.1.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/core/src/animation/slicing.ts: -------------------------------------------------------------------------------- 1 | import type { Slice } from '../types' 2 | 3 | const pairs = [ 4 | ['{', '}'], 5 | ['(', ')'], 6 | ['[', ']'], 7 | ['<', '>'], 8 | ['`', '`'], 9 | ['\'', '\''], 10 | ['"', '"'], 11 | ] 12 | 13 | const chars = '(){}\\[\\]<>\'"`' 14 | const splitRegex = new RegExp(`(\\s*[${chars}]\\s*)`) 15 | 16 | export function sliceInput(input: string): Slice[] { 17 | if (!input) 18 | return [] 19 | const tailingNewLines = input.match(/\n+$/m)?.[0] || '' 20 | const rest = tailingNewLines.length 21 | ? input.slice(0, -tailingNewLines.length) 22 | : input 23 | 24 | const strings = rest.split(splitRegex).filter(Boolean) 25 | 26 | let index = 0 27 | const slices = strings 28 | .map((s, idx) => { 29 | const trimmed = s.trim() 30 | const pair = pairs.find(i => i[1] === trimmed) 31 | const order = (pair && strings[idx - 2]?.trim() === pair[0]) 32 | ? idx - 2 // paired with previous 33 | : idx 34 | 35 | const _index = index 36 | index += s.length 37 | 38 | return { 39 | content: s, 40 | order, 41 | cursor: _index, 42 | } 43 | }) 44 | .sort((a, b) => a.order - b.order) 45 | 46 | let cursor = 0 47 | slices.forEach((i) => { 48 | i.cursor = Math.min(i.cursor, cursor) 49 | cursor += i.content.length 50 | }) 51 | 52 | if (tailingNewLines.length) { 53 | slices.unshift({ 54 | content: tailingNewLines, 55 | order: -1, 56 | cursor: 0, 57 | }) 58 | } 59 | 60 | return slices 61 | } 62 | 63 | export function applySlice(slices: Slice[]) { 64 | const result = slices.reduce((acc, i) => { 65 | const { content: contet, cursor } = i 66 | return acc.slice(0, cursor) + contet + acc.slice(cursor) 67 | }, '') 68 | return result 69 | } 70 | -------------------------------------------------------------------------------- /packages/core/src/animation/steps.ts: -------------------------------------------------------------------------------- 1 | import { calculatePatch, diff } from '../state/patch' 2 | import type { AnimatorStep, Patch, Snapshot } from '../types' 3 | import { sliceInput } from './slicing' 4 | 5 | export function *patchSteps(input: string, patches: Patch[]): Generator { 6 | let output = input 7 | let cursor = 0 8 | 9 | for (let index = 0; index < patches.length; index++) { 10 | const patch = patches[index] 11 | 12 | yield { 13 | type: 'patch-start', 14 | patch, 15 | index, 16 | total: patches.length, 17 | } 18 | 19 | if (patch.type === 'insert') { 20 | cursor = patch.cursor 21 | const head = output.slice(0, patch.cursor) 22 | const tail = output.slice(patch.cursor) 23 | for (const { char, output, cursor: delta } of animateInsertionSlices(patch.content)) { 24 | yield { 25 | type: 'insert', 26 | char, 27 | cursor: cursor + delta, 28 | content: head + output + tail, 29 | } 30 | } 31 | 32 | output = head + patch.content + tail 33 | } 34 | else if (patch.type === 'paste') { 35 | cursor = patch.cursor 36 | const head = output.slice(0, patch.cursor) 37 | const tail = output.slice(patch.cursor) 38 | const { content } = patch 39 | 40 | yield { 41 | type: 'paste', 42 | cursor: cursor + content.length, 43 | content, 44 | } 45 | 46 | output = head + patch.content + tail 47 | } 48 | else if (patch.type === 'removal') { 49 | cursor = patch.cursor - patch.length 50 | const head = output.slice(0, cursor) 51 | const tail = output.slice(patch.cursor) 52 | const selection = output.slice(cursor, patch.cursor) 53 | for (let i = selection.length - 1; i >= 0; i--) { 54 | yield { 55 | type: 'removal', 56 | cursor: cursor + i, 57 | content: head + selection.slice(0, i) + tail, 58 | } 59 | } 60 | output = head + tail 61 | } 62 | 63 | yield { 64 | type: 'patch-finish', 65 | content: output, 66 | index, 67 | total: patches.length, 68 | } 69 | } 70 | } 71 | 72 | export function *animateSteps(snapshots: Snapshot[]): Generator { 73 | let lastContent: string | undefined 74 | const copy = [...snapshots] 75 | for (let index = 0; index < copy.length; index++) { 76 | const snap = copy[index] 77 | if (lastContent == null) { 78 | lastContent = snap.content 79 | yield { 80 | type: 'init', 81 | content: lastContent, 82 | } 83 | continue 84 | } 85 | 86 | yield { 87 | type: 'snap-start', 88 | snap, 89 | index, 90 | total: copy.length, 91 | } 92 | 93 | const isPasted = snap.options?.paste 94 | 95 | const steps = stepsTo(lastContent, snap.content, isPasted) 96 | for (const step of steps) 97 | yield step 98 | 99 | yield { 100 | type: 'snap-finish', 101 | content: snap.content, 102 | snap, 103 | index, 104 | total: copy.length, 105 | } 106 | 107 | lastContent = snap.content 108 | } 109 | } 110 | 111 | export function *animateInsertionSlices(input: string) { 112 | const slices = sliceInput(input) 113 | let output = '' 114 | for (const { content, cursor } of slices) { 115 | const head = output.slice(0, cursor) 116 | const tail = output.slice(cursor) 117 | 118 | let body = '' 119 | for (const char of content) { 120 | body += char 121 | yield { 122 | char, 123 | output: head + body + tail, 124 | cursor: cursor + body.length, 125 | } 126 | } 127 | output = head + content + tail 128 | } 129 | } 130 | 131 | export function stepsTo(input: string, output: string, isPasted?: boolean) { 132 | const delta = diff(input, output) 133 | const patches = calculatePatch(delta, isPasted) 134 | return patchSteps(input, patches) 135 | } 136 | -------------------------------------------------------------------------------- /packages/core/src/animation/timing.ts: -------------------------------------------------------------------------------- 1 | // TODO: improve this with some real data 2 | export const timeRanges: [string, number, number][] = [ 3 | // default 4 | ['', 0, 100], 5 | // commons 6 | [' ', 0, 40], 7 | ['adefijkloosu', 0, 50], 8 | ['bcghmnpqrtvwxyz', 5, 70], 9 | // uppercase 10 | ['adefijkloosu'.toUpperCase(), 0, 80], 11 | ['bcghmnpqrtvwxyz'.toUpperCase(), 5, 100], 12 | // a bit far 13 | ['[];\',./-=', 10, 120], 14 | // need shift 15 | ['<>?:"{}|\\_+()*', 20, 140], 16 | // a bit far and shift 17 | ['!~`#$%^&', 20, 180], 18 | ] 19 | 20 | export function randRange(min: number, max: number): number { 21 | return Math.random() * (max - min) + min 22 | } 23 | 24 | export function getTimeout(char: string, multiplier = 1): number { 25 | const [, min, max] = timeRanges.find(range => range[0].includes(char)) || timeRanges[0] 26 | return randRange(min, max) * multiplier 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/animation/typewriter.ts: -------------------------------------------------------------------------------- 1 | import type { AnimatorStep, Snapshot, SnapshotOptions } from '../types' 2 | import { getTimeout, randRange } from './timing' 3 | 4 | export function sleep(ms: number) { 5 | return new Promise(resolve => setTimeout(resolve, ms)) 6 | } 7 | 8 | export interface TypewriterOptions { 9 | defaults?: SnapshotOptions 10 | } 11 | 12 | export async function *typingAnimator( 13 | steps: Generator, 14 | 15 | options: TypewriterOptions = {}, 16 | ): AsyncGenerator { 17 | function getOptions(snap: Snapshot) { 18 | return { 19 | ...(options.defaults || {}), 20 | ...(snap.options || {}), 21 | } 22 | } 23 | 24 | for (const step of steps) { 25 | switch (step.type) { 26 | case 'init': 27 | break 28 | case 'snap-start': 29 | if (step.index) { 30 | const { wait } = getOptions(step.snap) 31 | 32 | await sleep(wait !== undefined ? wait : randRange(700, 1000)) 33 | } 34 | if (getOptions(step.snap).pause) { 35 | yield { 36 | type: 'action-pause', 37 | snap: step.snap, 38 | } 39 | } 40 | break 41 | case 'patch-start': 42 | if (step.index) 43 | await sleep(randRange(200, 500)) 44 | break 45 | case 'insert': 46 | await sleep(getTimeout(step.char, 1.2)) 47 | break 48 | case 'paste': 49 | await sleep(randRange(100, 200)) 50 | break 51 | case 'removal': 52 | await sleep(randRange(0, 5)) 53 | break 54 | } 55 | yield step 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types' 2 | export * from './state/patch' 3 | export * from './state/snaps' 4 | export * from './state/parse' 5 | export * from './animation/steps' 6 | export * from './animation/slicing' 7 | export * from './animation/timing' 8 | export * from './animation/typewriter' 9 | -------------------------------------------------------------------------------- /packages/core/src/state/parse.ts: -------------------------------------------------------------------------------- 1 | import YAML from 'js-yaml' 2 | import type { ParsedHead, ParsedSnaphot, SnapshotOptions } from '../types' 3 | import type { Snapshots } from './snaps' 4 | 5 | export const SNAP_EXT = '.retypewriter' 6 | export const SNAP_HEADING = 'reTypewriter Snapshots v1\n' 7 | export const SNAP_SEPERATOR_PRE = '-'.repeat(2) 8 | export const SNAP_SEPERATOR_POST = '-'.repeat(10) 9 | export const SNAP_SEPERATOR = `${SNAP_SEPERATOR_PRE}--${SNAP_SEPERATOR_POST}` 10 | export const SNAP_SEPERATOR_OPTIONS = '-----options--' 11 | export const SNAP_SEPERATOR_MATCHER = new RegExp(`\\n?${SNAP_SEPERATOR_PRE}[\\w-]{2}${SNAP_SEPERATOR_POST}\\n`, 'g') 12 | export const SNAP_SEPERATOR_OPTIONS_MATCHER = new RegExp(`\\n?${SNAP_SEPERATOR_OPTIONS}\\n`, 'g') 13 | 14 | export function parseOptions(raw: string): SnapshotOptions | undefined { 15 | raw = raw.trim() 16 | if (!raw) 17 | return undefined 18 | return (raw.startsWith('{')) 19 | ? JSON.parse(raw) 20 | : YAML.load(raw) 21 | } 22 | 23 | export function stringifySnapshots(snapshots: Snapshots, useYaml = true) { 24 | function stringify(obj: any) { 25 | if (!Object.keys(obj).length) 26 | return undefined 27 | return useYaml 28 | ? YAML.dump(obj, { indent: 2 }).trimEnd() 29 | : Object.keys(obj).length > 1 30 | ? JSON.stringify(obj, null, 2) 31 | : JSON.stringify(obj) 32 | } 33 | 34 | return [ 35 | SNAP_HEADING, 36 | stringify(snapshots.defaults), 37 | ...snapshots 38 | .flatMap((snap, i) => [ 39 | SNAP_SEPERATOR_PRE + (i + 1).toString().padStart(2, '0') + SNAP_SEPERATOR_POST, 40 | snap.content, 41 | ...( 42 | snap.options 43 | ? [ 44 | SNAP_SEPERATOR_OPTIONS, 45 | stringify(snap.options), 46 | ] 47 | : [] 48 | ), 49 | ]), 50 | SNAP_SEPERATOR, 51 | '', 52 | ] 53 | .filter(i => i != null) 54 | .join('\n') 55 | } 56 | 57 | export function parseSnapshots(raw: string) { 58 | if (!raw.startsWith(SNAP_HEADING)) 59 | throw new SyntaxError('Invalid snapshot file') 60 | if (!raw.endsWith('\n')) 61 | raw += '\n' 62 | 63 | const head: ParsedHead = {} 64 | const snaps: ParsedSnaphot[] = [] 65 | 66 | SNAP_SEPERATOR_MATCHER.lastIndex = 0 67 | const matches = Array.from(raw.matchAll(SNAP_SEPERATOR_MATCHER)) 68 | 69 | matches.forEach((match, idx) => { 70 | const start = match.index! 71 | 72 | if (idx === 0) { 73 | const content = raw.slice(0, start) 74 | head.options = parseOptions(content.slice(SNAP_HEADING.length)) 75 | } 76 | 77 | const next = matches[idx + 1] 78 | const end = next?.index ?? raw.length 79 | const bodyStart = start + match[0].length 80 | const withOptions = raw.slice(bodyStart, end) 81 | const parts = withOptions.split(SNAP_SEPERATOR_OPTIONS_MATCHER) 82 | const body = parts[0] 83 | const bodyEnd = bodyStart + body.length 84 | const snap: ParsedSnaphot = { 85 | raw: raw.slice(start, end), 86 | start, 87 | end, 88 | body, 89 | bodyEnd, 90 | bodyStart, 91 | } 92 | const optionsRaw = parts[1]?.trim() 93 | if (optionsRaw) { 94 | snap.optionsRaw = optionsRaw 95 | snap.options = parseOptions(optionsRaw) 96 | } 97 | snaps.push(snap) 98 | }) 99 | 100 | // remove tailing separator 101 | snaps.pop() 102 | 103 | return { 104 | head, 105 | snapshots: snaps, 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /packages/core/src/state/patch.ts: -------------------------------------------------------------------------------- 1 | import type { Diff } from 'diff-match-patch-es' 2 | import { diffCleanupSemantic, diffMain } from 'diff-match-patch-es' 3 | import type { Patch } from '../types' 4 | 5 | export function diff(a: string, b: string): Diff[] { 6 | const delta = diffMain(a, b) 7 | diffCleanupSemantic(delta) 8 | return delta 9 | } 10 | 11 | export function calculatePatch(diff: Diff[], isPasted?: boolean): Patch[] { 12 | const patches: Patch[] = [] 13 | 14 | let cursor = 0 15 | for (const change of diff) { 16 | if (change[0] === 0) { 17 | cursor += change[1].length 18 | } 19 | else if (change[0] === -1) { 20 | const length = change[1].length 21 | patches.push({ 22 | type: 'removal', 23 | cursor: cursor + length, 24 | length, 25 | }) 26 | } 27 | else if (change[0] === 1) { 28 | const content = change[1] 29 | patches.push({ 30 | type: isPasted ? 'paste' : 'insert', 31 | cursor, 32 | content, 33 | }) 34 | cursor += content.length 35 | } 36 | else { 37 | throw new Error('unknown change type') 38 | } 39 | } 40 | return patches 41 | } 42 | -------------------------------------------------------------------------------- /packages/core/src/state/snaps.ts: -------------------------------------------------------------------------------- 1 | import type { Snapshot, SnapshotOptions } from '../types' 2 | import { animateSteps } from '../animation/steps' 3 | import type { TypewriterOptions } from '../animation/typewriter' 4 | import { typingAnimator } from '../animation/typewriter' 5 | import { SNAP_EXT, parseSnapshots, stringifySnapshots } from './parse' 6 | 7 | export class Snapshots extends Array { 8 | public defaults: SnapshotOptions = {} 9 | 10 | constructor(...args: Snapshot[]) { 11 | super(...args) 12 | } 13 | 14 | get last() { 15 | return this[this.length - 1] 16 | } 17 | 18 | get first() { 19 | return this[0] 20 | } 21 | 22 | move(from: number, to: number) { 23 | if (from === to) 24 | return 25 | 26 | const element = this[from] 27 | this.splice(from, 1) 28 | this.splice(to, 0, element) 29 | } 30 | 31 | duplicate(index: number) { 32 | const snap = this[index] 33 | this.splice(index + 1, 0, snap) 34 | } 35 | 36 | remove(index: number) { 37 | this.splice(index, 1) 38 | } 39 | 40 | toString(useYaml = true): string { 41 | return stringifySnapshots(this, useYaml) 42 | } 43 | 44 | fromString(raw: string) { 45 | const { snapshots: parsed, head } = parseSnapshots(raw) 46 | this.length = 0 47 | this.defaults = head.options || {} 48 | parsed.forEach((p) => { 49 | this.push({ 50 | content: p.body, 51 | options: p.options, 52 | }) 53 | }) 54 | return this 55 | } 56 | 57 | static fromString(raw: string) { 58 | return new Snapshots().fromString(raw) 59 | } 60 | 61 | steps() { 62 | return animateSteps(this) 63 | } 64 | 65 | typewriter(options?: TypewriterOptions) { 66 | return typingAnimator(this.steps(), { 67 | ...options, 68 | }) 69 | } 70 | } 71 | 72 | export type SnapshotFallbackLoader = (id: string) => Snapshots | undefined | Promise 73 | export interface SnapshotManagerOptions { 74 | ensureFallback?: SnapshotFallbackLoader 75 | } 76 | 77 | export class SnapshotManager extends Map { 78 | constructor( 79 | public options: SnapshotManagerOptions = {}, 80 | ) { 81 | super() 82 | } 83 | 84 | async ensure( 85 | id: string, 86 | load = this.options.ensureFallback, 87 | ) { 88 | if (!this.has(id)) 89 | this.set(id, await load?.(id) || new Snapshots()) 90 | return this.get(id)! 91 | } 92 | } 93 | 94 | export function getSnapshotPath(id: string) { 95 | if (id.endsWith(SNAP_EXT)) 96 | return id 97 | return id + SNAP_EXT 98 | } 99 | 100 | export function getOriginalFilePath(id: string) { 101 | if (id.endsWith(SNAP_EXT)) 102 | return id.slice(0, -SNAP_EXT.length) 103 | return undefined 104 | } 105 | -------------------------------------------------------------------------------- /packages/core/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface InsertPatch { 2 | type: 'insert' 3 | cursor: number 4 | content: string 5 | } 6 | 7 | export interface PastePatch { 8 | type: 'paste' 9 | cursor: number 10 | content: string 11 | } 12 | 13 | export interface RemovalPatch { 14 | type: 'removal' 15 | cursor: number 16 | length: number 17 | } 18 | 19 | export type Patch = InsertPatch | PastePatch | RemovalPatch 20 | 21 | export interface Slice { 22 | content: string 23 | order: number 24 | cursor: number 25 | } 26 | 27 | export interface Snapshot { 28 | content: string 29 | options?: SnapshotOptions 30 | } 31 | 32 | export interface SnapshotOptions { 33 | wait?: number 34 | pause?: boolean 35 | paste?: boolean 36 | } 37 | 38 | export interface AnimatorStepInsert { 39 | type: 'insert' | 'delete' 40 | cursor: number 41 | content: string 42 | char: string 43 | } 44 | 45 | export interface AnimatorStepPaste { 46 | type: 'paste' 47 | cursor: number 48 | content: string 49 | } 50 | 51 | export interface AnimatorStepRemoval { 52 | type: 'removal' 53 | cursor: number 54 | content: string 55 | } 56 | 57 | export interface AnimatorStepInit { 58 | type: 'init' 59 | content: string 60 | } 61 | 62 | export interface AnimatorStepPatch { 63 | type: 'patch-start' 64 | patch: Patch 65 | index: number 66 | total: number 67 | } 68 | 69 | export interface AnimatorStepSnap { 70 | type: 'snap-start' 71 | snap: Snapshot 72 | index: number 73 | total: number 74 | } 75 | 76 | export interface AnimatorStepSnapFinish { 77 | type: 'snap-finish' 78 | snap: Snapshot 79 | content: string 80 | index: number 81 | total: number 82 | } 83 | 84 | export interface AnimatorStepPatchFinish { 85 | type: 'patch-finish' 86 | content: string 87 | index: number 88 | total: number 89 | } 90 | 91 | export interface AnimatorStepActionPause { 92 | type: 'action-pause' 93 | snap: Snapshot 94 | } 95 | 96 | export type AnimatorStep = 97 | | AnimatorStepInsert 98 | | AnimatorStepPaste 99 | | AnimatorStepRemoval 100 | | AnimatorStepInit 101 | | AnimatorStepPatch 102 | | AnimatorStepSnap 103 | | AnimatorStepSnapFinish 104 | | AnimatorStepPatchFinish 105 | | AnimatorStepActionPause 106 | 107 | export interface ParsedSnaphot { 108 | raw: string 109 | start: number 110 | end: number 111 | body: string 112 | bodyStart: number 113 | bodyEnd: number 114 | optionsRaw?: string 115 | options?: SnapshotOptions 116 | } 117 | 118 | export interface ParsedHead { 119 | options?: SnapshotOptions 120 | } 121 | -------------------------------------------------------------------------------- /packages/core/test/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`should > exported > delta 1`] = ` 4 | [ 5 | [ 6 | 0, 7 | " 8 | import { describe, expect, it } from 'vitest'", 9 | ], 10 | [ 11 | -1, 12 | " 13 | import { one } from '../src'", 14 | ], 15 | [ 16 | 0, 17 | " 18 | 19 | describe('should', () => { 20 | it('", 21 | ], 22 | [ 23 | -1, 24 | "exported", 25 | ], 26 | [ 27 | 1, 28 | "one", 29 | ], 30 | [ 31 | 0, 32 | "', () => { 33 | expect(one).toEqual(1) 34 | ", 35 | ], 36 | [ 37 | 1, 38 | " expect(two).toEqual(2) 39 | ", 40 | ], 41 | [ 42 | 0, 43 | " }) 44 | }) 45 | ", 46 | ], 47 | ] 48 | `; 49 | 50 | exports[`should > exported > output 1`] = ` 51 | " 52 | import { describe, expect, it } from 'vitest' 53 | 54 | describe('should', () => { 55 | it('one', () => { 56 | expect(one).toEqual(1) 57 | expect(two).toEqual(2) 58 | }) 59 | }) 60 | " 61 | `; 62 | 63 | exports[`should > exported > patches 1`] = ` 64 | [ 65 | { 66 | "cursor": 75, 67 | "length": 29, 68 | "type": "removal", 69 | }, 70 | { 71 | "cursor": 89, 72 | "length": 8, 73 | "type": "removal", 74 | }, 75 | { 76 | "content": "one", 77 | "cursor": 81, 78 | "type": "insert", 79 | }, 80 | { 81 | "content": " expect(two).toEqual(2) 82 | ", 83 | "cursor": 122, 84 | "type": "insert", 85 | }, 86 | ] 87 | `; 88 | -------------------------------------------------------------------------------- /packages/core/test/__snapshots__/parse.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`works 1`] = ` 4 | { 5 | "head": { 6 | "options": { 7 | "delay": 100, 8 | "pause": true, 9 | }, 10 | }, 11 | "snapshots": [ 12 | { 13 | "body": "", 14 | "bodyEnd": 65, 15 | "bodyStart": 65, 16 | "end": 65, 17 | "raw": " 18 | --01---------- 19 | ", 20 | "start": 49, 21 | }, 22 | { 23 | "body": "import App from './App.vue' 24 | ", 25 | "bodyEnd": 109, 26 | "bodyStart": 81, 27 | "end": 109, 28 | "raw": " 29 | --02---------- 30 | import App from './App.vue' 31 | ", 32 | "start": 65, 33 | }, 34 | { 35 | "body": "import { } from 'vue' 36 | import App from './App.vue' 37 | ", 38 | "bodyEnd": 175, 39 | "bodyStart": 125, 40 | "end": 175, 41 | "raw": " 42 | --03---------- 43 | import { } from 'vue' 44 | import App from './App.vue' 45 | ", 46 | "start": 109, 47 | }, 48 | { 49 | "body": "import { createApp } from 'vue' 50 | import App from './App.vue' 51 | 52 | const app = createApp(App) 53 | app.mount('#app') 54 | ", 55 | "bodyEnd": 297, 56 | "bodyStart": 191, 57 | "end": 324, 58 | "options": { 59 | "pause": true, 60 | }, 61 | "optionsRaw": "pause: true", 62 | "raw": " 63 | --04---------- 64 | import { createApp } from 'vue' 65 | import App from './App.vue' 66 | 67 | const app = createApp(App) 68 | app.mount('#app') 69 | 70 | -----options-- 71 | pause: true", 72 | "start": 175, 73 | }, 74 | { 75 | "body": "import { createApp } from 'vue' 76 | import { createRouter } from 'vue-router' 77 | import App from './App.vue' 78 | 79 | const app = createApp(App) 80 | const router = createRouter() 81 | app.use(router) 82 | app.mount('#app') 83 | ", 84 | "bodyEnd": 534, 85 | "bodyStart": 340, 86 | "end": 534, 87 | "raw": " 88 | --05---------- 89 | import { createApp } from 'vue' 90 | import { createRouter } from 'vue-router' 91 | import App from './App.vue' 92 | 93 | const app = createApp(App) 94 | const router = createRouter() 95 | app.use(router) 96 | app.mount('#app') 97 | ", 98 | "start": 324, 99 | }, 100 | { 101 | "body": "import { createApp } from 'vue' 102 | import { createRouter, createWebHistory } from 'vue-router' 103 | import App from './App.vue' 104 | 105 | const app = createApp(App) 106 | const router = createRouter({ 107 | history: createWebHistory(), 108 | }) 109 | app.use(router) 110 | app.mount('#app') 111 | ", 112 | "bodyEnd": 796, 113 | "bodyStart": 550, 114 | "end": 796, 115 | "raw": " 116 | --06---------- 117 | import { createApp } from 'vue' 118 | import { createRouter, createWebHistory } from 'vue-router' 119 | import App from './App.vue' 120 | 121 | const app = createApp(App) 122 | const router = createRouter({ 123 | history: createWebHistory(), 124 | }) 125 | app.use(router) 126 | app.mount('#app') 127 | ", 128 | "start": 534, 129 | }, 130 | { 131 | "body": "import { createApp } from 'vue' 132 | import { createRouter, createWebHistory } from 'vue-router' 133 | import routes from 'virtual:generated-pages' 134 | import App from './App.vue' 135 | 136 | const app = createApp(App) 137 | const router = createRouter({ 138 | history: createWebHistory(), 139 | routes, 140 | }) 141 | app.use(router) 142 | app.mount('#app') 143 | ", 144 | "bodyEnd": 1113, 145 | "bodyStart": 812, 146 | "end": 1113, 147 | "raw": " 148 | --07---------- 149 | import { createApp } from 'vue' 150 | import { createRouter, createWebHistory } from 'vue-router' 151 | import routes from 'virtual:generated-pages' 152 | import App from './App.vue' 153 | 154 | const app = createApp(App) 155 | const router = createRouter({ 156 | history: createWebHistory(), 157 | routes, 158 | }) 159 | app.use(router) 160 | app.mount('#app') 161 | ", 162 | "start": 796, 163 | }, 164 | { 165 | "body": "// this code was generated using 166 | import { createApp } from 'vue' 167 | import { createRouter, createWebHistory } from 'vue-router' 168 | import routes from 'virtual:generated-pages' 169 | import App from './App.vue' 170 | 171 | const app = createApp(App) 172 | const router = createRouter({ 173 | history: createWebHistory(), 174 | routes, 175 | }) 176 | app.use(router) 177 | app.mount('#app') 178 | ", 179 | "bodyEnd": 1463, 180 | "bodyStart": 1129, 181 | "end": 1463, 182 | "raw": " 183 | --08---------- 184 | // this code was generated using 185 | import { createApp } from 'vue' 186 | import { createRouter, createWebHistory } from 'vue-router' 187 | import routes from 'virtual:generated-pages' 188 | import App from './App.vue' 189 | 190 | const app = createApp(App) 191 | const router = createRouter({ 192 | history: createWebHistory(), 193 | routes, 194 | }) 195 | app.use(router) 196 | app.mount('#app') 197 | ", 198 | "start": 1113, 199 | }, 200 | { 201 | "body": "// this code was generated using https://github.com/antfu/retypewriter 202 | import { createApp } from 'vue' 203 | import { createRouter, createWebHistory } from 'vue-router' 204 | import routes from 'virtual:generated-pages' 205 | import App from './App.vue' 206 | 207 | const app = createApp(App) 208 | const router = createRouter({ 209 | history: createWebHistory(), 210 | routes, 211 | }) 212 | app.use(router) 213 | app.mount('#app') 214 | ", 215 | "bodyEnd": 1851, 216 | "bodyStart": 1479, 217 | "end": 1888, 218 | "options": { 219 | "paste": true, 220 | "wait": 200, 221 | }, 222 | "optionsRaw": "wait: 200 223 | paste: true", 224 | "raw": " 225 | --09---------- 226 | // this code was generated using https://github.com/antfu/retypewriter 227 | import { createApp } from 'vue' 228 | import { createRouter, createWebHistory } from 'vue-router' 229 | import routes from 'virtual:generated-pages' 230 | import App from './App.vue' 231 | 232 | const app = createApp(App) 233 | const router = createRouter({ 234 | history: createWebHistory(), 235 | routes, 236 | }) 237 | app.use(router) 238 | app.mount('#app') 239 | 240 | -----options-- 241 | wait: 200 242 | paste: true", 243 | "start": 1463, 244 | }, 245 | ], 246 | } 247 | `; 248 | -------------------------------------------------------------------------------- /packages/core/test/__snapshots__/snaps.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`snaps 1`] = ` 4 | "reTypewriter Snapshots v1 5 | 6 | --01---------- 7 | 8 | --02---------- 9 | import {} from "vue" 10 | -----options-- 11 | pause: true 12 | --03---------- 13 | import { createApp } from "vue" 14 | 15 | const app = createApp() 16 | 17 | -------------- 18 | " 19 | `; 20 | -------------------------------------------------------------------------------- /packages/core/test/__snapshots__/steps.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`steps > steps 1`] = ` 4 | [ 5 | { 6 | "index": 0, 7 | "patch": { 8 | "cursor": 75, 9 | "length": 29, 10 | "type": "removal", 11 | }, 12 | "total": 4, 13 | "type": "patch-start", 14 | }, 15 | { 16 | "content": " 17 | import { describe, expect, it } from 'vitest' 18 | import { one } from '../src 19 | 20 | describe('should', () => { 21 | it('exported', () => { 22 | expect(one).toEqual(1) 23 | }) 24 | }) 25 | ", 26 | "cursor": 74, 27 | "type": "removal", 28 | }, 29 | { 30 | "content": " 31 | import { describe, expect, it } from 'vitest' 32 | import { one } from '../sr 33 | 34 | describe('should', () => { 35 | it('exported', () => { 36 | expect(one).toEqual(1) 37 | }) 38 | }) 39 | ", 40 | "cursor": 73, 41 | "type": "removal", 42 | }, 43 | { 44 | "content": " 45 | import { describe, expect, it } from 'vitest' 46 | import { one } from '../s 47 | 48 | describe('should', () => { 49 | it('exported', () => { 50 | expect(one).toEqual(1) 51 | }) 52 | }) 53 | ", 54 | "cursor": 72, 55 | "type": "removal", 56 | }, 57 | { 58 | "content": " 59 | import { describe, expect, it } from 'vitest' 60 | import { one } from '../ 61 | 62 | describe('should', () => { 63 | it('exported', () => { 64 | expect(one).toEqual(1) 65 | }) 66 | }) 67 | ", 68 | "cursor": 71, 69 | "type": "removal", 70 | }, 71 | { 72 | "content": " 73 | import { describe, expect, it } from 'vitest' 74 | import { one } from '.. 75 | 76 | describe('should', () => { 77 | it('exported', () => { 78 | expect(one).toEqual(1) 79 | }) 80 | }) 81 | ", 82 | "cursor": 70, 83 | "type": "removal", 84 | }, 85 | { 86 | "content": " 87 | import { describe, expect, it } from 'vitest' 88 | import { one } from '. 89 | 90 | describe('should', () => { 91 | it('exported', () => { 92 | expect(one).toEqual(1) 93 | }) 94 | }) 95 | ", 96 | "cursor": 69, 97 | "type": "removal", 98 | }, 99 | { 100 | "content": " 101 | import { describe, expect, it } from 'vitest' 102 | import { one } from ' 103 | 104 | describe('should', () => { 105 | it('exported', () => { 106 | expect(one).toEqual(1) 107 | }) 108 | }) 109 | ", 110 | "cursor": 68, 111 | "type": "removal", 112 | }, 113 | { 114 | "content": " 115 | import { describe, expect, it } from 'vitest' 116 | import { one } from 117 | 118 | describe('should', () => { 119 | it('exported', () => { 120 | expect(one).toEqual(1) 121 | }) 122 | }) 123 | ", 124 | "cursor": 67, 125 | "type": "removal", 126 | }, 127 | { 128 | "content": " 129 | import { describe, expect, it } from 'vitest' 130 | import { one } from 131 | 132 | describe('should', () => { 133 | it('exported', () => { 134 | expect(one).toEqual(1) 135 | }) 136 | }) 137 | ", 138 | "cursor": 66, 139 | "type": "removal", 140 | }, 141 | { 142 | "content": " 143 | import { describe, expect, it } from 'vitest' 144 | import { one } fro 145 | 146 | describe('should', () => { 147 | it('exported', () => { 148 | expect(one).toEqual(1) 149 | }) 150 | }) 151 | ", 152 | "cursor": 65, 153 | "type": "removal", 154 | }, 155 | { 156 | "content": " 157 | import { describe, expect, it } from 'vitest' 158 | import { one } fr 159 | 160 | describe('should', () => { 161 | it('exported', () => { 162 | expect(one).toEqual(1) 163 | }) 164 | }) 165 | ", 166 | "cursor": 64, 167 | "type": "removal", 168 | }, 169 | { 170 | "content": " 171 | import { describe, expect, it } from 'vitest' 172 | import { one } f 173 | 174 | describe('should', () => { 175 | it('exported', () => { 176 | expect(one).toEqual(1) 177 | }) 178 | }) 179 | ", 180 | "cursor": 63, 181 | "type": "removal", 182 | }, 183 | { 184 | "content": " 185 | import { describe, expect, it } from 'vitest' 186 | import { one } 187 | 188 | describe('should', () => { 189 | it('exported', () => { 190 | expect(one).toEqual(1) 191 | }) 192 | }) 193 | ", 194 | "cursor": 62, 195 | "type": "removal", 196 | }, 197 | { 198 | "content": " 199 | import { describe, expect, it } from 'vitest' 200 | import { one } 201 | 202 | describe('should', () => { 203 | it('exported', () => { 204 | expect(one).toEqual(1) 205 | }) 206 | }) 207 | ", 208 | "cursor": 61, 209 | "type": "removal", 210 | }, 211 | { 212 | "content": " 213 | import { describe, expect, it } from 'vitest' 214 | import { one 215 | 216 | describe('should', () => { 217 | it('exported', () => { 218 | expect(one).toEqual(1) 219 | }) 220 | }) 221 | ", 222 | "cursor": 60, 223 | "type": "removal", 224 | }, 225 | { 226 | "content": " 227 | import { describe, expect, it } from 'vitest' 228 | import { one 229 | 230 | describe('should', () => { 231 | it('exported', () => { 232 | expect(one).toEqual(1) 233 | }) 234 | }) 235 | ", 236 | "cursor": 59, 237 | "type": "removal", 238 | }, 239 | { 240 | "content": " 241 | import { describe, expect, it } from 'vitest' 242 | import { on 243 | 244 | describe('should', () => { 245 | it('exported', () => { 246 | expect(one).toEqual(1) 247 | }) 248 | }) 249 | ", 250 | "cursor": 58, 251 | "type": "removal", 252 | }, 253 | { 254 | "content": " 255 | import { describe, expect, it } from 'vitest' 256 | import { o 257 | 258 | describe('should', () => { 259 | it('exported', () => { 260 | expect(one).toEqual(1) 261 | }) 262 | }) 263 | ", 264 | "cursor": 57, 265 | "type": "removal", 266 | }, 267 | { 268 | "content": " 269 | import { describe, expect, it } from 'vitest' 270 | import { 271 | 272 | describe('should', () => { 273 | it('exported', () => { 274 | expect(one).toEqual(1) 275 | }) 276 | }) 277 | ", 278 | "cursor": 56, 279 | "type": "removal", 280 | }, 281 | { 282 | "content": " 283 | import { describe, expect, it } from 'vitest' 284 | import { 285 | 286 | describe('should', () => { 287 | it('exported', () => { 288 | expect(one).toEqual(1) 289 | }) 290 | }) 291 | ", 292 | "cursor": 55, 293 | "type": "removal", 294 | }, 295 | { 296 | "content": " 297 | import { describe, expect, it } from 'vitest' 298 | import 299 | 300 | describe('should', () => { 301 | it('exported', () => { 302 | expect(one).toEqual(1) 303 | }) 304 | }) 305 | ", 306 | "cursor": 54, 307 | "type": "removal", 308 | }, 309 | { 310 | "content": " 311 | import { describe, expect, it } from 'vitest' 312 | import 313 | 314 | describe('should', () => { 315 | it('exported', () => { 316 | expect(one).toEqual(1) 317 | }) 318 | }) 319 | ", 320 | "cursor": 53, 321 | "type": "removal", 322 | }, 323 | { 324 | "content": " 325 | import { describe, expect, it } from 'vitest' 326 | impor 327 | 328 | describe('should', () => { 329 | it('exported', () => { 330 | expect(one).toEqual(1) 331 | }) 332 | }) 333 | ", 334 | "cursor": 52, 335 | "type": "removal", 336 | }, 337 | { 338 | "content": " 339 | import { describe, expect, it } from 'vitest' 340 | impo 341 | 342 | describe('should', () => { 343 | it('exported', () => { 344 | expect(one).toEqual(1) 345 | }) 346 | }) 347 | ", 348 | "cursor": 51, 349 | "type": "removal", 350 | }, 351 | { 352 | "content": " 353 | import { describe, expect, it } from 'vitest' 354 | imp 355 | 356 | describe('should', () => { 357 | it('exported', () => { 358 | expect(one).toEqual(1) 359 | }) 360 | }) 361 | ", 362 | "cursor": 50, 363 | "type": "removal", 364 | }, 365 | { 366 | "content": " 367 | import { describe, expect, it } from 'vitest' 368 | im 369 | 370 | describe('should', () => { 371 | it('exported', () => { 372 | expect(one).toEqual(1) 373 | }) 374 | }) 375 | ", 376 | "cursor": 49, 377 | "type": "removal", 378 | }, 379 | { 380 | "content": " 381 | import { describe, expect, it } from 'vitest' 382 | i 383 | 384 | describe('should', () => { 385 | it('exported', () => { 386 | expect(one).toEqual(1) 387 | }) 388 | }) 389 | ", 390 | "cursor": 48, 391 | "type": "removal", 392 | }, 393 | { 394 | "content": " 395 | import { describe, expect, it } from 'vitest' 396 | 397 | 398 | describe('should', () => { 399 | it('exported', () => { 400 | expect(one).toEqual(1) 401 | }) 402 | }) 403 | ", 404 | "cursor": 47, 405 | "type": "removal", 406 | }, 407 | { 408 | "content": " 409 | import { describe, expect, it } from 'vitest' 410 | 411 | describe('should', () => { 412 | it('exported', () => { 413 | expect(one).toEqual(1) 414 | }) 415 | }) 416 | ", 417 | "cursor": 46, 418 | "type": "removal", 419 | }, 420 | { 421 | "content": " 422 | import { describe, expect, it } from 'vitest' 423 | 424 | describe('should', () => { 425 | it('exported', () => { 426 | expect(one).toEqual(1) 427 | }) 428 | }) 429 | ", 430 | "index": 0, 431 | "total": 4, 432 | "type": "patch-finish", 433 | }, 434 | { 435 | "index": 1, 436 | "patch": { 437 | "cursor": 89, 438 | "length": 8, 439 | "type": "removal", 440 | }, 441 | "total": 4, 442 | "type": "patch-start", 443 | }, 444 | { 445 | "content": " 446 | import { describe, expect, it } from 'vitest' 447 | 448 | describe('should', () => { 449 | it('exporte', () => { 450 | expect(one).toEqual(1) 451 | }) 452 | }) 453 | ", 454 | "cursor": 88, 455 | "type": "removal", 456 | }, 457 | { 458 | "content": " 459 | import { describe, expect, it } from 'vitest' 460 | 461 | describe('should', () => { 462 | it('export', () => { 463 | expect(one).toEqual(1) 464 | }) 465 | }) 466 | ", 467 | "cursor": 87, 468 | "type": "removal", 469 | }, 470 | { 471 | "content": " 472 | import { describe, expect, it } from 'vitest' 473 | 474 | describe('should', () => { 475 | it('expor', () => { 476 | expect(one).toEqual(1) 477 | }) 478 | }) 479 | ", 480 | "cursor": 86, 481 | "type": "removal", 482 | }, 483 | { 484 | "content": " 485 | import { describe, expect, it } from 'vitest' 486 | 487 | describe('should', () => { 488 | it('expo', () => { 489 | expect(one).toEqual(1) 490 | }) 491 | }) 492 | ", 493 | "cursor": 85, 494 | "type": "removal", 495 | }, 496 | { 497 | "content": " 498 | import { describe, expect, it } from 'vitest' 499 | 500 | describe('should', () => { 501 | it('exp', () => { 502 | expect(one).toEqual(1) 503 | }) 504 | }) 505 | ", 506 | "cursor": 84, 507 | "type": "removal", 508 | }, 509 | { 510 | "content": " 511 | import { describe, expect, it } from 'vitest' 512 | 513 | describe('should', () => { 514 | it('ex', () => { 515 | expect(one).toEqual(1) 516 | }) 517 | }) 518 | ", 519 | "cursor": 83, 520 | "type": "removal", 521 | }, 522 | { 523 | "content": " 524 | import { describe, expect, it } from 'vitest' 525 | 526 | describe('should', () => { 527 | it('e', () => { 528 | expect(one).toEqual(1) 529 | }) 530 | }) 531 | ", 532 | "cursor": 82, 533 | "type": "removal", 534 | }, 535 | { 536 | "content": " 537 | import { describe, expect, it } from 'vitest' 538 | 539 | describe('should', () => { 540 | it('', () => { 541 | expect(one).toEqual(1) 542 | }) 543 | }) 544 | ", 545 | "cursor": 81, 546 | "type": "removal", 547 | }, 548 | { 549 | "content": " 550 | import { describe, expect, it } from 'vitest' 551 | 552 | describe('should', () => { 553 | it('', () => { 554 | expect(one).toEqual(1) 555 | }) 556 | }) 557 | ", 558 | "index": 1, 559 | "total": 4, 560 | "type": "patch-finish", 561 | }, 562 | { 563 | "index": 2, 564 | "patch": { 565 | "content": "one", 566 | "cursor": 81, 567 | "type": "insert", 568 | }, 569 | "total": 4, 570 | "type": "patch-start", 571 | }, 572 | { 573 | "char": "o", 574 | "content": " 575 | import { describe, expect, it } from 'vitest' 576 | 577 | describe('should', () => { 578 | it('o', () => { 579 | expect(one).toEqual(1) 580 | }) 581 | }) 582 | ", 583 | "cursor": 82, 584 | "type": "insert", 585 | }, 586 | { 587 | "char": "n", 588 | "content": " 589 | import { describe, expect, it } from 'vitest' 590 | 591 | describe('should', () => { 592 | it('on', () => { 593 | expect(one).toEqual(1) 594 | }) 595 | }) 596 | ", 597 | "cursor": 83, 598 | "type": "insert", 599 | }, 600 | { 601 | "char": "e", 602 | "content": " 603 | import { describe, expect, it } from 'vitest' 604 | 605 | describe('should', () => { 606 | it('one', () => { 607 | expect(one).toEqual(1) 608 | }) 609 | }) 610 | ", 611 | "cursor": 84, 612 | "type": "insert", 613 | }, 614 | { 615 | "content": " 616 | import { describe, expect, it } from 'vitest' 617 | 618 | describe('should', () => { 619 | it('one', () => { 620 | expect(one).toEqual(1) 621 | }) 622 | }) 623 | ", 624 | "index": 2, 625 | "total": 4, 626 | "type": "patch-finish", 627 | }, 628 | { 629 | "index": 3, 630 | "patch": { 631 | "content": " expect(two).toEqual(2) 632 | ", 633 | "cursor": 122, 634 | "type": "insert", 635 | }, 636 | "total": 4, 637 | "type": "patch-start", 638 | }, 639 | { 640 | "char": " 641 | ", 642 | "content": " 643 | import { describe, expect, it } from 'vitest' 644 | 645 | describe('should', () => { 646 | it('one', () => { 647 | expect(one).toEqual(1) 648 | 649 | }) 650 | }) 651 | ", 652 | "cursor": 123, 653 | "type": "insert", 654 | }, 655 | { 656 | "char": " ", 657 | "content": " 658 | import { describe, expect, it } from 'vitest' 659 | 660 | describe('should', () => { 661 | it('one', () => { 662 | expect(one).toEqual(1) 663 | 664 | }) 665 | }) 666 | ", 667 | "cursor": 123, 668 | "type": "insert", 669 | }, 670 | { 671 | "char": " ", 672 | "content": " 673 | import { describe, expect, it } from 'vitest' 674 | 675 | describe('should', () => { 676 | it('one', () => { 677 | expect(one).toEqual(1) 678 | 679 | }) 680 | }) 681 | ", 682 | "cursor": 124, 683 | "type": "insert", 684 | }, 685 | { 686 | "char": " ", 687 | "content": " 688 | import { describe, expect, it } from 'vitest' 689 | 690 | describe('should', () => { 691 | it('one', () => { 692 | expect(one).toEqual(1) 693 | 694 | }) 695 | }) 696 | ", 697 | "cursor": 125, 698 | "type": "insert", 699 | }, 700 | { 701 | "char": " ", 702 | "content": " 703 | import { describe, expect, it } from 'vitest' 704 | 705 | describe('should', () => { 706 | it('one', () => { 707 | expect(one).toEqual(1) 708 | 709 | }) 710 | }) 711 | ", 712 | "cursor": 126, 713 | "type": "insert", 714 | }, 715 | { 716 | "char": "e", 717 | "content": " 718 | import { describe, expect, it } from 'vitest' 719 | 720 | describe('should', () => { 721 | it('one', () => { 722 | expect(one).toEqual(1) 723 | e 724 | }) 725 | }) 726 | ", 727 | "cursor": 127, 728 | "type": "insert", 729 | }, 730 | { 731 | "char": "x", 732 | "content": " 733 | import { describe, expect, it } from 'vitest' 734 | 735 | describe('should', () => { 736 | it('one', () => { 737 | expect(one).toEqual(1) 738 | ex 739 | }) 740 | }) 741 | ", 742 | "cursor": 128, 743 | "type": "insert", 744 | }, 745 | { 746 | "char": "p", 747 | "content": " 748 | import { describe, expect, it } from 'vitest' 749 | 750 | describe('should', () => { 751 | it('one', () => { 752 | expect(one).toEqual(1) 753 | exp 754 | }) 755 | }) 756 | ", 757 | "cursor": 129, 758 | "type": "insert", 759 | }, 760 | { 761 | "char": "e", 762 | "content": " 763 | import { describe, expect, it } from 'vitest' 764 | 765 | describe('should', () => { 766 | it('one', () => { 767 | expect(one).toEqual(1) 768 | expe 769 | }) 770 | }) 771 | ", 772 | "cursor": 130, 773 | "type": "insert", 774 | }, 775 | { 776 | "char": "c", 777 | "content": " 778 | import { describe, expect, it } from 'vitest' 779 | 780 | describe('should', () => { 781 | it('one', () => { 782 | expect(one).toEqual(1) 783 | expec 784 | }) 785 | }) 786 | ", 787 | "cursor": 131, 788 | "type": "insert", 789 | }, 790 | { 791 | "char": "t", 792 | "content": " 793 | import { describe, expect, it } from 'vitest' 794 | 795 | describe('should', () => { 796 | it('one', () => { 797 | expect(one).toEqual(1) 798 | expect 799 | }) 800 | }) 801 | ", 802 | "cursor": 132, 803 | "type": "insert", 804 | }, 805 | { 806 | "char": "(", 807 | "content": " 808 | import { describe, expect, it } from 'vitest' 809 | 810 | describe('should', () => { 811 | it('one', () => { 812 | expect(one).toEqual(1) 813 | expect( 814 | }) 815 | }) 816 | ", 817 | "cursor": 133, 818 | "type": "insert", 819 | }, 820 | { 821 | "char": ")", 822 | "content": " 823 | import { describe, expect, it } from 'vitest' 824 | 825 | describe('should', () => { 826 | it('one', () => { 827 | expect(one).toEqual(1) 828 | expect() 829 | }) 830 | }) 831 | ", 832 | "cursor": 134, 833 | "type": "insert", 834 | }, 835 | { 836 | "char": "t", 837 | "content": " 838 | import { describe, expect, it } from 'vitest' 839 | 840 | describe('should', () => { 841 | it('one', () => { 842 | expect(one).toEqual(1) 843 | expect(t) 844 | }) 845 | }) 846 | ", 847 | "cursor": 134, 848 | "type": "insert", 849 | }, 850 | { 851 | "char": "w", 852 | "content": " 853 | import { describe, expect, it } from 'vitest' 854 | 855 | describe('should', () => { 856 | it('one', () => { 857 | expect(one).toEqual(1) 858 | expect(tw) 859 | }) 860 | }) 861 | ", 862 | "cursor": 135, 863 | "type": "insert", 864 | }, 865 | { 866 | "char": "o", 867 | "content": " 868 | import { describe, expect, it } from 'vitest' 869 | 870 | describe('should', () => { 871 | it('one', () => { 872 | expect(one).toEqual(1) 873 | expect(two) 874 | }) 875 | }) 876 | ", 877 | "cursor": 136, 878 | "type": "insert", 879 | }, 880 | { 881 | "char": ".", 882 | "content": " 883 | import { describe, expect, it } from 'vitest' 884 | 885 | describe('should', () => { 886 | it('one', () => { 887 | expect(one).toEqual(1) 888 | expect(two). 889 | }) 890 | }) 891 | ", 892 | "cursor": 138, 893 | "type": "insert", 894 | }, 895 | { 896 | "char": "t", 897 | "content": " 898 | import { describe, expect, it } from 'vitest' 899 | 900 | describe('should', () => { 901 | it('one', () => { 902 | expect(one).toEqual(1) 903 | expect(two).t 904 | }) 905 | }) 906 | ", 907 | "cursor": 139, 908 | "type": "insert", 909 | }, 910 | { 911 | "char": "o", 912 | "content": " 913 | import { describe, expect, it } from 'vitest' 914 | 915 | describe('should', () => { 916 | it('one', () => { 917 | expect(one).toEqual(1) 918 | expect(two).to 919 | }) 920 | }) 921 | ", 922 | "cursor": 140, 923 | "type": "insert", 924 | }, 925 | { 926 | "char": "E", 927 | "content": " 928 | import { describe, expect, it } from 'vitest' 929 | 930 | describe('should', () => { 931 | it('one', () => { 932 | expect(one).toEqual(1) 933 | expect(two).toE 934 | }) 935 | }) 936 | ", 937 | "cursor": 141, 938 | "type": "insert", 939 | }, 940 | { 941 | "char": "q", 942 | "content": " 943 | import { describe, expect, it } from 'vitest' 944 | 945 | describe('should', () => { 946 | it('one', () => { 947 | expect(one).toEqual(1) 948 | expect(two).toEq 949 | }) 950 | }) 951 | ", 952 | "cursor": 142, 953 | "type": "insert", 954 | }, 955 | { 956 | "char": "u", 957 | "content": " 958 | import { describe, expect, it } from 'vitest' 959 | 960 | describe('should', () => { 961 | it('one', () => { 962 | expect(one).toEqual(1) 963 | expect(two).toEqu 964 | }) 965 | }) 966 | ", 967 | "cursor": 143, 968 | "type": "insert", 969 | }, 970 | { 971 | "char": "a", 972 | "content": " 973 | import { describe, expect, it } from 'vitest' 974 | 975 | describe('should', () => { 976 | it('one', () => { 977 | expect(one).toEqual(1) 978 | expect(two).toEqua 979 | }) 980 | }) 981 | ", 982 | "cursor": 144, 983 | "type": "insert", 984 | }, 985 | { 986 | "char": "l", 987 | "content": " 988 | import { describe, expect, it } from 'vitest' 989 | 990 | describe('should', () => { 991 | it('one', () => { 992 | expect(one).toEqual(1) 993 | expect(two).toEqual 994 | }) 995 | }) 996 | ", 997 | "cursor": 145, 998 | "type": "insert", 999 | }, 1000 | { 1001 | "char": "(", 1002 | "content": " 1003 | import { describe, expect, it } from 'vitest' 1004 | 1005 | describe('should', () => { 1006 | it('one', () => { 1007 | expect(one).toEqual(1) 1008 | expect(two).toEqual( 1009 | }) 1010 | }) 1011 | ", 1012 | "cursor": 146, 1013 | "type": "insert", 1014 | }, 1015 | { 1016 | "char": ")", 1017 | "content": " 1018 | import { describe, expect, it } from 'vitest' 1019 | 1020 | describe('should', () => { 1021 | it('one', () => { 1022 | expect(one).toEqual(1) 1023 | expect(two).toEqual() 1024 | }) 1025 | }) 1026 | ", 1027 | "cursor": 147, 1028 | "type": "insert", 1029 | }, 1030 | { 1031 | "char": "2", 1032 | "content": " 1033 | import { describe, expect, it } from 'vitest' 1034 | 1035 | describe('should', () => { 1036 | it('one', () => { 1037 | expect(one).toEqual(1) 1038 | expect(two).toEqual(2) 1039 | }) 1040 | }) 1041 | ", 1042 | "cursor": 147, 1043 | "type": "insert", 1044 | }, 1045 | { 1046 | "content": " 1047 | import { describe, expect, it } from 'vitest' 1048 | 1049 | describe('should', () => { 1050 | it('one', () => { 1051 | expect(one).toEqual(1) 1052 | expect(two).toEqual(2) 1053 | }) 1054 | }) 1055 | ", 1056 | "index": 3, 1057 | "total": 4, 1058 | "type": "patch-finish", 1059 | }, 1060 | ] 1061 | `; 1062 | -------------------------------------------------------------------------------- /packages/core/test/fixture.ts: -------------------------------------------------------------------------------- 1 | export const input = ` 2 | import { describe, expect, it } from 'vitest' 3 | import { one } from '../src' 4 | 5 | describe('should', () => { 6 | it('exported', () => { 7 | expect(one).toEqual(1) 8 | }) 9 | }) 10 | ` 11 | 12 | export const output = ` 13 | import { describe, expect, it } from 'vitest' 14 | 15 | describe('should', () => { 16 | it('one', () => { 17 | expect(one).toEqual(1) 18 | expect(two).toEqual(2) 19 | }) 20 | }) 21 | ` 22 | -------------------------------------------------------------------------------- /packages/core/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import type { Patch } from '../src' 3 | import { calculatePatch, diff, patchSteps } from '../src' 4 | import { input, output } from './fixture' 5 | 6 | export function applyPatches(input: string, patches: Patch[]) { 7 | let output = input 8 | for (const patch of patchSteps(input, patches)) { 9 | if (patch.type === 'patch-finish') 10 | output = patch.content 11 | } 12 | return output 13 | } 14 | 15 | describe('should', () => { 16 | it('exported', () => { 17 | const delta = diff(input, output) 18 | expect(delta).toMatchSnapshot('delta') 19 | const patches = calculatePatch(delta) 20 | expect(patches).toMatchSnapshot('patches') 21 | const applied = applyPatches(input, patches) 22 | expect(applied).toMatchSnapshot('output') 23 | expect(applied).toEqual(output) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /packages/core/test/parse.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseSnapshots } from '../src/state/parse' 3 | import fixture from '../../../examples/main.js.retypewriter?raw' 4 | import { Snapshots } from '../src' 5 | 6 | it('works', () => { 7 | expect(parseSnapshots(fixture)).toMatchSnapshot() 8 | }) 9 | 10 | it('serializable', () => { 11 | const snapshots = Snapshots.fromString(fixture) 12 | const stringified = snapshots.toString() 13 | expect(stringified).toEqual(fixture) 14 | }) 15 | -------------------------------------------------------------------------------- /packages/core/test/slicing.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { applySlice, sliceInput } from '../src/animation/slicing' 3 | 4 | it('works', () => { 5 | const fixture = ` 6 | import { describe, expect, it } from 'vitest' 7 | 8 | describe('should', () => { 9 | it('one', () => { 10 | expect(one).toEqual(1) 11 | }) 12 | })\n` 13 | 14 | const slices = sliceInput(fixture) 15 | 16 | expect(applySlice(slices.slice(0, 4))).toMatchInlineSnapshot(` 17 | " 18 | import { } 19 | " 20 | `) 21 | expect(applySlice(slices.slice(0, 5))).toMatchInlineSnapshot(` 22 | " 23 | import { describe, expect, it } 24 | " 25 | `) 26 | expect(applySlice(slices.slice(0, 6))).toMatchInlineSnapshot(` 27 | " 28 | import { describe, expect, it } from 29 | " 30 | `) 31 | expect(slices.slice(0, 4)).toMatchInlineSnapshot(` 32 | [ 33 | { 34 | "content": " 35 | ", 36 | "cursor": 0, 37 | "order": -1, 38 | }, 39 | { 40 | "content": " 41 | import", 42 | "cursor": 0, 43 | "order": 0, 44 | }, 45 | { 46 | "content": " { ", 47 | "cursor": 7, 48 | "order": 1, 49 | }, 50 | { 51 | "content": " } ", 52 | "cursor": 10, 53 | "order": 1, 54 | }, 55 | ] 56 | `) 57 | expect(applySlice(slices)).toEqual(fixture) 58 | }) 59 | -------------------------------------------------------------------------------- /packages/core/test/snaps.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import type { Snapshot } from '../src' 3 | import { Snapshots } from '../src' 4 | 5 | it('snaps', () => { 6 | const data: Snapshot[] = [ 7 | { content: '' }, 8 | { content: 'import {} from "vue"', options: { pause: true } }, 9 | { content: 'import { createApp } from "vue"\n\nconst app = createApp()\n' }, 10 | ] 11 | 12 | const snaps = new Snapshots(...data) 13 | const serialized = snaps.toString() 14 | 15 | expect(serialized).toMatchSnapshot() 16 | 17 | const deserialized = Snapshots.fromString(serialized) 18 | 19 | expect(data).toEqual([...deserialized]) 20 | }) 21 | -------------------------------------------------------------------------------- /packages/core/test/steps.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { stepsTo } from '../src' 3 | import { input, output } from './fixture' 4 | 5 | it('steps', () => { 6 | const steps = stepsTo(input, output) 7 | 8 | expect([...steps]).toMatchSnapshot('steps') 9 | }) 10 | -------------------------------------------------------------------------------- /packages/vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | .github/** 2 | .vscode/** 3 | .vscode-test/** 4 | scripts/** 5 | node_modules/** 6 | -------------------------------------------------------------------------------- /packages/vscode/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Anthony Fu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/vscode/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | retypewriter 4 |

5 | 6 |

7 | reTypewriter 8 |

9 | 10 |

11 | Replay the steps of your changes at ease. 12 |
A diff based typing simulator. 13 |
14 |
15 | NPM version 16 |

17 | 18 | ## Screenshots 19 | 20 | ![](https://user-images.githubusercontent.com/11247099/194193067-209e01e1-107e-4d10-b73c-d37adc0e64d1.png) 21 | 22 | ## Usage 23 | 24 | 1. Open or create a new file you want to record for typing. 25 | 2. Press the `➕` button on the top right tools bar to record a snapshot. 26 | 3. Make some changes to the file. 27 | 4. Press the `➕` button again to take another snapshot. 28 | 5. Repeat steps 3. and 4. until you are finished. 29 | 6. Press the `▶️` button to replay the typing. 30 | 31 | After you create a snapshot, you will see a `.retypewriter` file created alongside. It stores the snapshots you created. You can edit it manually as wish. 32 | 33 | ## Sponsors 34 | 35 |

36 | 37 | 38 | 39 |

40 | 41 | ## License 42 | 43 | [MIT](./LICENSE) License © 2022 [Anthony Fu](https://github.com/antfu) 44 | -------------------------------------------------------------------------------- /packages/vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "publisher": "antfu", 3 | "name": "retypewriter", 4 | "displayName": "reTypewriter", 5 | "version": "0.1.6", 6 | "private": true, 7 | "description": "Reply the steps of your changes at ease.", 8 | "author": "Anthony Fu ", 9 | "license": "MIT", 10 | "funding": "https://github.com/sponsors/antfu", 11 | "homepage": "https://github.com/antfu/retypewriter#readme", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/antfu/retypewriter", 15 | "directory": "packages/core" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/antfu/retypewriter/issues" 19 | }, 20 | "categories": [ 21 | "Other" 22 | ], 23 | "main": "./dist/index.js", 24 | "icon": "res/icon.png", 25 | "engines": { 26 | "vscode": "^1.80.0" 27 | }, 28 | "activationEvents": [ 29 | "onStartupFinished" 30 | ], 31 | "contributes": { 32 | "languages": [ 33 | { 34 | "id": "retypewriter", 35 | "aliases": [ 36 | "reTypewriter" 37 | ], 38 | "extensions": [ 39 | ".retypewriter" 40 | ] 41 | }, 42 | { 43 | "id": "retypewriter-js", 44 | "aliases": [ 45 | "reTypewriter JavaScript" 46 | ], 47 | "extensions": [ 48 | ".js.retypewriter" 49 | ] 50 | }, 51 | { 52 | "id": "retypewriter-jsx", 53 | "aliases": [ 54 | "reTypewriter JSX" 55 | ], 56 | "extensions": [ 57 | ".jsx.retypewriter" 58 | ] 59 | }, 60 | { 61 | "id": "retypewriter-ts", 62 | "aliases": [ 63 | "reTypewriter TypeScript" 64 | ], 65 | "extensions": [ 66 | ".ts.retypewriter" 67 | ] 68 | }, 69 | { 70 | "id": "retypewriter-tsx", 71 | "aliases": [ 72 | "reTypewriter TSX" 73 | ], 74 | "extensions": [ 75 | ".tsx.retypewriter" 76 | ] 77 | }, 78 | { 79 | "id": "retypewriter-vue", 80 | "aliases": [ 81 | "reTypewriter Vue" 82 | ], 83 | "extensions": [ 84 | ".vue.retypewriter" 85 | ] 86 | }, 87 | { 88 | "id": "retypewriter-html", 89 | "aliases": [ 90 | "reTypewriter HTML" 91 | ], 92 | "extensions": [ 93 | ".html.retypewriter" 94 | ] 95 | } 96 | ], 97 | "grammars": [ 98 | { 99 | "language": "retypewriter", 100 | "scopeName": "source.retypewriter", 101 | "path": "./syntaxes/default.json" 102 | }, 103 | { 104 | "language": "retypewriter-js", 105 | "scopeName": "source.retypewriter-js", 106 | "path": "./syntaxes/javascript.json" 107 | }, 108 | { 109 | "language": "retypewriter-jsx", 110 | "scopeName": "source.retypewriter-jsx", 111 | "path": "./syntaxes/javascriptreact.json" 112 | }, 113 | { 114 | "language": "retypewriter-ts", 115 | "scopeName": "source.retypewriter-ts", 116 | "path": "./syntaxes/typescript.json" 117 | }, 118 | { 119 | "language": "retypewriter-tsx", 120 | "scopeName": "source.retypewriter-tsx", 121 | "path": "./syntaxes/typescriptreact.json" 122 | }, 123 | { 124 | "language": "retypewriter-vue", 125 | "scopeName": "source.retypewriter-vue", 126 | "path": "./syntaxes/vue.json" 127 | }, 128 | { 129 | "language": "retypewriter-html", 130 | "scopeName": "source.retypewriter-html", 131 | "path": "./syntaxes/html.json" 132 | } 133 | ], 134 | "commands": [ 135 | { 136 | "category": "reTypewriter", 137 | "command": "retypewriter.snap", 138 | "title": "Take snapshot of current file", 139 | "icon": "$(diff-added)" 140 | }, 141 | { 142 | "category": "reTypewriter", 143 | "command": "retypewriter.play", 144 | "title": "Start playing typewritter", 145 | "icon": "$(play)" 146 | }, 147 | { 148 | "category": "reTypewriter", 149 | "command": "retypewriter.abort", 150 | "title": "Stop playing typewritter", 151 | "icon": "$(stop)" 152 | } 153 | ], 154 | "menus": { 155 | "editor/title": [ 156 | { 157 | "command": "retypewriter.snap", 158 | "group": "navigation@1170", 159 | "when": "editorLangId != retypewriter && reTypewriter.isNotPlaying" 160 | }, 161 | { 162 | "command": "retypewriter.play", 163 | "group": "navigation@1170", 164 | "when": "reTypewriter.isNotPlaying" 165 | }, 166 | { 167 | "command": "retypewriter.abort", 168 | "group": "navigation@1170", 169 | "when": "reTypewriter.isPlaying" 170 | } 171 | ] 172 | } 173 | }, 174 | "scripts": { 175 | "prepare": "esno scripts/generateSyntaxes.ts", 176 | "build": "tsup src/index.ts --external vscode", 177 | "dev": "nr build --watch", 178 | "vscode:prepublish": "nr build", 179 | "publish": "vsce publish --no-dependencies", 180 | "pack": "vsce package --no-dependencies" 181 | }, 182 | "devDependencies": { 183 | "@antfu/utils": "0.7.5", 184 | "@types/vscode": "^1.80.0", 185 | "tsup": "^8.0.2" 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /packages/vscode/res/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu/retypewriter/eecd2da94d15a86e8155f82ea9c67aa2c651acc6/packages/vscode/res/icon.png -------------------------------------------------------------------------------- /packages/vscode/scripts/generateSyntaxes.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs' 2 | import { join, resolve } from 'node:path' 3 | import { syntaxMaps } from '../src/syntaxes' 4 | 5 | const root = resolve(__dirname, '..') 6 | 7 | export async function run() { 8 | const pkg = JSON.parse(await fs.readFile(join(root, 'package.json'), 'utf8')) 9 | pkg.contributes.grammars = [] 10 | pkg.contributes.languages = [] 11 | for (const [e, langId, langScope, display] of syntaxMaps) { 12 | const language = e ? `retypewriter-${e}` : 'retypewriter' 13 | const scopeName = `source.${language}` 14 | const filename = `${langId || 'default'}.json` 15 | const content = generateSyntax(scopeName, langScope) 16 | await fs.writeFile(join(root, 'syntaxes', filename), `${JSON.stringify(content, null, 2)}\n`) 17 | pkg.contributes.grammars.push({ 18 | language, 19 | scopeName, 20 | path: `./syntaxes/${filename}`, 21 | }) 22 | pkg.contributes.languages.push({ 23 | id: language, 24 | aliases: [ 25 | `reTypewriter ${display}`.trim(), 26 | ], 27 | extensions: [ 28 | e ? `.${e}.retypewriter` : '.retypewriter', 29 | ], 30 | }) 31 | } 32 | await fs.writeFile(join(root, 'package.json'), `${JSON.stringify(pkg, null, 2)}\n`) 33 | } 34 | 35 | export function generateSyntax(scopeName: string, langScope: string) { 36 | return { 37 | $schema: 'https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json', 38 | scopeName, 39 | patterns: [ 40 | { 41 | include: '#options', 42 | }, 43 | { 44 | include: '#snapshot', 45 | }, 46 | { 47 | include: '#seperators', 48 | }, 49 | { 50 | include: langScope, 51 | }, 52 | ], 53 | repository: { 54 | snapshot: { 55 | name: 'punctuation.separator', 56 | begin: '--[\\d-]{2}----------', 57 | end: '(-----options--|--[\\d-]{2}----------)', 58 | patterns: [ 59 | { 60 | include: langScope, 61 | }, 62 | ], 63 | }, 64 | options: { 65 | name: 'punctuation.separator', 66 | begin: '-----options--', 67 | end: '--[\\d-]{2}----------', 68 | patterns: [ 69 | { 70 | include: 'source.yaml', 71 | }, 72 | ], 73 | }, 74 | seperators: { 75 | name: 'punctuation.separator', 76 | match: '(--[\\d-]{2}----------|-----options--|reTypewriter Snapshots v\\d)', 77 | }, 78 | }, 79 | } 80 | } 81 | 82 | run() 83 | -------------------------------------------------------------------------------- /packages/vscode/src/decoration.ts: -------------------------------------------------------------------------------- 1 | import { DecorationRangeBehavior, Range, ThemeColor, window, workspace } from 'vscode' 2 | import { calculatePatch, diff, parseSnapshots } from '../../core/src' 3 | 4 | const DecorationInserted = window.createTextEditorDecorationType({ 5 | // color: new ThemeColor('terminal.ansiGreen'), 6 | backgroundColor: new ThemeColor('diffEditor.insertedTextBackground'), 7 | rangeBehavior: DecorationRangeBehavior.ClosedClosed, 8 | }) 9 | const DecorationRemoved = window.createTextEditorDecorationType({ 10 | // color: new ThemeColor('terminal.ansiRed'), 11 | backgroundColor: new ThemeColor('diffEditor.removedTextBackground'), 12 | rangeBehavior: DecorationRangeBehavior.ClosedClosed, 13 | }) 14 | 15 | export async function updateAnnotation(editor = window.activeTextEditor) { 16 | const doc = editor?.document 17 | if (!doc || !doc.languageId.includes('retypewriter')) 18 | return reset() 19 | 20 | const code = doc.getText() 21 | const snaps = parseSnapshots(code).snapshots 22 | 23 | const removed: Range[] = [] 24 | const inserted: Range[] = [] 25 | 26 | snaps.forEach((snap, index) => { 27 | if (index === 0) 28 | return 29 | const prev = snaps[index - 1] 30 | 31 | calculatePatch(diff(prev.body, snap.body)) 32 | .forEach((patch) => { 33 | if (patch.type === 'insert') { 34 | inserted.push(new Range( 35 | doc.positionAt(snap.bodyStart + patch.cursor), 36 | doc.positionAt(snap.bodyStart + patch.cursor + patch.content.length), 37 | )) 38 | } 39 | }) 40 | 41 | calculatePatch(diff(snap.body, prev.body)) 42 | .forEach((patch) => { 43 | if (patch.type === 'insert') { 44 | removed.push(new Range( 45 | doc.positionAt(prev.bodyStart + patch.cursor), 46 | doc.positionAt(prev.bodyStart + patch.cursor + patch.content.length), 47 | )) 48 | } 49 | }) 50 | }) 51 | 52 | function reset() { 53 | editor?.setDecorations(DecorationInserted, []) 54 | editor?.setDecorations(DecorationRemoved, []) 55 | } 56 | editor.setDecorations(DecorationInserted, inserted) 57 | editor.setDecorations(DecorationRemoved, removed) 58 | } 59 | 60 | export function registerAnnonations() { 61 | const throttledUpdateAnnotation = throttle(updateAnnotation, 200) 62 | 63 | updateAnnotation() 64 | 65 | return [ 66 | window.onDidChangeActiveTextEditor(updateAnnotation), 67 | workspace.onDidChangeTextDocument((e) => { 68 | if (e.document === window.activeTextEditor?.document) 69 | throttledUpdateAnnotation() 70 | }), 71 | ] 72 | } 73 | 74 | export function throttle any)>(func: T, timeFrame: number): T { 75 | let lastTime = 0 76 | let timer: any 77 | return function () { 78 | const now = Date.now() 79 | clearTimeout(timer) 80 | if (now - lastTime >= timeFrame) { 81 | lastTime = now 82 | return func() 83 | } 84 | else { 85 | timer = setTimeout(func, timeFrame) 86 | } 87 | } as T 88 | } 89 | -------------------------------------------------------------------------------- /packages/vscode/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { ExtensionContext } from 'vscode' 2 | import { commands, languages, window, workspace } from 'vscode' 3 | import { SNAP_EXT } from '../../core/src' 4 | import { registerAnnonations } from './decoration' 5 | import { Lens } from './lens' 6 | import { manager } from './manager' 7 | import { duplicate, moveDown, moveUp, remove, reverse } from './manipulate' 8 | import { continuePause, isPlaying, playAbort, playStart, updateContext } from './play' 9 | import { snap } from './record' 10 | import { langageIds } from './syntaxes' 11 | import { reveal } from './utils' 12 | 13 | export function activate(ctx: ExtensionContext) { 14 | const watcher = workspace.createFileSystemWatcher(`**/*\\${SNAP_EXT}`) 15 | 16 | ctx.subscriptions.push( 17 | watcher, 18 | watcher.onDidChange(uri => manager.delete(uri.fsPath.replace(SNAP_EXT, ''))), 19 | watcher.onDidDelete(uri => manager.delete(uri.fsPath.replace(SNAP_EXT, ''))), 20 | watcher.onDidCreate(uri => manager.delete(uri.fsPath.replace(SNAP_EXT, ''))), 21 | 22 | commands.registerCommand('retypewriter.snap', snap), 23 | commands.registerCommand('retypewriter.play', playStart), 24 | commands.registerCommand('retypewriter.abort', playAbort), 25 | commands.registerCommand('retypewriter.continue', continuePause), 26 | commands.registerCommand('retypewriter.snap-move-up', moveUp), 27 | commands.registerCommand('retypewriter.snap-move-down', moveDown), 28 | commands.registerCommand('retypewriter.snap-remove', remove), 29 | commands.registerCommand('retypewriter.snap-reverse', reverse), 30 | commands.registerCommand('retypewriter.snap-duplicate', duplicate), 31 | commands.registerCommand('retypewriter.reveal', reveal), 32 | 33 | languages.registerCodeLensProvider(langageIds, new Lens()), 34 | 35 | ...registerAnnonations(), 36 | ) 37 | 38 | window.onDidChangeActiveTextEditor(() => { 39 | if (isPlaying()) 40 | playAbort() 41 | }) 42 | 43 | updateContext() 44 | } 45 | 46 | export function deactivate() { 47 | 48 | } 49 | -------------------------------------------------------------------------------- /packages/vscode/src/lens.ts: -------------------------------------------------------------------------------- 1 | import type { CodeLensProvider, Event, ProviderResult, TextDocument } from 'vscode' 2 | import { CodeLens, EventEmitter, Range, Uri } from 'vscode' 3 | import { getOriginalFilePath, parseSnapshots } from '../../core/src' 4 | 5 | export class Lens implements CodeLensProvider { 6 | private _onDidChangeCodeLenses: EventEmitter = new EventEmitter() 7 | public readonly onDidChangeCodeLenses: Event = this._onDidChangeCodeLenses.event 8 | 9 | provideCodeLenses(document: TextDocument): ProviderResult { 10 | const { 11 | snapshots, 12 | } = parseSnapshots(document.getText()) 13 | 14 | const startRange = new Range(1, 0, 1, 0) 15 | const head: CodeLens[] = [ 16 | new CodeLens( 17 | startRange, 18 | { 19 | title: '▶ Play', 20 | tooltip: 'Play', 21 | command: 'retypewriter.play', 22 | arguments: [Uri.file(getOriginalFilePath(document.uri.fsPath)!)], 23 | }, 24 | ), 25 | new CodeLens( 26 | startRange, 27 | { 28 | title: 'Go to file', 29 | tooltip: 'Go to file', 30 | command: 'retypewriter.reveal', 31 | arguments: [Uri.file(getOriginalFilePath(document.uri.fsPath)!)], 32 | }, 33 | ), 34 | new CodeLens( 35 | startRange, 36 | { 37 | title: 'Reverse', 38 | tooltip: 'Revese items', 39 | command: 'retypewriter.snap-reverse', 40 | arguments: [document], 41 | }, 42 | ), 43 | ] 44 | 45 | const items = snapshots.flatMap((i, idx) => { 46 | const start = document.positionAt(i.start + 1) 47 | const end = document.positionAt(i.start + i.raw.length - 1) 48 | const range = new Range(start, end) 49 | const lens: CodeLens[] = [] 50 | 51 | if (idx > 0) { 52 | lens.push(new CodeLens(range, { 53 | title: '△', 54 | tooltip: 'Move up', 55 | command: 'retypewriter.snap-move-up', 56 | arguments: [document, idx], 57 | })) 58 | } 59 | if (idx < snapshots.length - 1) { 60 | lens.push(new CodeLens(range, { 61 | title: '▽', 62 | tooltip: 'Move down', 63 | command: 'retypewriter.snap-move-down', 64 | arguments: [document, idx], 65 | })) 66 | } 67 | 68 | // lens.push(new CodeLens(range, { 69 | // title: 'Remove', 70 | // tooltip: 'Remove', 71 | // command: 'retypewriter.snap-remove', 72 | // arguments: [document, idx], 73 | // })) 74 | 75 | lens.push(new CodeLens(range, { 76 | title: '⊕', 77 | tooltip: 'Duplicate', 78 | command: 'retypewriter.snap-duplicate', 79 | arguments: [document, idx], 80 | })) 81 | 82 | return lens 83 | }) 84 | 85 | return [ 86 | ...head, 87 | ...items, 88 | ] 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /packages/vscode/src/manager.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, promises as fs } from 'node:fs' 2 | import { SnapshotManager, Snapshots, getSnapshotPath } from '../../core/src' 3 | 4 | export const manager = new SnapshotManager({ 5 | async ensureFallback(path) { 6 | const filepath = getSnapshotPath(path) 7 | if (existsSync(filepath)) { 8 | const content = await fs.readFile(filepath, 'utf8') 9 | const snap = Snapshots.fromString(content) 10 | return snap 11 | } 12 | }, 13 | }) 14 | 15 | export async function writeSnapshots(path: string, snap: Snapshots) { 16 | const filepath = getSnapshotPath(path) 17 | await fs.writeFile(filepath, snap.toString(), 'utf8') 18 | } 19 | -------------------------------------------------------------------------------- /packages/vscode/src/manipulate.ts: -------------------------------------------------------------------------------- 1 | import { Snapshots, parseSnapshots } from 'retypewriter' 2 | import type { TextDocument, TextEditor } from 'vscode' 3 | import { Range, Selection, window } from 'vscode' 4 | import { updateAnnotation } from './decoration' 5 | import { manager } from './manager' 6 | 7 | export async function manipulateSnapshotsInDocument( 8 | doc: TextDocument, 9 | fn: ((snaps: Snapshots) => Promise | any), 10 | ) { 11 | const raw = doc.getText() 12 | const snaps = Snapshots.fromString(raw) 13 | 14 | await fn(snaps) 15 | 16 | manager.set(doc.uri.fsPath, snaps) 17 | const result = snaps.toString() 18 | let editor: TextEditor | undefined 19 | if (result !== raw) { 20 | editor = await window.showTextDocument(doc, { preview: false, preserveFocus: false }) 21 | await editor.edit((e) => { 22 | e.replace(new Range(0, 0, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY), snaps.toString()) 23 | }) 24 | await updateAnnotation(editor) 25 | } 26 | 27 | return { 28 | snaps, 29 | doc, 30 | editor, 31 | } 32 | } 33 | 34 | export async function move(doc: TextDocument, index: number, delta = 1) { 35 | const to = index + delta 36 | const { editor } = await manipulateSnapshotsInDocument( 37 | doc, 38 | snaps => snaps.move(index, to), 39 | ) 40 | 41 | if (editor) { 42 | // select to highlight the moved block 43 | const target = parseSnapshots(doc.getText()).snapshots[to] 44 | if (target) { 45 | editor.selection = new Selection( 46 | doc.positionAt(target.start + 1), 47 | doc.positionAt(target.end), 48 | ) 49 | } 50 | } 51 | } 52 | 53 | export const moveUp = (doc: TextDocument, index: number) => move(doc, index, -1) 54 | export const moveDown = (doc: TextDocument, index: number) => move(doc, index, 1) 55 | 56 | export function remove(doc: TextDocument, index: number) { 57 | return manipulateSnapshotsInDocument( 58 | doc, 59 | snaps => snaps.remove(index), 60 | ) 61 | } 62 | 63 | export function duplicate(doc: TextDocument, index: number) { 64 | return manipulateSnapshotsInDocument( 65 | doc, 66 | snaps => snaps.duplicate(index), 67 | ) 68 | } 69 | 70 | export function reverse(doc: TextDocument) { 71 | return manipulateSnapshotsInDocument( 72 | doc, 73 | snaps => snaps.reverse(), 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /packages/vscode/src/play.ts: -------------------------------------------------------------------------------- 1 | import type { StatusBarItem, TextDocument, Uri } from 'vscode' 2 | import { CancellationTokenSource, Range, Selection, StatusBarAlignment, ThemeColor, commands, window } from 'vscode' 3 | import type { ControlledPromise } from '@antfu/utils' 4 | import { createControlledPromise } from '@antfu/utils' 5 | import { manager } from './manager' 6 | import { resolveDoc } from './utils' 7 | 8 | let token: CancellationTokenSource | undefined 9 | let pausePromise: ControlledPromise | undefined 10 | let status: StatusBarItem | undefined 11 | 12 | export async function playAbort(prompts = false) { 13 | if (token) { 14 | if (prompts && await window.showInformationMessage('Abort playing?', 'Yes', 'Cancel') !== 'Yes') 15 | return 16 | continuePause() 17 | token.cancel() 18 | token = undefined 19 | if (status) { 20 | status.dispose() 21 | status = undefined 22 | } 23 | updateContext() 24 | } 25 | } 26 | 27 | export function updateContext() { 28 | const playing = isPlaying() 29 | commands.executeCommand('setContext', 'reTypewriter.isPlaying', playing) 30 | commands.executeCommand('setContext', 'reTypewriter.isNotPlaying', !playing) 31 | } 32 | 33 | export function isPlaying() { 34 | return token !== undefined 35 | } 36 | 37 | export async function continuePause() { 38 | pausePromise?.resolve() 39 | } 40 | 41 | export async function playStart(arg?: TextDocument | Uri) { 42 | const { doc, editor } = await resolveDoc(arg) 43 | if (!doc || !editor) 44 | return 45 | 46 | if (token) { 47 | window.showErrorMessage('reTypewriter: Already playing') 48 | return 49 | } 50 | const path = doc.uri.fsPath 51 | 52 | const snaps = await manager.ensure(path) 53 | if (!snaps?.length) { 54 | window.showErrorMessage('reTypewriter: No snapshots found') 55 | return 56 | } 57 | 58 | const lastSnap = snaps[snaps.length - 1] 59 | if (lastSnap.content !== doc.getText()) { 60 | const take = 'Take Snapshot' 61 | const discard = 'Discard' 62 | const cancel = 'Cancel' 63 | 64 | const result = await window.showInformationMessage( 65 | 'The current document has been modified since last snapshot. Do you want take another snapshot?', 66 | { modal: true }, 67 | take, 68 | discard, 69 | cancel, 70 | ) 71 | if (!result || result === cancel) 72 | return 73 | if (result === take) 74 | await commands.executeCommand('retypewriter.snap') 75 | } 76 | 77 | const setCursor = (index: number) => { 78 | const pos = doc.positionAt(index) 79 | editor.selection = new Selection(pos, pos) 80 | } 81 | 82 | const total = snaps.length - 1 83 | let message = `Step 0 of ${total}` 84 | const spin = '$(loading~spin) ' 85 | 86 | token = new CancellationTokenSource() 87 | status = window.createStatusBarItem(StatusBarAlignment.Left, Number.POSITIVE_INFINITY) 88 | status.show() 89 | updateContext() 90 | 91 | let lastProgress = 0 92 | function updateProgress(index = lastProgress) { 93 | lastProgress = index 94 | message = `Step ${index} of ${total}` 95 | if (status) { 96 | status.color = new ThemeColor('terminal.ansiBrightGreen') 97 | status.text = spin + message 98 | status.backgroundColor = undefined 99 | status.command = { 100 | title: 'Abort', 101 | command: 'retypewriter.abort', 102 | arguments: [true], 103 | } 104 | } 105 | } 106 | async function pause() { 107 | pausePromise = createControlledPromise() 108 | if (status) { 109 | status.backgroundColor = new ThemeColor('statusBarItem.warningBackground') 110 | status.color = undefined 111 | status.text = '$(debug-pause) Paused, press any key to continue' 112 | status.command = { 113 | title: 'Continue', 114 | command: 'retypewriter.continue', 115 | } 116 | } 117 | const command = commands.registerCommand('type', ({ text } = {}) => { 118 | if (!text) 119 | return 120 | continuePause() 121 | command.dispose() 122 | }) 123 | await pausePromise 124 | 125 | pausePromise = undefined 126 | updateProgress() 127 | command.dispose() 128 | } 129 | 130 | updateProgress(0) 131 | 132 | for await (const snap of snaps.typewriter()) { 133 | if (!token) 134 | break 135 | if (token.token.isCancellationRequested) 136 | break 137 | switch (snap.type) { 138 | case 'snap-start': 139 | updateProgress(snap.index) 140 | break 141 | 142 | case 'patch-finish': 143 | updateProgress(snap.index) 144 | break 145 | 146 | case 'init': 147 | await editor.edit(edit => edit.replace(new Range(0, 0, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY), snap.content)) 148 | break 149 | 150 | case 'paste': 151 | await editor.edit(edit => edit.insert( 152 | doc.positionAt(snap.cursor - snap.content.length), 153 | snap.content, 154 | )) 155 | setCursor(snap.cursor) 156 | break 157 | 158 | case 'insert': 159 | await editor.edit(edit => edit.insert( 160 | doc.positionAt(snap.cursor - 1), 161 | snap.char, 162 | )) 163 | setCursor(snap.cursor) 164 | break 165 | 166 | case 'removal': 167 | await editor.edit(edit => edit.delete(new Range( 168 | doc.positionAt(snap.cursor), 169 | doc.positionAt(snap.cursor + 1), 170 | ))) 171 | setCursor(snap.cursor) 172 | break 173 | 174 | case 'action-pause': 175 | await pause() 176 | break 177 | } 178 | } 179 | 180 | status?.dispose() 181 | token = undefined 182 | updateContext() 183 | } 184 | -------------------------------------------------------------------------------- /packages/vscode/src/record.ts: -------------------------------------------------------------------------------- 1 | import type { TextDocument, Uri } from 'vscode' 2 | import { window } from 'vscode' 3 | import { SNAP_EXT } from '../../core/src' 4 | import { manager, writeSnapshots } from './manager' 5 | import { resolveDoc } from './utils' 6 | 7 | export async function snap(arg?: TextDocument | Uri) { 8 | const { doc, editor } = await resolveDoc(arg) 9 | if (!doc || !editor) 10 | return 11 | 12 | const path = doc.uri.fsPath 13 | if (path.endsWith(SNAP_EXT)) 14 | return 15 | 16 | const snaps = await manager.ensure(path) 17 | 18 | snaps.push({ content: doc.getText() }) 19 | await writeSnapshots(path, snaps) 20 | 21 | window.showInformationMessage(`reTypewriter: Snapshot added (${snaps.length})`) 22 | } 23 | -------------------------------------------------------------------------------- /packages/vscode/src/syntaxes.ts: -------------------------------------------------------------------------------- 1 | export const syntaxMaps: [string, string, string, string][] = [ 2 | ['', '', 'source.text', ''], 3 | ['js', 'javascript', 'source.js', 'JavaScript'], 4 | ['jsx', 'javascriptreact', 'source.jsx', 'JSX'], 5 | ['ts', 'typescript', 'source.ts', 'TypeScript'], 6 | ['tsx', 'typescriptreact', 'source.jsx', 'TSX'], 7 | ['vue', 'vue', 'source.vue', 'Vue'], 8 | ['html', 'html', 'source.html', 'HTML'], 9 | ] 10 | 11 | export function getSyntaxInfo(ext: string) { 12 | return syntaxMaps.find(item => item[0] === ext) || syntaxMaps[0] 13 | } 14 | 15 | export const langageIds = syntaxMaps.map(item => item[0] ? `retypewriter-${item[0]}` : item[0]) 16 | -------------------------------------------------------------------------------- /packages/vscode/src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { TextDocument, TextEditor } from 'vscode' 2 | import { Uri, window, workspace } from 'vscode' 3 | import { getOriginalFilePath } from '../../core/src' 4 | 5 | export async function resolveDoc(doc?: TextDocument | Uri): Promise<{ 6 | doc?: TextDocument 7 | editor?: TextEditor 8 | }> { 9 | if (doc instanceof Uri) { 10 | const path = getOriginalFilePath(doc.fsPath) || doc.fsPath 11 | doc = await workspace.openTextDocument(Uri.file(path)) 12 | } 13 | doc = doc || window.activeTextEditor?.document 14 | if (!doc) 15 | return {} 16 | const editor = await window.showTextDocument(doc) 17 | return { 18 | doc, 19 | editor, 20 | } 21 | } 22 | 23 | export async function reveal(uri: Uri) { 24 | const doc = await workspace.openTextDocument(uri) 25 | await window.showTextDocument(doc) 26 | } 27 | -------------------------------------------------------------------------------- /packages/vscode/syntaxes/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "scopeName": "source.retypewriter", 4 | "patterns": [ 5 | { 6 | "include": "#options" 7 | }, 8 | { 9 | "include": "#snapshot" 10 | }, 11 | { 12 | "include": "#seperators" 13 | }, 14 | { 15 | "include": "source.text" 16 | } 17 | ], 18 | "repository": { 19 | "snapshot": { 20 | "name": "punctuation.separator", 21 | "begin": "--[\\d-]{2}----------", 22 | "end": "(-----options--|--[\\d-]{2}----------)", 23 | "patterns": [ 24 | { 25 | "include": "source.text" 26 | } 27 | ] 28 | }, 29 | "options": { 30 | "name": "punctuation.separator", 31 | "begin": "-----options--", 32 | "end": "--[\\d-]{2}----------", 33 | "patterns": [ 34 | { 35 | "include": "source.yaml" 36 | } 37 | ] 38 | }, 39 | "seperators": { 40 | "name": "punctuation.separator", 41 | "match": "(--[\\d-]{2}----------|-----options--|reTypewriter Snapshots v\\d)" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/vscode/syntaxes/html.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "scopeName": "source.retypewriter-html", 4 | "patterns": [ 5 | { 6 | "include": "#options" 7 | }, 8 | { 9 | "include": "#snapshot" 10 | }, 11 | { 12 | "include": "#seperators" 13 | }, 14 | { 15 | "include": "source.html" 16 | } 17 | ], 18 | "repository": { 19 | "snapshot": { 20 | "name": "punctuation.separator", 21 | "begin": "--[\\d-]{2}----------", 22 | "end": "(-----options--|--[\\d-]{2}----------)", 23 | "patterns": [ 24 | { 25 | "include": "source.html" 26 | } 27 | ] 28 | }, 29 | "options": { 30 | "name": "punctuation.separator", 31 | "begin": "-----options--", 32 | "end": "--[\\d-]{2}----------", 33 | "patterns": [ 34 | { 35 | "include": "source.yaml" 36 | } 37 | ] 38 | }, 39 | "seperators": { 40 | "name": "punctuation.separator", 41 | "match": "(--[\\d-]{2}----------|-----options--|reTypewriter Snapshots v\\d)" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/vscode/syntaxes/javascript.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "scopeName": "source.retypewriter-js", 4 | "patterns": [ 5 | { 6 | "include": "#options" 7 | }, 8 | { 9 | "include": "#snapshot" 10 | }, 11 | { 12 | "include": "#seperators" 13 | }, 14 | { 15 | "include": "source.js" 16 | } 17 | ], 18 | "repository": { 19 | "snapshot": { 20 | "name": "punctuation.separator", 21 | "begin": "--[\\d-]{2}----------", 22 | "end": "(-----options--|--[\\d-]{2}----------)", 23 | "patterns": [ 24 | { 25 | "include": "source.js" 26 | } 27 | ] 28 | }, 29 | "options": { 30 | "name": "punctuation.separator", 31 | "begin": "-----options--", 32 | "end": "--[\\d-]{2}----------", 33 | "patterns": [ 34 | { 35 | "include": "source.yaml" 36 | } 37 | ] 38 | }, 39 | "seperators": { 40 | "name": "punctuation.separator", 41 | "match": "(--[\\d-]{2}----------|-----options--|reTypewriter Snapshots v\\d)" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/vscode/syntaxes/javascriptreact.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "scopeName": "source.retypewriter-jsx", 4 | "patterns": [ 5 | { 6 | "include": "#options" 7 | }, 8 | { 9 | "include": "#snapshot" 10 | }, 11 | { 12 | "include": "#seperators" 13 | }, 14 | { 15 | "include": "source.jsx" 16 | } 17 | ], 18 | "repository": { 19 | "snapshot": { 20 | "name": "punctuation.separator", 21 | "begin": "--[\\d-]{2}----------", 22 | "end": "(-----options--|--[\\d-]{2}----------)", 23 | "patterns": [ 24 | { 25 | "include": "source.jsx" 26 | } 27 | ] 28 | }, 29 | "options": { 30 | "name": "punctuation.separator", 31 | "begin": "-----options--", 32 | "end": "--[\\d-]{2}----------", 33 | "patterns": [ 34 | { 35 | "include": "source.yaml" 36 | } 37 | ] 38 | }, 39 | "seperators": { 40 | "name": "punctuation.separator", 41 | "match": "(--[\\d-]{2}----------|-----options--|reTypewriter Snapshots v\\d)" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/vscode/syntaxes/typescript.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "scopeName": "source.retypewriter-ts", 4 | "patterns": [ 5 | { 6 | "include": "#options" 7 | }, 8 | { 9 | "include": "#snapshot" 10 | }, 11 | { 12 | "include": "#seperators" 13 | }, 14 | { 15 | "include": "source.ts" 16 | } 17 | ], 18 | "repository": { 19 | "snapshot": { 20 | "name": "punctuation.separator", 21 | "begin": "--[\\d-]{2}----------", 22 | "end": "(-----options--|--[\\d-]{2}----------)", 23 | "patterns": [ 24 | { 25 | "include": "source.ts" 26 | } 27 | ] 28 | }, 29 | "options": { 30 | "name": "punctuation.separator", 31 | "begin": "-----options--", 32 | "end": "--[\\d-]{2}----------", 33 | "patterns": [ 34 | { 35 | "include": "source.yaml" 36 | } 37 | ] 38 | }, 39 | "seperators": { 40 | "name": "punctuation.separator", 41 | "match": "(--[\\d-]{2}----------|-----options--|reTypewriter Snapshots v\\d)" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/vscode/syntaxes/typescriptreact.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "scopeName": "source.retypewriter-tsx", 4 | "patterns": [ 5 | { 6 | "include": "#options" 7 | }, 8 | { 9 | "include": "#snapshot" 10 | }, 11 | { 12 | "include": "#seperators" 13 | }, 14 | { 15 | "include": "source.jsx" 16 | } 17 | ], 18 | "repository": { 19 | "snapshot": { 20 | "name": "punctuation.separator", 21 | "begin": "--[\\d-]{2}----------", 22 | "end": "(-----options--|--[\\d-]{2}----------)", 23 | "patterns": [ 24 | { 25 | "include": "source.jsx" 26 | } 27 | ] 28 | }, 29 | "options": { 30 | "name": "punctuation.separator", 31 | "begin": "-----options--", 32 | "end": "--[\\d-]{2}----------", 33 | "patterns": [ 34 | { 35 | "include": "source.yaml" 36 | } 37 | ] 38 | }, 39 | "seperators": { 40 | "name": "punctuation.separator", 41 | "match": "(--[\\d-]{2}----------|-----options--|reTypewriter Snapshots v\\d)" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/vscode/syntaxes/vue.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "scopeName": "source.retypewriter-vue", 4 | "patterns": [ 5 | { 6 | "include": "#options" 7 | }, 8 | { 9 | "include": "#snapshot" 10 | }, 11 | { 12 | "include": "#seperators" 13 | }, 14 | { 15 | "include": "source.vue" 16 | } 17 | ], 18 | "repository": { 19 | "snapshot": { 20 | "name": "punctuation.separator", 21 | "begin": "--[\\d-]{2}----------", 22 | "end": "(-----options--|--[\\d-]{2}----------)", 23 | "patterns": [ 24 | { 25 | "include": "source.vue" 26 | } 27 | ] 28 | }, 29 | "options": { 30 | "name": "punctuation.separator", 31 | "begin": "-----options--", 32 | "end": "--[\\d-]{2}----------", 33 | "patterns": [ 34 | { 35 | "include": "source.yaml" 36 | } 37 | ] 38 | }, 39 | "seperators": { 40 | "name": "punctuation.separator", 41 | "match": "(--[\\d-]{2}----------|-----options--|reTypewriter Snapshots v\\d)" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | reTypewriter Playground 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite --open", 7 | "build": "vite build", 8 | "serve": "vite preview" 9 | }, 10 | "devDependencies": { 11 | "@iconify-json/carbon": "^1.1.30", 12 | "@types/codemirror": "^5.60.15", 13 | "@unocss/reset": "^0.58.5", 14 | "@vitejs/plugin-vue": "^5.0.4", 15 | "@vueuse/core": "^10.9.0", 16 | "codemirror": "^5.65.16", 17 | "codemirror-theme-vars": "^0.1.2", 18 | "splitpanes": "^3.1.5", 19 | "unocss": "^0.58.5", 20 | "unplugin-auto-import": "^0.17.5", 21 | "unplugin-vue-components": "^0.26.0", 22 | "vite": "^5.1.4", 23 | "vite-plugin-inspect": "^0.8.3", 24 | "vue": "^3.4.21" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /playground/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /playground/src/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | export {} 7 | declare global { 8 | const EffectScope: typeof import('vue')['EffectScope'] 9 | const asyncComputed: typeof import('@vueuse/core')['asyncComputed'] 10 | const autoResetRef: typeof import('@vueuse/core')['autoResetRef'] 11 | const computed: typeof import('vue')['computed'] 12 | const computedAsync: typeof import('@vueuse/core')['computedAsync'] 13 | const computedEager: typeof import('@vueuse/core')['computedEager'] 14 | const computedInject: typeof import('@vueuse/core')['computedInject'] 15 | const computedWithControl: typeof import('@vueuse/core')['computedWithControl'] 16 | const controlledComputed: typeof import('@vueuse/core')['controlledComputed'] 17 | const controlledRef: typeof import('@vueuse/core')['controlledRef'] 18 | const createApp: typeof import('vue')['createApp'] 19 | const createEventHook: typeof import('@vueuse/core')['createEventHook'] 20 | const createGlobalState: typeof import('@vueuse/core')['createGlobalState'] 21 | const createInjectionState: typeof import('@vueuse/core')['createInjectionState'] 22 | const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn'] 23 | const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate'] 24 | const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable'] 25 | const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise'] 26 | const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn'] 27 | const customRef: typeof import('vue')['customRef'] 28 | const debouncedRef: typeof import('@vueuse/core')['debouncedRef'] 29 | const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch'] 30 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 31 | const defineComponent: typeof import('vue')['defineComponent'] 32 | const eagerComputed: typeof import('@vueuse/core')['eagerComputed'] 33 | const effectScope: typeof import('vue')['effectScope'] 34 | const extendRef: typeof import('@vueuse/core')['extendRef'] 35 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 36 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 37 | const h: typeof import('vue')['h'] 38 | const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] 39 | const inject: typeof import('vue')['inject'] 40 | const injectLocal: typeof import('@vueuse/core')['injectLocal'] 41 | const isDefined: typeof import('@vueuse/core')['isDefined'] 42 | const isProxy: typeof import('vue')['isProxy'] 43 | const isReactive: typeof import('vue')['isReactive'] 44 | const isReadonly: typeof import('vue')['isReadonly'] 45 | const isRef: typeof import('vue')['isRef'] 46 | const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable'] 47 | const markRaw: typeof import('vue')['markRaw'] 48 | const nextTick: typeof import('vue')['nextTick'] 49 | const onActivated: typeof import('vue')['onActivated'] 50 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 51 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 52 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 53 | const onClickOutside: typeof import('@vueuse/core')['onClickOutside'] 54 | const onDeactivated: typeof import('vue')['onDeactivated'] 55 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 56 | const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke'] 57 | const onLongPress: typeof import('@vueuse/core')['onLongPress'] 58 | const onMounted: typeof import('vue')['onMounted'] 59 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 60 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 61 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 62 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 63 | const onStartTyping: typeof import('@vueuse/core')['onStartTyping'] 64 | const onUnmounted: typeof import('vue')['onUnmounted'] 65 | const onUpdated: typeof import('vue')['onUpdated'] 66 | const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] 67 | const provide: typeof import('vue')['provide'] 68 | const provideLocal: typeof import('@vueuse/core')['provideLocal'] 69 | const reactify: typeof import('@vueuse/core')['reactify'] 70 | const reactifyObject: typeof import('@vueuse/core')['reactifyObject'] 71 | const reactive: typeof import('vue')['reactive'] 72 | const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed'] 73 | const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit'] 74 | const reactivePick: typeof import('@vueuse/core')['reactivePick'] 75 | const readonly: typeof import('vue')['readonly'] 76 | const ref: typeof import('vue')['ref'] 77 | const refAutoReset: typeof import('@vueuse/core')['refAutoReset'] 78 | const refDebounced: typeof import('@vueuse/core')['refDebounced'] 79 | const refDefault: typeof import('@vueuse/core')['refDefault'] 80 | const refThrottled: typeof import('@vueuse/core')['refThrottled'] 81 | const refWithControl: typeof import('@vueuse/core')['refWithControl'] 82 | const resolveComponent: typeof import('vue')['resolveComponent'] 83 | const resolveRef: typeof import('@vueuse/core')['resolveRef'] 84 | const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] 85 | const shallowReactive: typeof import('vue')['shallowReactive'] 86 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 87 | const shallowRef: typeof import('vue')['shallowRef'] 88 | const syncRef: typeof import('@vueuse/core')['syncRef'] 89 | const syncRefs: typeof import('@vueuse/core')['syncRefs'] 90 | const templateRef: typeof import('@vueuse/core')['templateRef'] 91 | const throttledRef: typeof import('@vueuse/core')['throttledRef'] 92 | const throttledWatch: typeof import('@vueuse/core')['throttledWatch'] 93 | const toRaw: typeof import('vue')['toRaw'] 94 | const toReactive: typeof import('@vueuse/core')['toReactive'] 95 | const toRef: typeof import('vue')['toRef'] 96 | const toRefs: typeof import('vue')['toRefs'] 97 | const toValue: typeof import('vue')['toValue'] 98 | const triggerRef: typeof import('vue')['triggerRef'] 99 | const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount'] 100 | const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount'] 101 | const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted'] 102 | const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose'] 103 | const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted'] 104 | const unref: typeof import('vue')['unref'] 105 | const unrefElement: typeof import('@vueuse/core')['unrefElement'] 106 | const until: typeof import('@vueuse/core')['until'] 107 | const useActiveElement: typeof import('@vueuse/core')['useActiveElement'] 108 | const useAnimate: typeof import('@vueuse/core')['useAnimate'] 109 | const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference'] 110 | const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery'] 111 | const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter'] 112 | const useArrayFind: typeof import('@vueuse/core')['useArrayFind'] 113 | const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex'] 114 | const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast'] 115 | const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes'] 116 | const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin'] 117 | const useArrayMap: typeof import('@vueuse/core')['useArrayMap'] 118 | const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce'] 119 | const useArraySome: typeof import('@vueuse/core')['useArraySome'] 120 | const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique'] 121 | const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue'] 122 | const useAsyncState: typeof import('@vueuse/core')['useAsyncState'] 123 | const useAttrs: typeof import('vue')['useAttrs'] 124 | const useBase64: typeof import('@vueuse/core')['useBase64'] 125 | const useBattery: typeof import('@vueuse/core')['useBattery'] 126 | const useBluetooth: typeof import('@vueuse/core')['useBluetooth'] 127 | const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints'] 128 | const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel'] 129 | const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation'] 130 | const useCached: typeof import('@vueuse/core')['useCached'] 131 | const useClipboard: typeof import('@vueuse/core')['useClipboard'] 132 | const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems'] 133 | const useCloned: typeof import('@vueuse/core')['useCloned'] 134 | const useColorMode: typeof import('@vueuse/core')['useColorMode'] 135 | const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog'] 136 | const useCounter: typeof import('@vueuse/core')['useCounter'] 137 | const useCssModule: typeof import('vue')['useCssModule'] 138 | const useCssVar: typeof import('@vueuse/core')['useCssVar'] 139 | const useCssVars: typeof import('vue')['useCssVars'] 140 | const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement'] 141 | const useCycleList: typeof import('@vueuse/core')['useCycleList'] 142 | const useDark: typeof import('@vueuse/core')['useDark'] 143 | const useDateFormat: typeof import('@vueuse/core')['useDateFormat'] 144 | const useDebounce: typeof import('@vueuse/core')['useDebounce'] 145 | const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn'] 146 | const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory'] 147 | const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion'] 148 | const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation'] 149 | const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio'] 150 | const useDevicesList: typeof import('@vueuse/core')['useDevicesList'] 151 | const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia'] 152 | const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility'] 153 | const useDraggable: typeof import('@vueuse/core')['useDraggable'] 154 | const useDropZone: typeof import('@vueuse/core')['useDropZone'] 155 | const useElementBounding: typeof import('@vueuse/core')['useElementBounding'] 156 | const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint'] 157 | const useElementHover: typeof import('@vueuse/core')['useElementHover'] 158 | const useElementSize: typeof import('@vueuse/core')['useElementSize'] 159 | const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility'] 160 | const useEventBus: typeof import('@vueuse/core')['useEventBus'] 161 | const useEventListener: typeof import('@vueuse/core')['useEventListener'] 162 | const useEventSource: typeof import('@vueuse/core')['useEventSource'] 163 | const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper'] 164 | const useFavicon: typeof import('@vueuse/core')['useFavicon'] 165 | const useFetch: typeof import('@vueuse/core')['useFetch'] 166 | const useFileDialog: typeof import('@vueuse/core')['useFileDialog'] 167 | const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess'] 168 | const useFocus: typeof import('@vueuse/core')['useFocus'] 169 | const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin'] 170 | const useFps: typeof import('@vueuse/core')['useFps'] 171 | const useFullscreen: typeof import('@vueuse/core')['useFullscreen'] 172 | const useGamepad: typeof import('@vueuse/core')['useGamepad'] 173 | const useGeolocation: typeof import('@vueuse/core')['useGeolocation'] 174 | const useIdle: typeof import('@vueuse/core')['useIdle'] 175 | const useImage: typeof import('@vueuse/core')['useImage'] 176 | const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll'] 177 | const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver'] 178 | const useInterval: typeof import('@vueuse/core')['useInterval'] 179 | const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn'] 180 | const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier'] 181 | const useLastChanged: typeof import('@vueuse/core')['useLastChanged'] 182 | const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage'] 183 | const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys'] 184 | const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory'] 185 | const useMediaControls: typeof import('@vueuse/core')['useMediaControls'] 186 | const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery'] 187 | const useMemoize: typeof import('@vueuse/core')['useMemoize'] 188 | const useMemory: typeof import('@vueuse/core')['useMemory'] 189 | const useMounted: typeof import('@vueuse/core')['useMounted'] 190 | const useMouse: typeof import('@vueuse/core')['useMouse'] 191 | const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement'] 192 | const useMousePressed: typeof import('@vueuse/core')['useMousePressed'] 193 | const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver'] 194 | const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage'] 195 | const useNetwork: typeof import('@vueuse/core')['useNetwork'] 196 | const useNow: typeof import('@vueuse/core')['useNow'] 197 | const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl'] 198 | const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination'] 199 | const useOnline: typeof import('@vueuse/core')['useOnline'] 200 | const usePageLeave: typeof import('@vueuse/core')['usePageLeave'] 201 | const useParallax: typeof import('@vueuse/core')['useParallax'] 202 | const useParentElement: typeof import('@vueuse/core')['useParentElement'] 203 | const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver'] 204 | const usePermission: typeof import('@vueuse/core')['usePermission'] 205 | const usePointer: typeof import('@vueuse/core')['usePointer'] 206 | const usePointerLock: typeof import('@vueuse/core')['usePointerLock'] 207 | const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe'] 208 | const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme'] 209 | const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast'] 210 | const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark'] 211 | const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages'] 212 | const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion'] 213 | const usePrevious: typeof import('@vueuse/core')['usePrevious'] 214 | const useRafFn: typeof import('@vueuse/core')['useRafFn'] 215 | const useRefHistory: typeof import('@vueuse/core')['useRefHistory'] 216 | const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver'] 217 | const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation'] 218 | const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea'] 219 | const useScriptTag: typeof import('@vueuse/core')['useScriptTag'] 220 | const useScroll: typeof import('@vueuse/core')['useScroll'] 221 | const useScrollLock: typeof import('@vueuse/core')['useScrollLock'] 222 | const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage'] 223 | const useShare: typeof import('@vueuse/core')['useShare'] 224 | const useSlots: typeof import('vue')['useSlots'] 225 | const useSorted: typeof import('@vueuse/core')['useSorted'] 226 | const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition'] 227 | const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis'] 228 | const useStepper: typeof import('@vueuse/core')['useStepper'] 229 | const useStorage: typeof import('@vueuse/core')['useStorage'] 230 | const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync'] 231 | const useStyleTag: typeof import('@vueuse/core')['useStyleTag'] 232 | const useSupported: typeof import('@vueuse/core')['useSupported'] 233 | const useSwipe: typeof import('@vueuse/core')['useSwipe'] 234 | const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList'] 235 | const useTextDirection: typeof import('@vueuse/core')['useTextDirection'] 236 | const useTextSelection: typeof import('@vueuse/core')['useTextSelection'] 237 | const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize'] 238 | const useThrottle: typeof import('@vueuse/core')['useThrottle'] 239 | const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn'] 240 | const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory'] 241 | const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo'] 242 | const useTimeout: typeof import('@vueuse/core')['useTimeout'] 243 | const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn'] 244 | const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll'] 245 | const useTimestamp: typeof import('@vueuse/core')['useTimestamp'] 246 | const useTitle: typeof import('@vueuse/core')['useTitle'] 247 | const useToNumber: typeof import('@vueuse/core')['useToNumber'] 248 | const useToString: typeof import('@vueuse/core')['useToString'] 249 | const useToggle: typeof import('@vueuse/core')['useToggle'] 250 | const useTransition: typeof import('@vueuse/core')['useTransition'] 251 | const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams'] 252 | const useUserMedia: typeof import('@vueuse/core')['useUserMedia'] 253 | const useVModel: typeof import('@vueuse/core')['useVModel'] 254 | const useVModels: typeof import('@vueuse/core')['useVModels'] 255 | const useVibrate: typeof import('@vueuse/core')['useVibrate'] 256 | const useVirtualList: typeof import('@vueuse/core')['useVirtualList'] 257 | const useWakeLock: typeof import('@vueuse/core')['useWakeLock'] 258 | const useWebNotification: typeof import('@vueuse/core')['useWebNotification'] 259 | const useWebSocket: typeof import('@vueuse/core')['useWebSocket'] 260 | const useWebWorker: typeof import('@vueuse/core')['useWebWorker'] 261 | const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn'] 262 | const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus'] 263 | const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll'] 264 | const useWindowSize: typeof import('@vueuse/core')['useWindowSize'] 265 | const watch: typeof import('vue')['watch'] 266 | const watchArray: typeof import('@vueuse/core')['watchArray'] 267 | const watchAtMost: typeof import('@vueuse/core')['watchAtMost'] 268 | const watchDebounced: typeof import('@vueuse/core')['watchDebounced'] 269 | const watchDeep: typeof import('@vueuse/core')['watchDeep'] 270 | const watchEffect: typeof import('vue')['watchEffect'] 271 | const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable'] 272 | const watchImmediate: typeof import('@vueuse/core')['watchImmediate'] 273 | const watchOnce: typeof import('@vueuse/core')['watchOnce'] 274 | const watchPausable: typeof import('@vueuse/core')['watchPausable'] 275 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 276 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 277 | const watchThrottled: typeof import('@vueuse/core')['watchThrottled'] 278 | const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable'] 279 | const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter'] 280 | const whenever: typeof import('@vueuse/core')['whenever'] 281 | } 282 | // for type re-export 283 | declare global { 284 | // @ts-ignore 285 | export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue' 286 | import('vue') 287 | } 288 | -------------------------------------------------------------------------------- /playground/src/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | export {} 7 | 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | CodeMirror: typeof import('./components/CodeMirror.vue')['default'] 11 | Player: typeof import('./components/Player.vue')['default'] 12 | Playground: typeof import('./components/Playground.vue')['default'] 13 | Snap: typeof import('./components/Snap.vue')['default'] 14 | Snapshots: typeof import('./components/Snapshots.vue')['default'] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /playground/src/components/CodeMirror.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 |