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
-
-
+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
+
+
+
+
+## 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