diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..3550a30f --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/pull_request_template.md b/.github/pull_request_template.md similarity index 100% rename from pull_request_template.md rename to .github/pull_request_template.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f221ccaf..52aaacce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,13 +13,17 @@ jobs: with: access_token: ${{ github.token }} - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - - uses: actions/setup-node@v2 + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 + cache: pnpm + cache-dependency-path: pnpm-lock.yaml - - run: yarn install - - run: cd ./dev/cm5 && yarn install - - run: yarn run build - - run: yarn run testAll + - run: pnpm install --frozen-lockfile + - run: pnpm run typecheck + - run: pnpm run build + - run: pnpm run testAll diff --git a/.github/workflows/publish-cm5.yml b/.github/workflows/publish-cm5.yml new file mode 100644 index 00000000..1a01e643 --- /dev/null +++ b/.github/workflows/publish-cm5.yml @@ -0,0 +1,40 @@ +name: publish cm5-vim +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + publish: + name: Publish cm5-vim + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + registry-url: https://registry.npmjs.org + + - run: pnpm install --frozen-lockfile + + # cm5-vim has no prepare script; its published files (vim.js, + # vim_test.js) are generated by its build. + - run: pnpm --filter cm5-vim run build + + - name: Publish + env: + NODE_AUTH_TOKEN: ${{ secrets.CM5_NPM_TOKEN }} + run: | + version=$(node -p "require('./packages/cm5-vim/package.json').version") + if [ "$(npm view "cm5-vim@$version" version 2>/dev/null)" = "$version" ]; then + echo "cm5-vim@$version is already published, skipping" + else + echo "Publishing cm5-vim@$version" + (cd packages/cm5-vim && pnpm publish --no-git-checks) + fi diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..4f07d075 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,41 @@ +name: publish +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + publish: + name: Publish @replit/codemirror-vim-core and @replit/codemirror-vim + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + registry-url: https://registry.npmjs.org + + - run: pnpm install --frozen-lockfile + + # Core is published first: @replit/codemirror-vim depends on it, so the + # version its workspace:^ range resolves to must exist on the registry. + - name: Publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + for dir in packages/codemirror-vim-core packages/codemirror-vim; do + name=$(node -p "require('./$dir/package.json').name") + version=$(node -p "require('./$dir/package.json').version") + if [ "$(npm view "$name@$version" version 2>/dev/null)" = "$version" ]; then + echo "$name@$version is already published, skipping" + else + echo "Publishing $name@$version" + (cd "$dir" && pnpm publish --access public --no-git-checks) + fi + done diff --git a/.github/workflows/test-link.yml b/.github/workflows/test-link.yml index 46ed8f1e..d2814235 100644 --- a/.github/workflows/test-link.yml +++ b/.github/workflows/test-link.yml @@ -20,7 +20,7 @@ jobs: const owner = context.payload.pull_request.head.repo.owner.login; const repo = context.payload.pull_request.head.repo.name; const commit = context.payload.pull_request.head.sha; - const link = `https://raw.githack.com/${owner}/${repo}/${commit}/dev/web-demo.html`; + const link = `https://raw.githack.com/${owner}/${repo}/${commit}/packages/codemirror-vim/dev/web-demo.html`; const { data: pullRequest } = await github.rest.pulls.get({ owner: context.repo.owner, diff --git a/.gitignore b/.gitignore index a0546c48..831540a4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,11 @@ node_modules package-lock.json dist out -/test/*.js -/test/*.d.ts -/test/*.d.ts.map +/packages/codemirror-vim/test/*.js +/packages/codemirror-vim/test/*.d.ts +/packages/codemirror-vim/test/*.d.ts.map .* +!.envrc +!.github +!.github/** .DS_Store \ No newline at end of file diff --git a/.replit b/.replit index f768bf8d..78034eb4 100644 --- a/.replit +++ b/.replit @@ -1,7 +1,7 @@ -run="yarn run dev" +run="pnpm run dev" [packager] -language = "nodejs-yarn" +language = "nodejs-pnpm" [packager.features] packageSearch = true guessImports = false diff --git a/README.md b/README.md index efe1e15c..313c8f0b 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,21 @@ -# Vim keybindings for CM6 +# codemirror-vim -Run on Replit badge -NPM version badge +Vim keybindings for CodeMirror. -## Installation +This is a pnpm workspace containing three packages: -```sh -npm i @replit/codemirror-vim -``` - -## Usage - -```js -import { basicSetup, EditorView } from 'codemirror'; -import { vim } from "@replit/codemirror-vim" - -let view = new EditorView({ - doc: "", - extensions: [ - // make sure vim is included before other keymaps - vim(), - // include the default keymap and all other keymaps you want to use in insert mode - basicSetup, - ], - parent: document.querySelector('#editor'), -}) -``` -> **Note**: -> if you are not using `basicSetup`, make sure you include the [drawSelection](https://codemirror.net/docs/ref/#view.drawSelection) plugin to correctly render the selection in visual mode. - -## Usage of cm5 vim extension api - -The same api that could be used in previous version of codemirror https://codemirror.net/doc/manual.html#vimapi, can be used with this plugin too, just replace the old editor instance with `view.cm` in your code - -```js -import {Vim, getCM} from "@replit/codemirror-vim" - -let cm = getCM(view) -// use cm to access the old cm5 api -Vim.exitInsertMode(cm) -Vim.handleKey(cm, "") -``` - -### Define additional ex commands -```js -Vim.defineEx('write', 'w', function() { - // save the file -}); -``` - -### Map keys -```js -Vim.map("jj", "", "insert"); // in insert mode -Vim.map("Y", "y$"); // in normal mode -``` +- [`@replit/codemirror-vim`](packages/codemirror-vim) — Vim keybindings for CodeMirror 6 (the main package) +- [`cm5-vim`](packages/cm5-vim) — the same vim engine bundled for CodeMirror 5 +- [`@replit/codemirror-vim-core`](packages/codemirror-vim-core) — the editor-agnostic vim engine and shared test suite used by both, originally authored by [@mightyguava](https://github.com/mightyguava) (Yunchi Luo) -### Unmap keys +## Development -```js -Vim.unmap("jj", "insert"); -``` - -### Add custom key - -```js - defaultKeymap.push({ keys: 'gq', type: 'operator', operator: 'hardWrap' }); - Vim.defineOperator("hardWrap", function(cm, operatorArgs, ranges, oldAnchor, newHead) { - // make changes and return new cursor position - }); +```sh +pnpm install +pnpm run dev # start the CodeMirror 6 demo +pnpm run test # run the CodeMirror 6 test suite +pnpm run testAll # run all tests, including cm5 and packaging checks ``` -## Credits - -This plugin was originally authored by [@mightyguava](https://github.com/mightyguava) (Yunchi Luo) as part of [CodeMirror](https://github.com/codemirror/dev), before being extracted and maintained here. +The repo ships a nix dev shell via direnv: `direnv allow` provides node, pnpm, +and the browsers needed for tests. diff --git a/dev/cm5/package.json b/dev/cm5/package.json deleted file mode 100644 index e4ef59ac..00000000 --- a/dev/cm5/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "cm5-vim", - "version": "0.0.6", - "description": "Vim keybindings for CodeMirror 5", - "files": [ - "vim.js", - "vim_test.js" - ], - "scripts": { - "test": "cm-runtests .", - "build": "node bin/build.js", - "buildAndTest": "yarn run build && yarn test", - "publish": "yarn run build && npm publish" - }, - "keywords": [ - "editor", - "code" - ], - "main": "vim.js", - "sideEffects": false, - "license": "MIT", - "devDependencies": { - "codemirror": "5.*" - }, - "repository": { - "type": "git", - "url": "https://github.com/replit/codemirror-vim.git" - } -} \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..cbc90502 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1785141334, + "narHash": "sha256-kh35kIx7el4Jk8Ki3BH9/Pn1eZYSYLJ6LMALos0zOy0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "38a4887411571457d700c51c64a6e49ead2ed5ab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..f5cb035a --- /dev/null +++ b/flake.nix @@ -0,0 +1,24 @@ +{ + description = "Dev environment for @replit/codemirror-vim"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + + outputs = { self, nixpkgs }: + let + systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; + forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system}); + in { + devShells = forAllSystems (pkgs: { + default = pkgs.mkShell { + packages = [ + pkgs.nodejs_22 + pkgs.pnpm + ] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ + # needed by cm-runtests (selenium) + pkgs.chromedriver + pkgs.chromium + ]; + }; + }); + }; +} diff --git a/package.json b/package.json index 6fcc66b5..9e3ab3e2 100644 --- a/package.json +++ b/package.json @@ -1,56 +1,14 @@ { - "name": "@replit/codemirror-vim", - "version": "6.3.0", - "description": "Vim keybindings for CodeMirror 6", - "author": { - "name": "Yunchi Luo", - "email": "mightyguava@gmail.com" - }, + "name": "codemirror-vim-workspace", + "private": true, + "packageManager": "pnpm@11.17.0", "scripts": { - "dev": "vite ./dev", - "test": "cm-runtests", - "build-types": "rm -rf out && tsc && dts-bundle-generator out/index.ts -o dist/index.d.ts && cp dist/index.d.ts dist/index.d.cts", - "testAll": "yarn run build-test-package && yarn run test && cd dev/cm5 && yarn run buildAndTest", - "build": "cm-buildhelper src/index.ts && node scripts/addVersion.cjs && npm run build-types", - "publish": "yarn run build && npm publish", - "build-test-package": "bash ./scripts/build-test-package.sh", - "prepare": "yarn run build" - }, - "keywords": [ - "editor", - "code" - ], - "type": "module", - "main": "dist/index.cjs", - "exports": { - "import": "./dist/index.js", - "require": "./dist/index.cjs" - }, - "types": "dist/index.d.ts", - "module": "dist/index.js", - "sideEffects": false, - "license": "MIT", - "peerDependencies": { - "@codemirror/commands": "6.x.x", - "@codemirror/language": "6.x.x", - "@codemirror/search": "6.x.x", - "@codemirror/state": "6.x.x", - "@codemirror/view": "6.x.x" - }, - "devDependencies": { - "@codemirror/buildhelper": "^1.0.2", - "@codemirror/lang-javascript": "6.x.x", - "@codemirror/lang-xml": "6.x.x", - "@codemirror/language": "6.x.x", - "codemirror": "6.x.x", - "dts-bundle-generator": "^9.5.1", - "typescript": "^5.8.2", - "vite": "^6.2.2" + "dev": "pnpm --filter @replit/codemirror-vim run dev", + "build": "pnpm -r run build", + "test": "pnpm --filter @replit/codemirror-vim run test", + "testAll": "pnpm --filter @replit/codemirror-vim run testAll && pnpm --filter cm5-vim run buildAndTest", + "typecheck": "pnpm -r run typecheck" }, - "files": [ - "dist", - "README.md" - ], "repository": { "type": "git", "url": "https://github.com/replit/codemirror-vim.git" diff --git a/dev/cm5/.gitignore b/packages/cm5-vim/.gitignore similarity index 100% rename from dev/cm5/.gitignore rename to packages/cm5-vim/.gitignore diff --git a/packages/cm5-vim/LICENSE b/packages/cm5-vim/LICENSE new file mode 100644 index 00000000..3af12e62 --- /dev/null +++ b/packages/cm5-vim/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (C) 2018-2021 by Marijn Haverbeke and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/dev/cm5/bin/build.js b/packages/cm5-vim/bin/build.js similarity index 77% rename from dev/cm5/bin/build.js rename to packages/cm5-vim/bin/build.js index 92dc67b8..539912eb 100644 --- a/dev/cm5/bin/build.js +++ b/packages/cm5-vim/bin/build.js @@ -1,8 +1,10 @@ var fs = require("fs"); var { rollup } = require('rollup'); +var { nodeResolve } = require('@rollup/plugin-node-resolve'); rollup({ - input: "./index" + input: "./index", + plugins: [nodeResolve()] }).then(function(a) { return a.write({ file: "./bin/.bundle.js", @@ -21,23 +23,19 @@ rollup({ mod(CodeMirror); })(function(CodeMirror) { 'use strict'; - ` - + bundle + + ` + + bundle + ` CodeMirror.Vim = initVim(CodeMirror); }); `; + bundle = bundle.replace("", require("../package.json").version); + fs.writeFileSync("vim.js", bundle, "utf8"); }); -var test = fs.readFileSync("../../test/vim_test.js", "utf8"); +var test = fs.readFileSync(require.resolve("@replit/codemirror-vim-core/test/vim_test.js"), "utf8"); test = test.replace(/export function/, "function") + "\nvimTests(CodeMirror, test);"; fs.writeFileSync("vim_test.js", test, "utf8"); - -// update version -var version = require("../../../package.json").version; -var package = require("../package.json"); -package.version = version; -fs.writeFileSync("./package.json", JSON.stringify(package, null, 2), "utf8"); diff --git a/dev/cm5/index.html b/packages/cm5-vim/index.html similarity index 98% rename from dev/cm5/index.html rename to packages/cm5-vim/index.html index b720d9ea..186595a6 100644 --- a/dev/cm5/index.html +++ b/packages/cm5-vim/index.html @@ -90,7 +90,7 @@

