@vemjs/plugin-api 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,50 @@
1
+ # @vemjs/plugin-api
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 0498765: chore: release infrastructure, package metadata, and documentation scaffolding
8
+
9
+ This changeset covers all release preparation work for the initial 0.1.0 publish:
10
+
11
+ **Package metadata** — Added `license`, `repository`, `keywords`, and `publishConfig` fields to
12
+ all four packages so they display correctly on npmjs.com with proper source links, license badges,
13
+ and searchable tags.
14
+
15
+ **CI/CD pipeline** — Rewrote `.github/workflows/ci.yml` and `release.yml`:
16
+
17
+ - `quality` job: build → test → lint (oxlint) → dead-code scan (knip) on every PR and push
18
+ - `publish` job: automatic `changeset publish` to npm on every merge to `main` via
19
+ `changesets/action@v1` using the `NPM_TOKEN` org secret
20
+
21
+ **Changesets** — Initialized `.changeset/` with a `config.json` configured for public access and
22
+ patch-level internal dependency updates, enabling a fully automated release flow.
23
+
24
+ **Tooling** — Added `knip.config.ts` (dead-code detection), `oxlintrc.json` (TypeScript-aware
25
+ lint rules), `.lintstagedrc.json` (auto-fix staged files on commit), and updated root
26
+ `package.json` scripts: `build`, `test`, `lint`, `knip`, `changeset`, `version-packages`,
27
+ `release`.
28
+
29
+ **Dependabot** — Configured weekly npm dependency scanning with dev/prod groups and
30
+ `@vectojs/*` major-version pin to avoid upstream breaking changes.
31
+
32
+ **Repository** — Updated root `README.md` with CI/npm/license badges and package table.
33
+ Updated `SECURITY.md` with all four `@vemjs/*` packages and coordinated-disclosure guidance.
34
+ Added GitHub topics (vim, editor, typescript, vectojs, canvas, modal-editing, lsp) and branch
35
+ protection requiring the `quality` status check before merging to `main`.
36
+
37
+ **Build hygiene** — Cleaned all `dist/` directories and rebuilt from source to ensure no
38
+ test artefacts are included in published tarballs. Verified `knip` reports zero issues.
39
+
40
+ - Updated dependencies [0498765]
41
+ - Updated dependencies [3fa4848]
42
+ - @vemjs/core@0.2.0
43
+
44
+ ## 0.1.0
45
+
46
+ ### Features
47
+
48
+ - `PluginRegistry` — plugin activation lifecycle manager
49
+ - `PluginContext` — plugin SDK: `registerKeybinding`, `registerCommand`, `onDidOpenBuffer`, `onDidChangeBuffer`, `onDidChangeMode`
50
+ - Chord-based custom keybinding with prefix-match buffering and Vim-native keystroke replay fallback
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # @vemjs/plugin-api
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@vemjs/plugin-api.svg)](https://www.npmjs.com/package/@vemjs/plugin-api)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
+
6
+ The official Plugin SDK and life-cycle registration layer for the **Vem Editor**. It allows third-party developers to register custom keybindings, hook into buffer updates, intercept mode transitions, and build complex chords or commands.
7
+
8
+ ## Features
9
+
10
+ - **Standardized Plugin Interface**: Clean activation/deactivation lifecycles matching the `@vemjs/core` environment.
11
+ - **Keybinding Registration**: Dynamically override editor shortcuts or register command chords in specific modes.
12
+ - **Event Observers**: Hook into buffer creation (`onDidOpenBuffer`), text mutations (`onDidChangeBuffer`), and mode changes (`onDidChangeMode`).
13
+ - **Command Management**: Define global editor commands that can be invoked via the Command Bar or mapped to shortcuts.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ bun add @vemjs/plugin-api
19
+ # or via npm
20
+ npm install @vemjs/plugin-api
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ Create and activate a plugin that adds a custom keystroke in NORMAL mode:
26
+
27
+ ```typescript
28
+ import { VemEditorState } from '@vemjs/core';
29
+ import { PluginRegistry, type VemPlugin } from '@vemjs/plugin-api';
30
+
31
+ const editor = new VemEditorState('initial text');
32
+ const registry = new PluginRegistry(editor);
33
+
34
+ // Define a plugin
35
+ const myPlugin: VemPlugin = {
36
+ name: 'my-custom-shortcuts',
37
+ version: '1.0.0',
38
+ activate(context) {
39
+ // Register a command
40
+ context.registerCommand('custom.hello', () => {
41
+ console.log('Hello from command!');
42
+ });
43
+
44
+ // Bind keys (e.g. pressing 'gh' in NORMAL mode fires command)
45
+ context.registerKeybinding('NORMAL', 'gh', 'custom.hello');
46
+ },
47
+ };
48
+
49
+ // Register & Activate
50
+ registry.register(myPlugin);
51
+
52
+ // Pressing keys in sequence triggers the command
53
+ editor.input('g');
54
+ editor.input('h'); // Console prints: "Hello from command!"
55
+ ```
56
+
57
+ ## API Reference
58
+
59
+ ### `VemPlugin`
60
+
61
+ Interface representing a loadable plugin.
62
+
63
+ - `name: string`: Unique package identifier.
64
+ - `version: string`: Semantic version string.
65
+ - `activate(context: PluginContext): void`: Called by the engine when loading the plugin.
66
+ - `deactivate?(): void`: Optional cleanup callback.
67
+
68
+ ### `PluginContext`
69
+
70
+ Provided to `activate(context)` to interact with the editor.
71
+
72
+ - `editorState: VemEditorState`: Access to core editor properties.
73
+ - `registerCommand(commandName: string, callback: () => void): void`: Registers a function callback globally.
74
+ - `registerKeybinding(mode: EditorMode, keys: string, commandName: string): void`: Binds a specific key combination (or chord prefix) in the specified mode to a registered command.
75
+ - `onDidOpenBuffer(cb: () => void): void`: Subscribes to buffer open events.
76
+ - `onDidChangeBuffer(cb: () => void): void`: Subscribes to buffer content mutation events.
77
+ - `onDidChangeMode(cb: (mode: EditorMode) => void): void`: Subscribes to mode change notifications.
78
+
79
+ ### `PluginRegistry`
80
+
81
+ Manager executing plugin installation.
82
+
83
+ - `constructor(editorState: VemEditorState)`: Creates the registry bound to an editor instance.
84
+ - `register(plugin: VemPlugin): void`: Registers context callbacks and triggers the plugin's `activate` method.
85
+ - `executeCommand(name: string): void`: Runs the callback associated with a command.
86
+
87
+ ---
88
+
89
+ ## Plugin Development Guide
90
+
91
+ ### Naming Conventions
92
+
93
+ For consistency in the ecosystem, plugins should be named using the prefix `vem-plugin-` when published as standalone packages, or `@vemjs/plugin-<name>` if they are officially maintained packages:
94
+
95
+ - Third-party: `vem-plugin-format-on-save`
96
+ - Official: `@vemjs/plugin-treesitter`
97
+
98
+ ### Complete Example: Format-on-Save
99
+
100
+ Here is a full plugin implementation that automatically trims trailing whitespace whenever a buffer changes (an automated linter/formatter behavior):
101
+
102
+ ```typescript
103
+ import { type VemPlugin } from '@vemjs/plugin-api';
104
+
105
+ export const FormatOnSavePlugin: VemPlugin = {
106
+ name: 'format-on-save',
107
+ version: '1.0.0',
108
+ activate(context) {
109
+ let isFormatting = false;
110
+
111
+ context.onDidChangeBuffer(() => {
112
+ // Avoid recursive change loops
113
+ if (isFormatting) return;
114
+
115
+ const editor = context.editorState;
116
+ const originalText = editor.getText();
117
+ const lines = originalText.split('\n');
118
+
119
+ // Trim trailing spaces
120
+ const formattedLines = lines.map((line) => line.trimEnd());
121
+ const formattedText = formattedLines.join('\n');
122
+
123
+ if (formattedText !== originalText) {
124
+ isFormatting = true;
125
+ // Access buffer to set raw text
126
+ editor.getBuffer().setText(formattedText);
127
+ isFormatting = false;
128
+ console.log('[FormatOnSave] Trimmed trailing whitespaces');
129
+ }
130
+ });
131
+ },
132
+ };
133
+ ```
134
+
135
+ ## Architecture
136
+
137
+ ```mermaid
138
+ graph LR
139
+ Engine[VemEditorState] <--> Context[PluginContext]
140
+ Context --> Registry[PluginRegistry]
141
+ Registry --> Plugin1[VemPlugin A]
142
+ Registry --> Plugin2[VemPlugin B]
143
+ ```
144
+
145
+ ## Contributing
146
+
147
+ Please review [CONTRIBUTING.md](../../CONTRIBUTING.md) for details on our workflow and engineering guidelines.
148
+
149
+ ## License
150
+
151
+ This package is licensed under the MIT License - see the LICENSE file for details.
@@ -0,0 +1,25 @@
1
+ import { VemEditorState } from '@vemjs/core';
2
+ import type { EditorMode } from '@vemjs/core';
3
+ export interface VemPlugin {
4
+ name: string;
5
+ version: string;
6
+ activate(context: PluginContext): void;
7
+ deactivate?(): void;
8
+ }
9
+ export interface PluginContext {
10
+ editorState: VemEditorState;
11
+ registerCommand(commandName: string, callback: () => void): void;
12
+ registerKeybinding(mode: EditorMode, keys: string, commandName: string): void;
13
+ onDidOpenBuffer(cb: () => void): void;
14
+ onDidChangeBuffer(cb: () => void): void;
15
+ onDidChangeMode(cb: (mode: EditorMode) => void): void;
16
+ }
17
+ export declare class PluginRegistry {
18
+ private plugins;
19
+ private commands;
20
+ private editorState;
21
+ constructor(editorState: VemEditorState);
22
+ register(plugin: VemPlugin): void;
23
+ executeCommand(name: string): void;
24
+ }
25
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAAC;IACvC,UAAU,CAAC,IAAI,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,cAAc,CAAC;IAC5B,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IACjE,kBAAkB,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9E,eAAe,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IACtC,iBAAiB,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IACxC,eAAe,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;CACvD;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,OAAO,CAAqC;IACpD,OAAO,CAAC,QAAQ,CAAsC;IACtD,OAAO,CAAC,WAAW,CAAiB;gBAExB,WAAW,EAAE,cAAc;IAIhC,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI;IAiBjC,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;CAQ1C"}
package/dist/index.js ADDED
@@ -0,0 +1,34 @@
1
+ import { VemEditorState } from '@vemjs/core';
2
+ export class PluginRegistry {
3
+ plugins = new Map();
4
+ commands = new Map();
5
+ editorState;
6
+ constructor(editorState) {
7
+ this.editorState = editorState;
8
+ }
9
+ register(plugin) {
10
+ const context = {
11
+ editorState: this.editorState,
12
+ registerCommand: (name, cb) => this.commands.set(name, cb),
13
+ registerKeybinding: (mode, keys, commandName) => {
14
+ this.editorState.registerKeybinding(mode, keys, commandName);
15
+ },
16
+ onDidOpenBuffer: (cb) => this.editorState.onDidOpenBuffer(cb),
17
+ onDidChangeBuffer: (cb) => this.editorState.onDidChangeBuffer(cb),
18
+ onDidChangeMode: (cb) => this.editorState.onDidChangeMode(cb),
19
+ };
20
+ plugin.activate(context);
21
+ this.plugins.set(plugin.name, plugin);
22
+ console.log(`Plugin [${plugin.name}] activated.`);
23
+ }
24
+ executeCommand(name) {
25
+ const cmd = this.commands.get(name);
26
+ if (cmd) {
27
+ cmd();
28
+ }
29
+ else {
30
+ console.warn(`Command [${name}] not found.`);
31
+ }
32
+ }
33
+ }
34
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAmB7C,MAAM,OAAO,cAAc;IACjB,OAAO,GAA2B,IAAI,GAAG,EAAE,CAAC;IAC5C,QAAQ,GAA4B,IAAI,GAAG,EAAE,CAAC;IAC9C,WAAW,CAAiB;IAEpC,YAAY,WAA2B;QACrC,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IACjC,CAAC;IAEM,QAAQ,CAAC,MAAiB;QAC/B,MAAM,OAAO,GAAkB;YAC7B,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,eAAe,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC;YAC1D,kBAAkB,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE;gBAC9C,IAAI,CAAC,WAAW,CAAC,kBAAkB,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;YAC/D,CAAC;YACD,eAAe,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,eAAe,CAAC,EAAE,CAAC;YAC7D,iBAAiB,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,iBAAiB,CAAC,EAAE,CAAC;YACjE,eAAe,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,eAAe,CAAC,EAAE,CAAC;SAC9D,CAAC;QAEF,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACzB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,WAAW,MAAM,CAAC,IAAI,cAAc,CAAC,CAAC;IACpD,CAAC;IAEM,cAAc,CAAC,IAAY;QAChC,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,GAAG,EAAE,CAAC;YACR,GAAG,EAAE,CAAC;QACR,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,IAAI,CAAC,YAAY,IAAI,cAAc,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC;CACF"}
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@vemjs/plugin-api",
3
+ "version": "0.1.0",
4
+ "description": "SDK interfaces for 3rd-party plugins on the Vem editor",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsc"
10
+ },
11
+ "dependencies": {
12
+ "@vemjs/core": "workspace:*"
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ }
17
+ }
@@ -0,0 +1,102 @@
1
+ import { describe, it, expect } from 'bun:test';
2
+ import { VemEditorState } from '@vemjs/core';
3
+ import { PluginRegistry, type VemPlugin } from './index';
4
+
5
+ describe('Plugin System', () => {
6
+ it('should register and activate a plugin', () => {
7
+ const editor = new VemEditorState('test content');
8
+ const registry = new PluginRegistry(editor);
9
+
10
+ let activated = false;
11
+ const testPlugin: VemPlugin = {
12
+ name: 'test-plugin',
13
+ version: '1.0.0',
14
+ activate(_context) {
15
+ activated = true;
16
+ },
17
+ };
18
+
19
+ registry.register(testPlugin);
20
+ expect(activated).toBe(true);
21
+ });
22
+
23
+ it('should execute registered custom commands and intercept custom keybindings', () => {
24
+ const editor = new VemEditorState('hello');
25
+ const registry = new PluginRegistry(editor);
26
+
27
+ let executed = false;
28
+ const testPlugin: VemPlugin = {
29
+ name: 'chord-plugin',
30
+ version: '1.0.0',
31
+ activate(context) {
32
+ context.registerCommand('testCmd', () => {
33
+ executed = true;
34
+ });
35
+ context.registerKeybinding('NORMAL', ' gw', 'testCmd');
36
+ },
37
+ };
38
+
39
+ registry.register(testPlugin);
40
+
41
+ editor.onExecutePluginCommand((name) => {
42
+ registry.executeCommand(name);
43
+ });
44
+
45
+ // Type chord: Space, g, w
46
+ editor.handleKey(' ');
47
+ editor.handleKey('g');
48
+ editor.handleKey('w');
49
+
50
+ expect(executed).toBe(true);
51
+ });
52
+
53
+ it('should support buffer change event listeners', () => {
54
+ const editor = new VemEditorState('line1');
55
+ const registry = new PluginRegistry(editor);
56
+
57
+ let changeCount = 0;
58
+ const testPlugin: VemPlugin = {
59
+ name: 'buffer-listener',
60
+ version: '1.0.0',
61
+ activate(context) {
62
+ context.onDidChangeBuffer(() => {
63
+ changeCount++;
64
+ });
65
+ },
66
+ };
67
+
68
+ registry.register(testPlugin);
69
+
70
+ // Modify the buffer
71
+ editor.handleKey('i');
72
+ editor.handleKey('a');
73
+ editor.handleKey('Escape');
74
+
75
+ expect(changeCount).toBe(1);
76
+ });
77
+
78
+ it('should support mode change event listeners', () => {
79
+ const editor = new VemEditorState('line1');
80
+ const registry = new PluginRegistry(editor);
81
+
82
+ let lastMode = 'NORMAL';
83
+ const testPlugin: VemPlugin = {
84
+ name: 'mode-listener',
85
+ version: '1.0.0',
86
+ activate(context) {
87
+ context.onDidChangeMode((mode) => {
88
+ lastMode = mode;
89
+ });
90
+ },
91
+ };
92
+
93
+ registry.register(testPlugin);
94
+
95
+ // Change mode
96
+ editor.handleKey('i');
97
+ expect(lastMode).toBe('INSERT');
98
+
99
+ editor.handleKey('Escape');
100
+ expect(lastMode).toBe('NORMAL');
101
+ });
102
+ });
package/src/index.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { VemEditorState } from '@vemjs/core';
2
+ import type { EditorMode } from '@vemjs/core';
3
+
4
+ export interface VemPlugin {
5
+ name: string;
6
+ version: string;
7
+ activate(context: PluginContext): void;
8
+ deactivate?(): void;
9
+ }
10
+
11
+ export interface PluginContext {
12
+ editorState: VemEditorState;
13
+ registerCommand(commandName: string, callback: () => void): void;
14
+ registerKeybinding(mode: EditorMode, keys: string, commandName: string): void;
15
+ onDidOpenBuffer(cb: () => void): void;
16
+ onDidChangeBuffer(cb: () => void): void;
17
+ onDidChangeMode(cb: (mode: EditorMode) => void): void;
18
+ }
19
+
20
+ export class PluginRegistry {
21
+ private plugins: Map<string, VemPlugin> = new Map();
22
+ private commands: Map<string, () => void> = new Map();
23
+ private editorState: VemEditorState;
24
+
25
+ constructor(editorState: VemEditorState) {
26
+ this.editorState = editorState;
27
+ }
28
+
29
+ public register(plugin: VemPlugin): void {
30
+ const context: PluginContext = {
31
+ editorState: this.editorState,
32
+ registerCommand: (name, cb) => this.commands.set(name, cb),
33
+ registerKeybinding: (mode, keys, commandName) => {
34
+ this.editorState.registerKeybinding(mode, keys, commandName);
35
+ },
36
+ onDidOpenBuffer: (cb) => this.editorState.onDidOpenBuffer(cb),
37
+ onDidChangeBuffer: (cb) => this.editorState.onDidChangeBuffer(cb),
38
+ onDidChangeMode: (cb) => this.editorState.onDidChangeMode(cb),
39
+ };
40
+
41
+ plugin.activate(context);
42
+ this.plugins.set(plugin.name, plugin);
43
+ console.log(`Plugin [${plugin.name}] activated.`);
44
+ }
45
+
46
+ public executeCommand(name: string): void {
47
+ const cmd = this.commands.get(name);
48
+ if (cmd) {
49
+ cmd();
50
+ } else {
51
+ console.warn(`Command [${name}] not found.`);
52
+ }
53
+ }
54
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": false,
5
+ "declaration": true,
6
+ "declarationMap": true,
7
+ "sourceMap": true,
8
+ "rootDir": "./src",
9
+ "outDir": "./dist"
10
+ },
11
+ "include": ["src/**/*"],
12
+ "exclude": ["src/**/*.test.ts"]
13
+ }