Vim bindings for CodeMirror 5 demo

window._editor = editor; - import { vimTests } from "../../test/vim_test.js"; + import { vimTests } from "../codemirror-vim-core/test/vim_test.js"; window.vimTests = vimTests; setTimeout(onLoad); diff --git a/dev/cm5/index.js b/packages/cm5-vim/index.js similarity index 98% rename from dev/cm5/index.js rename to packages/cm5-vim/index.js index 2d1dd39e..9d562cc2 100644 --- a/dev/cm5/index.js +++ b/packages/cm5-vim/index.js @@ -1,4 +1,4 @@ -import { initVim as initVimInternal } from "../../src/vim.js"; +import { initVim as initVimInternal } from "@replit/codemirror-vim-core"; export function initVim(CodeMirror) { var Vim = CodeMirror.Vim = initVimInternal(CodeMirror); diff --git a/packages/cm5-vim/package.json b/packages/cm5-vim/package.json new file mode 100644 index 00000000..8871a7ca --- /dev/null +++ b/packages/cm5-vim/package.json @@ -0,0 +1,38 @@ +{ + "name": "cm5-vim", + "version": "0.0.6", + "description": "Vim keybindings for CodeMirror 5", + "author": { + "name": "nightwing", + "url": "https://github.com/nightwing" + }, + "files": [ + "vim.js", + "vim_test.js" + ], + "scripts": { + "test": "cm-runtests .", + "build": "node bin/build.js", + "buildAndTest": "pnpm run build && pnpm test", + "publish": "pnpm run build && npm publish" + }, + "keywords": [ + "editor", + "code" + ], + "main": "vim.js", + "sideEffects": false, + "license": "MIT", + "devDependencies": { + "@codemirror/buildhelper": "^1.0.2", + "@replit/codemirror-vim-core": "workspace:^", + "@rollup/plugin-node-resolve": "^16.0.0", + "codemirror": "5.*", + "rollup": "^4.36.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/replit/codemirror-vim.git", + "directory": "packages/cm5-vim" + } +} diff --git a/dev/cm5/test/webtest-cm5.js b/packages/cm5-vim/test/webtest-cm5.js similarity index 95% rename from dev/cm5/test/webtest-cm5.js rename to packages/cm5-vim/test/webtest-cm5.js index b45d50c5..04b10e2a 100644 --- a/dev/cm5/test/webtest-cm5.js +++ b/packages/cm5-vim/test/webtest-cm5.js @@ -1,4 +1,4 @@ -import { vimTests } from "../../../test/vim_test.js" +import { vimTests } from "@replit/codemirror-vim-core/test/vim_test.js" import "codemirror/lib/codemirror.js" import "codemirror/addon/dialog/dialog.js" import "codemirror/addon/search/searchcursor.js" diff --git a/packages/codemirror-vim-core/LICENSE b/packages/codemirror-vim-core/LICENSE new file mode 100644 index 00000000..3af12e62 --- /dev/null +++ b/packages/codemirror-vim-core/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (C) 2018-2021 by Marijn Haverbeke and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/codemirror-vim-core/README.md b/packages/codemirror-vim-core/README.md new file mode 100644 index 00000000..28ce2b29 --- /dev/null +++ b/packages/codemirror-vim-core/README.md @@ -0,0 +1,17 @@ +# @replit/codemirror-vim-core + +The editor-agnostic vim engine behind [`@replit/codemirror-vim`](https://www.npmjs.com/package/@replit/codemirror-vim) (CodeMirror 6) and [`cm5-vim`](https://www.npmjs.com/package/cm5-vim) (CodeMirror 5). + +It implements vim's modal editing — motions, operators, actions, registers, marks, macros, and ex commands — against a CodeMirror-5-style editor interface (see `CM5EditorInterface` in the type definitions). You most likely want one of the packages above rather than this one directly; use this package if you're building your own editor adapter. + +```js +import { initVim } from "@replit/codemirror-vim-core" + +const Vim = initVim(CodeMirror) // a CM5-style editor constructor +``` + +The shared vim test suite (700+ tests) ships at `@replit/codemirror-vim-core/test/vim_test.js`. + +## Credits + +This engine was originally authored by [@mightyguava](https://github.com/mightyguava) (Yunchi Luo) as part of [CodeMirror](https://github.com/codemirror/dev), before being extracted and maintained in [replit/codemirror-vim](https://github.com/replit/codemirror-vim). diff --git a/packages/codemirror-vim-core/main.d.ts b/packages/codemirror-vim-core/main.d.ts new file mode 100644 index 00000000..35496fae --- /dev/null +++ b/packages/codemirror-vim-core/main.d.ts @@ -0,0 +1,2 @@ +export { initVim } from "./dist/vim" +export * from "./dist/types" diff --git a/packages/codemirror-vim-core/package.json b/packages/codemirror-vim-core/package.json new file mode 100644 index 00000000..8f3a6f42 --- /dev/null +++ b/packages/codemirror-vim-core/package.json @@ -0,0 +1,45 @@ +{ + "name": "@replit/codemirror-vim-core", + "version": "0.1.0", + "description": "Editor-agnostic vim engine shared by @replit/codemirror-vim (CodeMirror 6) and cm5-vim (CodeMirror 5)", + "author": { + "name": "Yunchi Luo", + "url": "https://github.com/mightyguava" + }, + "type": "module", + "main": "./vim.js", + "types": "./main.d.ts", + "exports": { + ".": { + "types": "./main.d.ts", + "default": "./vim.js" + }, + "./test/vim_test.js": "./test/vim_test.js" + }, + "scripts": { + "build": "tsc", + "prepare": "pnpm run build", + "typecheck": "tsc --emitDeclarationOnly false --noEmit" + }, + "keywords": [ + "editor", + "code", + "vim" + ], + "sideEffects": false, + "license": "MIT", + "devDependencies": { + "typescript": "^5.8.2" + }, + "files": [ + "vim.js", + "main.d.ts", + "dist", + "test/vim_test.js" + ], + "repository": { + "type": "git", + "url": "https://github.com/replit/codemirror-vim.git", + "directory": "packages/codemirror-vim-core" + } +} diff --git a/test/vim_test.js b/packages/codemirror-vim-core/test/vim_test.js similarity index 100% rename from test/vim_test.js rename to packages/codemirror-vim-core/test/vim_test.js diff --git a/packages/codemirror-vim-core/tsconfig.json b/packages/codemirror-vim-core/tsconfig.json new file mode 100644 index 00000000..e54e04ba --- /dev/null +++ b/packages/codemirror-vim-core/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "noImplicitAny": false, + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "dist", + "allowJs": true, + "checkJs": true, + "lib": ["es6", "dom", "scripthost"], + "stripInternal": true, + "noUnusedLocals": true, + "strict": true, + "target": "es6", + "module": "es2020", + "newLine": "lf", + "moduleResolution": "node", + }, + "files": ["vim.js", "types.ts"] +} diff --git a/src/types.ts b/packages/codemirror-vim-core/types.ts similarity index 63% rename from src/types.ts rename to packages/codemirror-vim-core/types.ts index ba15af2e..fbf98a6c 100644 --- a/src/types.ts +++ b/packages/codemirror-vim-core/types.ts @@ -1,5 +1,4 @@ -import { CodeMirror } from "./cm_adapter" -import {initVim} from "./vim" +import {initVim} from "./vim.js" export type Vim = ReturnType export type vimState = { onPasteFn?: () => void, @@ -37,8 +36,16 @@ export type vimState = { mode?: string, expectLiteralNext?: boolean, } -export type Marker = ReturnType -export type LineHandle = ReturnType +export interface Marker { + cm: CM5EditorInterface + id: number + offset: number | null + assoc: number + clear(): void + find(): Pos | null + update(change: any): void +} +export type LineHandle = { row: number, index: number } export type Pos = { line: number, ch: number, sticky?: string } export interface CM5Range { @@ -81,8 +88,8 @@ export type OperatorArgs = { }, keepCursor?: boolean; } -// version of CodeMirror with vim state checked -export type CodeMirrorV = CodeMirror & {state: {vim: vimState}} +// version of CM5EditorInterface with vim state checked +export type CodeMirrorV = CM5EditorInterface & {state: {vim: vimState}} export type OperatorFn = (cm: CodeMirrorV, args: OperatorArgs, ranges: CM5RangeInterface[], oldAnchor: Pos, newHead?: Pos) => Pos|void export type vimOperators = { change(cm: CodeMirrorV, args: OperatorArgs, ranges: CM5RangeInterface[]): void, @@ -197,10 +204,10 @@ export type exCommandDefinition = { noremap?: boolean, }; -export type optionCallback = (value?: string|undefined, cm?: CodeMirror) => any -export type booleanOptionCallback = (value?: boolean, cm?: CodeMirror) => any -export type numberOptionCallback = (value?: number, cm?: CodeMirror) => any -export type stringOptionCallback = (value?: string, cm?: CodeMirror) => any +export type optionCallback = (value?: string|undefined, cm?: CM5EditorInterface) => any +export type booleanOptionCallback = (value?: boolean, cm?: CM5EditorInterface) => any +export type numberOptionCallback = (value?: number, cm?: CM5EditorInterface) => any +export type stringOptionCallback = (value?: string, cm?: CM5EditorInterface) => any export type vimOption = { type?: string, @@ -412,4 +419,175 @@ declare global { interface String { trimStart(): string } -} \ No newline at end of file +} +/** + * The editor API the vim engine drives. This is the CM5-style editor + * interface; @replit/codemirror-vim's adapter implements it on top of + * CM5EditorInterface 6, and cm5-vim binds it to a real CM5EditorInterface 5 instance. + */ +export interface CM5EditorInterface { + state: { + statusbar?: Element | null + dialog?: HTMLElement | null + vimPlugin?: any + vim?: vimState | null + currentNotificationClose?: Function | null + closeVimNotification?: Function | null + keyMap?: string + overwrite?: boolean + textwidth?: number + } + marks: Record + $mid: number + curOp: { + $d: number + isVimOp?: boolean + cursorActivityHandlers?: Function[] + cursorActivity?: boolean + lastChange?: any + change?: any + changeHandlers?: Function[] + $changeStart?: number + } | null | undefined + options: any + _handlers: any + /** the host editor instance the adapter wraps (an EditorView for CM5EditorInterface 6) */ + cm6: any + openDialog(template: Element, callback: Function | undefined, options: any): (newVal?: string) => void + openNotification(template: Node, options: { bottom?: boolean, duration?: number }): () => void + on(type: string, f: Function): void + off(type: string, f: Function): void + signal(type: string, e: any, handlers?: any): void + indexFromPos(pos: Pos): number + posFromIndex(offset: number): Pos + foldCode(pos: Pos): void + firstLine(): number + lastLine(): number + lineCount(): number + setCursor(line: number, ch: number): void + setCursor(line: Pos): void + getCursor(p?: "head" | "anchor" | "start" | "end"): Pos + listSelections(): { anchor: Pos, head: Pos }[] + setSelections(p: CM5RangeInterface[], primIndex?: number): void + setSelection(anchor: Pos, head: Pos, options?: any): void + getLine(row: number): string + getLineHandle(row: number): LineHandle + getLineNumber(handle: any): number | null + releaseLineHandles(): void + getRange(s: Pos, e: Pos): string + replaceRange(text: string, s: Pos, e?: Pos, source?: string): void + replaceSelection(text: string): void + replaceSelections(replacements: string[]): void + getSelection(): string + getSelections(): string[] + somethingSelected(): boolean + getInputField(): HTMLElement + clipPos(p: Pos): Pos + getValue(): string + setValue(text: string): void + focus(): void + blur(): void + defaultTextHeight(): number + findMatchingBracket(pos: Pos, _options?: any): { to: Pos } | { to: undefined } + scanForBracket(pos: Pos, dir: 1 | -1, style: any, config: any): false | { pos: Pos, ch: string } | null + indentLine(line: number, more?: boolean): void + indentMore(): void + indentLess(): void + execCommand(name: string): void + setBookmark(cursor: Pos, options?: { insertLeft: boolean }): Marker + addOverlay(overlay: { query: RegExp }): any + removeOverlay(overlay?: any): void + getSearchCursor(query: RegExp, pos: Pos): { + findNext: () => string[] | null | undefined + findPrevious: () => string[] | null | undefined + find: (back?: boolean) => string[] | null | undefined + from: () => Pos | undefined + to: () => Pos | undefined + replace: (text: string) => void + readonly match: string[] | null + } + findPosV(start: Pos, amount: number, unit: "page" | "line", goalColumn?: number): Pos & { hitSide?: boolean } + charCoords(pos: Pos, mode: "div" | "local"): { left: number, top: number, bottom: number } + coordsChar(coords: { left: number, top: number }, mode: "div" | "local"): Pos + getScrollInfo(): { left: number, top: number, height: number, width: number, clientHeight: number, clientWidth: number } + scrollTo(x?: number | null, y?: number | null): void + scrollIntoView(pos?: Pos, margin?: number): void + getWrapperElement(): HTMLElement + getMode(): { name: string | number | boolean | undefined } + setSize(w: number, h: number): void + refresh(): void + destroy(): void + getLastEditEnd(): Pos + $lastChangeEndOffset: number + $lineHandleChanges: any[] | undefined + onChange(update: any): void + onSelectionChange(): void + operation(fn: () => T, force?: boolean): T + onBeforeEndOperation(): void + moveH(increment: number, unit: string): void + setOption(name: string, val: any): void + getOption(name: "firstLineNumber" | "tabSize" | "textwidth"): number + getOption(name: string): number | boolean | string | undefined + toggleOverwrite(on: boolean): void + getTokenTypeAt(pos: Pos): "" | "string" | "comment" + overWriteSelection(text: string): void + isInMultiSelectMode(): boolean + virtualSelectionMode(): boolean + virtualSelection: any + forEachSelection(command: Function): void + hardWrap(options: { from: number, to: number, column?: number, allowMerge?: boolean }): number + showMatchesOnScrollbar?: Function + save?: Function +} + +/** The constructor-with-statics shape passed to initVim. */ +export interface CodeMirrorConstructor { + new (host: any): CM5EditorInterface + isMac: boolean + Pos: new (line: number, ch: number) => Pos + StringStream: new (str: string) => StringStream + commands: { + toggleLineComment: (cm: CM5EditorInterface) => void + cursorCharLeft: (cm: CM5EditorInterface) => void + redo: (cm: CM5EditorInterface) => void + undo: (cm: CM5EditorInterface) => void + newlineAndIndent: (cm: CM5EditorInterface) => void + indentAuto: (cm: CM5EditorInterface) => void + newlineAndIndentContinueComment: any + save: any + } + isWordChar: (ch: string) => boolean + keys: any + addClass: (el: any, str: string) => void + rmClass: (el: any, str: string) => void + e_preventDefault: (e: Event) => void + e_stop: (e: Event) => void + lookupKey: (key: string, map: string, handle: Function) => void + on: (emitter: any, type: string, f: Function) => void + off: (emitter: any, type: string, f: Function) => void + signal: (emitter: any, type: string, ...args: any[]) => void + findMatchingTag: (cm: CM5EditorInterface, pos: Pos) => any + findEnclosingTag: (cm: CM5EditorInterface, pos: Pos) => { open: { from: Pos, to: Pos }, close: { from: Pos, to: Pos } } | undefined + keyName?: Function +} + +/** Structural interface of the token stream handed to ex-command parsers. */ +export interface StringStream { + pos: number + start: number + string: string + next(): string | void + peek(): string | void + eat(match: string | RegExp | ((ch: string) => boolean)): string | void + eatWhile(match: string | RegExp | ((ch: string) => boolean)): boolean + eatSpace(): boolean + skipToEnd(): void + skipTo(ch: string): boolean | void + backUp(n: number): void + column(): number + indentation(): number + match(pattern: string | RegExp, consume?: boolean, caseInsensitive?: boolean): boolean | RegExpMatchArray | null + current(): string + eol(): boolean + sol(): boolean +} diff --git a/src/vim.js b/packages/codemirror-vim-core/vim.js similarity index 99% rename from src/vim.js rename to packages/codemirror-vim-core/vim.js index d2665026..3cafa6bc 100644 --- a/src/vim.js +++ b/packages/codemirror-vim-core/vim.js @@ -37,11 +37,11 @@ */ -/** @arg {typeof import("./cm_adapter").CodeMirror} CM */ +/** @arg {import("./types").CodeMirrorConstructor} CM */ export function initVim(CM) { /** - * @typedef { import("./cm_adapter").CodeMirror } CodeMirror + * @typedef { import("./types").CM5EditorInterface } CodeMirror * @typedef { import("./types").CodeMirrorV} CodeMirrorV * @typedef { import("./types").Pos } Pos * @typedef { import("./types").vimState } vimState @@ -5687,7 +5687,7 @@ export function initVim(CM) { } /** * @param {CodeMirrorV} cm - * @param {import("@codemirror/language").StringStream} inputStream + * @param {import("./types").StringStream} inputStream * @param {{ callback?: (() => void) | undefined; input?: string | undefined; line?: any; commandName?: any; lineEnd?: any; selectionLine?: any; selectionLineEnd?: any; }} result */ parseInput_(cm, inputStream, result) { @@ -5734,7 +5734,7 @@ export function initVim(CM) { } /** * @param {CodeMirrorV} cm - * @param {import("@codemirror/language").StringStream} inputStream + * @param {import("./types").StringStream} inputStream */ parseLineSpec_(cm, inputStream) { var numberMatch = inputStream.match(/^([\d]+)/); @@ -5765,7 +5765,7 @@ export function initVim(CM) { } /** * @param {CodeMirrorV} cm - * @param {string | import("@codemirror/language").StringStream} inputStream + * @param {string | import("./types").StringStream} inputStream * @param {number} line */ parseLineSpecOffset_(cm, inputStream, line) { @@ -5805,7 +5805,7 @@ export function initVim(CM) { return line; } /** - * @param {import("@codemirror/language").StringStream} inputStream + * @param {import("./types").StringStream} inputStream * @param {import("./types").exCommandArgs} params * @param {import("./types").exCommandDefinition} command */ diff --git a/CHANGELOG.md b/packages/codemirror-vim/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to packages/codemirror-vim/CHANGELOG.md diff --git a/packages/codemirror-vim/LICENSE b/packages/codemirror-vim/LICENSE new file mode 100644 index 00000000..3af12e62 --- /dev/null +++ b/packages/codemirror-vim/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (C) 2018-2021 by Marijn Haverbeke and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/codemirror-vim/README.md b/packages/codemirror-vim/README.md new file mode 100644 index 00000000..fdb59ae2 --- /dev/null +++ b/packages/codemirror-vim/README.md @@ -0,0 +1,72 @@ +# Vim keybindings for CM6 + +Run on Replit badge +NPM version badge + +## Installation + +```sh +npm i @replit/codemirror-vim +``` + +## Usage + +```js +import { basicSetup, EditorView } from 'codemirror'; +import { vim } from "@replit/codemirror-vim" + +let view = new EditorView({ + doc: "", + extensions: [ + // make sure vim is included before other keymaps + vim(), + // include the default keymap and all other keymaps you want to use in insert mode + basicSetup, + ], + parent: document.querySelector('#editor'), +}) +``` +> **Note**: +> if you are not using `basicSetup`, make sure you include the [drawSelection](https://codemirror.net/docs/ref/#view.drawSelection) plugin to correctly render the selection in visual mode. + +## Usage of cm5 vim extension api + +The same api that could be used in previous version of codemirror https://codemirror.net/doc/manual.html#vimapi, can be used with this plugin too, just replace the old editor instance with `view.cm` in your code + +```js +import {Vim, getCM} from "@replit/codemirror-vim" + +let cm = getCM(view) +// use cm to access the old cm5 api +Vim.exitInsertMode(cm) +Vim.handleKey(cm, "") +``` + +### Define additional ex commands +```js +Vim.defineEx('write', 'w', function() { + // save the file +}); +``` + +### Map keys +```js +Vim.map("jj", "", "insert"); // in insert mode +Vim.map("Y", "y$"); // in normal mode +``` + +### Unmap keys + +```js +Vim.unmap("jj", "insert"); +``` + +### Add custom key + +```js + defaultKeymap.push({ keys: 'gq', type: 'operator', operator: 'hardWrap' }); + Vim.defineOperator("hardWrap", function(cm, operatorArgs, ranges, oldAnchor, newHead) { + // make changes and return new cursor position + }); +``` + diff --git a/dev/index.html b/packages/codemirror-vim/dev/index.html similarity index 92% rename from dev/index.html rename to packages/codemirror-vim/dev/index.html index 9dd5e00b..3daf8036 100644 --- a/dev/index.html +++ b/packages/codemirror-vim/dev/index.html @@ -19,6 +19,6 @@
tests |  - cm5 demo + cm5 demo diff --git a/dev/index.ts b/packages/codemirror-vim/dev/index.ts similarity index 100% rename from dev/index.ts rename to packages/codemirror-vim/dev/index.ts diff --git a/dev/test.html b/packages/codemirror-vim/dev/test.html similarity index 100% rename from dev/test.html rename to packages/codemirror-vim/dev/test.html diff --git a/dev/vite.config.js b/packages/codemirror-vim/dev/vite.config.js similarity index 100% rename from dev/vite.config.js rename to packages/codemirror-vim/dev/vite.config.js diff --git a/dev/web-demo.html b/packages/codemirror-vim/dev/web-demo.html similarity index 98% rename from dev/web-demo.html rename to packages/codemirror-vim/dev/web-demo.html index 2ec99fd5..4829db9c 100644 --- a/dev/web-demo.html +++ b/packages/codemirror-vim/dev/web-demo.html @@ -611,7 +611,7 @@

tests
- cm5 demo + cm5 demo