vim-prose 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.
Files changed (33) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +264 -0
  3. package/dist/extensions/vim/commands.d.ts +26 -0
  4. package/dist/extensions/vim/commands.js +231 -0
  5. package/dist/extensions/vim/commands.js.map +1 -0
  6. package/dist/extensions/vim/index.d.ts +3 -0
  7. package/dist/extensions/vim/index.js +2 -0
  8. package/dist/extensions/vim/index.js.map +1 -0
  9. package/dist/extensions/vim/keyHandler.d.ts +6 -0
  10. package/dist/extensions/vim/keyHandler.js +1400 -0
  11. package/dist/extensions/vim/keyHandler.js.map +1 -0
  12. package/dist/extensions/vim/motions.d.ts +80 -0
  13. package/dist/extensions/vim/motions.js +437 -0
  14. package/dist/extensions/vim/motions.js.map +1 -0
  15. package/dist/extensions/vim/operators.d.ts +36 -0
  16. package/dist/extensions/vim/operators.js +350 -0
  17. package/dist/extensions/vim/operators.js.map +1 -0
  18. package/dist/extensions/vim/state.d.ts +8 -0
  19. package/dist/extensions/vim/state.js +174 -0
  20. package/dist/extensions/vim/state.js.map +1 -0
  21. package/dist/extensions/vim/tiptap.d.ts +17 -0
  22. package/dist/extensions/vim/tiptap.js +49 -0
  23. package/dist/extensions/vim/types.d.ts +55 -0
  24. package/dist/extensions/vim/types.js +29 -0
  25. package/dist/extensions/vim/types.js.map +1 -0
  26. package/dist/extensions/vim/utils.d.ts +76 -0
  27. package/dist/extensions/vim/utils.js +224 -0
  28. package/dist/extensions/vim/utils.js.map +1 -0
  29. package/dist/extensions/vim/vim-mode.css +81 -0
  30. package/dist/extensions/vim/visual.d.ts +15 -0
  31. package/dist/extensions/vim/visual.js +58 -0
  32. package/dist/extensions/vim/visual.js.map +1 -0
  33. package/package.json +64 -0
@@ -0,0 +1,55 @@
1
+ import type { Node as ProseMirrorNode } from 'prosemirror-model';
2
+ export type Mode = 'normal' | 'insert' | 'visual' | 'visual-line';
3
+ export interface Register {
4
+ text: string;
5
+ linewise: boolean;
6
+ content: ProseMirrorNode[] | null;
7
+ }
8
+ export interface RepeatableAction {
9
+ type: 'command' | 'operator-linewise' | 'operator-motion' | 'operator-textobject' | 'insert-command';
10
+ key: string;
11
+ count: number;
12
+ operator?: 'd' | 'y' | 'c';
13
+ motion?: string;
14
+ findChar?: string;
15
+ findMotion?: 'f' | 'F' | 't' | 'T';
16
+ textObject?: {
17
+ type: 'i' | 'a';
18
+ object: string;
19
+ };
20
+ insertedText?: string;
21
+ }
22
+ export interface VimState {
23
+ mode: Mode;
24
+ count: number | null;
25
+ operator: 'd' | 'y' | 'c' | null;
26
+ findPending: boolean;
27
+ findMotion: 'f' | 'F' | 't' | 'T' | null;
28
+ ggPending: boolean;
29
+ visualAnchor: number | null;
30
+ visualHead: number | null;
31
+ register: Register;
32
+ goalColumn: number | null;
33
+ marks: Record<string, number>;
34
+ markPending: boolean;
35
+ gotoMarkPending: boolean;
36
+ searchTerm: string;
37
+ searchWholeWord: boolean;
38
+ searchActive: boolean;
39
+ searchQuery: string;
40
+ searchHighlightsVisible: boolean;
41
+ lastAction: RepeatableAction | null;
42
+ insertTextBuffer: string;
43
+ isTrackingInsert: boolean;
44
+ shiftRightPending: boolean;
45
+ shiftLeftPending: boolean;
46
+ zzPending: boolean;
47
+ statusMessage: string;
48
+ }
49
+ export interface VimEditorCommands {
50
+ undo(): boolean;
51
+ redo(): boolean;
52
+ indent?(): boolean;
53
+ outdent?(): boolean;
54
+ }
55
+ export declare function defaultVimState(): VimState;
@@ -0,0 +1,29 @@
1
+ export function defaultVimState() {
2
+ return {
3
+ mode: 'normal',
4
+ count: null,
5
+ operator: null,
6
+ findPending: false,
7
+ findMotion: null,
8
+ ggPending: false,
9
+ visualAnchor: null,
10
+ visualHead: null,
11
+ register: { text: '', linewise: false, content: null },
12
+ goalColumn: null,
13
+ marks: {},
14
+ markPending: false,
15
+ gotoMarkPending: false,
16
+ searchTerm: '',
17
+ searchWholeWord: false,
18
+ searchActive: false,
19
+ searchQuery: '',
20
+ searchHighlightsVisible: false,
21
+ lastAction: null,
22
+ insertTextBuffer: '',
23
+ isTrackingInsert: false,
24
+ shiftRightPending: false,
25
+ shiftLeftPending: false,
26
+ zzPending: false,
27
+ statusMessage: '',
28
+ };
29
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/extensions/vim/types.ts"],"names":[],"mappings":"AAmBA,MAAM,UAAU,eAAe;IAC7B,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,KAAK,EAAE,IAAI;QACX,aAAa,EAAE,IAAI;QACnB,QAAQ,EAAE,IAAI;QACd,WAAW,EAAE,KAAK;QAClB,UAAU,EAAE,IAAI;QAChB,SAAS,EAAE,KAAK;QAChB,YAAY,EAAE,IAAI;QAClB,QAAQ,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE;KACxC,CAAA;AACH,CAAC"}
@@ -0,0 +1,76 @@
1
+ import { EditorState } from 'prosemirror-state';
2
+ /**
3
+ * Get the start position of the current paragraph/line (inside the text node).
4
+ */
5
+ export declare function lineStart(state: EditorState): number;
6
+ /**
7
+ * Get the end position of the current paragraph/line (inside the text node).
8
+ */
9
+ export declare function lineEnd(state: EditorState): number;
10
+ /**
11
+ * Get line start/end for an arbitrary position.
12
+ */
13
+ export declare function lineStartAt(state: EditorState, pos: number): number;
14
+ export declare function lineEndAt(state: EditorState, pos: number): number;
15
+ /**
16
+ * Get the text content of the current line/paragraph.
17
+ */
18
+ export declare function lineText(state: EditorState): string;
19
+ /**
20
+ * Get the cursor offset within its current paragraph.
21
+ */
22
+ export declare function cursorOffsetInLine(state: EditorState): number;
23
+ /**
24
+ * Check if a character is a word character (\w).
25
+ */
26
+ export declare function isWordChar(ch: string): boolean;
27
+ /**
28
+ * Check if a character is whitespace.
29
+ */
30
+ export declare function isWhitespace(ch: string): boolean;
31
+ /**
32
+ * Get the first non-blank character position on the current line.
33
+ */
34
+ export declare function firstNonBlank(state: EditorState): number;
35
+ /**
36
+ * Clamp a position within the document.
37
+ */
38
+ export declare function clampPos(state: EditorState, pos: number): number;
39
+ /**
40
+ * Get character at a given document position (returns empty string if out of bounds).
41
+ */
42
+ export declare function charAt(state: EditorState, pos: number): string;
43
+ /**
44
+ * Find the paragraph node boundaries that contain the given position.
45
+ * Returns [nodeStart, nodeEnd] where nodeStart is the position before the node
46
+ * and nodeEnd is the position after the node (including the node itself).
47
+ */
48
+ export declare function paragraphBounds(state: EditorState, pos: number): {
49
+ from: number;
50
+ to: number;
51
+ };
52
+ /**
53
+ * Get the word under cursor at the given position.
54
+ */
55
+ export declare function wordUnderCursor(state: EditorState, pos: number): string | null;
56
+ /**
57
+ * Find all positions of a search term in the document.
58
+ */
59
+ export declare function findAllMatches(state: EditorState, term: string, wholeWord?: boolean): number[];
60
+ /**
61
+ * Find the next match position after fromPos, wrapping around.
62
+ */
63
+ export declare function findNextMatch(state: EditorState, term: string, fromPos: number, wholeWord?: boolean): number | null;
64
+ /**
65
+ * Find the previous match position before fromPos, wrapping around.
66
+ */
67
+ export declare function findPrevMatch(state: EditorState, term: string, fromPos: number, wholeWord?: boolean): number | null;
68
+ /**
69
+ * Find the "line-level" node boundaries for linewise operations (dd, yy, V, etc.).
70
+ * For text inside a list item with a single child, returns the list item bounds.
71
+ * Otherwise returns the textblock bounds (same as paragraphBounds).
72
+ */
73
+ export declare function lineBounds(state: EditorState, pos: number): {
74
+ from: number;
75
+ to: number;
76
+ };
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Get the start position of the current paragraph/line (inside the text node).
3
+ */
4
+ export function lineStart(state) {
5
+ const { $head } = state.selection;
6
+ return $head.start($head.depth);
7
+ }
8
+ /**
9
+ * Get the end position of the current paragraph/line (inside the text node).
10
+ */
11
+ export function lineEnd(state) {
12
+ const { $head } = state.selection;
13
+ return $head.end($head.depth);
14
+ }
15
+ /**
16
+ * Get line start/end for an arbitrary position.
17
+ */
18
+ export function lineStartAt(state, pos) {
19
+ const $pos = state.doc.resolve(pos);
20
+ if ($pos.depth === 0)
21
+ return pos;
22
+ return $pos.start($pos.depth);
23
+ }
24
+ export function lineEndAt(state, pos) {
25
+ const $pos = state.doc.resolve(pos);
26
+ if ($pos.depth === 0)
27
+ return pos;
28
+ return $pos.end($pos.depth);
29
+ }
30
+ /**
31
+ * Get the text content of the current line/paragraph.
32
+ */
33
+ export function lineText(state) {
34
+ const start = lineStart(state);
35
+ const end = lineEnd(state);
36
+ return state.doc.textBetween(start, end, '\n', '\n');
37
+ }
38
+ /**
39
+ * Get the cursor offset within its current paragraph.
40
+ */
41
+ export function cursorOffsetInLine(state) {
42
+ const { $head } = state.selection;
43
+ return $head.pos - $head.start($head.depth);
44
+ }
45
+ /**
46
+ * Check if a character is a word character (\w).
47
+ */
48
+ export function isWordChar(ch) {
49
+ return /\w/.test(ch);
50
+ }
51
+ /**
52
+ * Check if a character is whitespace.
53
+ */
54
+ export function isWhitespace(ch) {
55
+ return /\s/.test(ch);
56
+ }
57
+ /**
58
+ * Get the first non-blank character position on the current line.
59
+ */
60
+ export function firstNonBlank(state) {
61
+ const start = lineStart(state);
62
+ const text = lineText(state);
63
+ for (let i = 0; i < text.length; i++) {
64
+ if (!/\s/.test(text[i])) {
65
+ return start + i;
66
+ }
67
+ }
68
+ return start;
69
+ }
70
+ /**
71
+ * Clamp a position within the document.
72
+ */
73
+ export function clampPos(state, pos) {
74
+ return Math.max(0, Math.min(pos, state.doc.content.size));
75
+ }
76
+ /**
77
+ * Get character at a given document position (returns empty string if out of bounds).
78
+ */
79
+ export function charAt(state, pos) {
80
+ if (pos < 0 || pos >= state.doc.content.size)
81
+ return '';
82
+ try {
83
+ return state.doc.textBetween(pos, pos + 1, '\n', '\n');
84
+ }
85
+ catch {
86
+ return '';
87
+ }
88
+ }
89
+ /**
90
+ * Find the paragraph node boundaries that contain the given position.
91
+ * Returns [nodeStart, nodeEnd] where nodeStart is the position before the node
92
+ * and nodeEnd is the position after the node (including the node itself).
93
+ */
94
+ export function paragraphBounds(state, pos) {
95
+ let $pos = state.doc.resolve(pos);
96
+ if ($pos.depth === 0) {
97
+ // At document root (between nodes), resolve into nearest textblock
98
+ if (pos < state.doc.content.size) {
99
+ $pos = state.doc.resolve(pos + 1);
100
+ }
101
+ else if (pos > 0) {
102
+ $pos = state.doc.resolve(pos - 1);
103
+ }
104
+ else {
105
+ return { from: 0, to: state.doc.content.size };
106
+ }
107
+ }
108
+ const depth = $pos.depth;
109
+ const start = $pos.before(depth);
110
+ const end = $pos.after(depth);
111
+ return { from: start, to: end };
112
+ }
113
+ /**
114
+ * Get the word under cursor at the given position.
115
+ */
116
+ export function wordUnderCursor(state, pos) {
117
+ const $pos = state.doc.resolve(pos);
118
+ if ($pos.depth === 0)
119
+ return null;
120
+ const lineS = $pos.start($pos.depth);
121
+ const lineE = $pos.end($pos.depth);
122
+ const ch = charAt(state, pos);
123
+ if (!ch || !isWordChar(ch))
124
+ return null;
125
+ let from = pos;
126
+ let to = pos;
127
+ while (from > lineS && isWordChar(charAt(state, from - 1)))
128
+ from--;
129
+ while (to < lineE && isWordChar(charAt(state, to)))
130
+ to++;
131
+ if (from === to)
132
+ return null;
133
+ return state.doc.textBetween(from, to);
134
+ }
135
+ /**
136
+ * Find all positions of a search term in the document.
137
+ */
138
+ export function findAllMatches(state, term, wholeWord = false) {
139
+ if (!term)
140
+ return [];
141
+ const positions = [];
142
+ state.doc.descendants((node, pos) => {
143
+ if (node.isText && node.text) {
144
+ let idx = 0;
145
+ while (true) {
146
+ const found = node.text.indexOf(term, idx);
147
+ if (found === -1)
148
+ break;
149
+ if (wholeWord) {
150
+ const before = found > 0 ? node.text[found - 1] : '';
151
+ const after = found + term.length < node.text.length
152
+ ? node.text[found + term.length]
153
+ : '';
154
+ if ((before && isWordChar(before)) || (after && isWordChar(after))) {
155
+ idx = found + 1;
156
+ continue;
157
+ }
158
+ }
159
+ positions.push(pos + found);
160
+ idx = found + 1;
161
+ }
162
+ }
163
+ });
164
+ return positions.sort((a, b) => a - b);
165
+ }
166
+ /**
167
+ * Find the next match position after fromPos, wrapping around.
168
+ */
169
+ export function findNextMatch(state, term, fromPos, wholeWord = false) {
170
+ const matches = findAllMatches(state, term, wholeWord);
171
+ if (matches.length === 0)
172
+ return null;
173
+ for (const pos of matches) {
174
+ if (pos > fromPos)
175
+ return pos;
176
+ }
177
+ return matches[0]; // wrap around
178
+ }
179
+ /**
180
+ * Find the previous match position before fromPos, wrapping around.
181
+ */
182
+ export function findPrevMatch(state, term, fromPos, wholeWord = false) {
183
+ const matches = findAllMatches(state, term, wholeWord);
184
+ if (matches.length === 0)
185
+ return null;
186
+ for (let i = matches.length - 1; i >= 0; i--) {
187
+ if (matches[i] < fromPos)
188
+ return matches[i];
189
+ }
190
+ return matches[matches.length - 1]; // wrap around
191
+ }
192
+ /**
193
+ * Find the "line-level" node boundaries for linewise operations (dd, yy, V, etc.).
194
+ * For text inside a list item with a single child, returns the list item bounds.
195
+ * Otherwise returns the textblock bounds (same as paragraphBounds).
196
+ */
197
+ export function lineBounds(state, pos) {
198
+ let $pos = state.doc.resolve(pos);
199
+ if ($pos.depth === 0) {
200
+ if (pos < state.doc.content.size) {
201
+ $pos = state.doc.resolve(pos + 1);
202
+ }
203
+ else if (pos > 0) {
204
+ $pos = state.doc.resolve(pos - 1);
205
+ }
206
+ else {
207
+ return { from: 0, to: state.doc.content.size };
208
+ }
209
+ }
210
+ let depth = $pos.depth;
211
+ // Walk up through ancestors looking for a list item
212
+ for (let d = $pos.depth - 1; d >= 1; d--) {
213
+ const node = $pos.node(d);
214
+ const name = node.type.name;
215
+ if (name === 'listItem' || name === 'list_item') {
216
+ // Use list item bounds if it has only one child (the common case)
217
+ if (node.childCount === 1) {
218
+ depth = d;
219
+ }
220
+ break;
221
+ }
222
+ }
223
+ return { from: $pos.before(depth), to: $pos.after(depth) };
224
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../../src/extensions/vim/utils.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,81 @@
1
+ /* Vim mode visual indicators */
2
+
3
+ /* Hide native caret in non-insert modes */
4
+ .vim-mode-normal,
5
+ .vim-mode-visual,
6
+ .vim-mode-visual-line {
7
+ caret-color: transparent;
8
+ }
9
+
10
+ /* Show native caret in insert mode */
11
+ .vim-mode-insert {
12
+ caret-color: auto;
13
+ }
14
+
15
+ /* Block cursor (character under cursor in normal/visual modes) */
16
+ .vim-block-cursor {
17
+ background: rgba(55, 53, 47, 0.8);
18
+ color: #ffffff;
19
+ border-radius: 1px;
20
+ }
21
+
22
+ /* Block cursor at end of line (widget) */
23
+ .vim-block-cursor-eol {
24
+ background: rgba(55, 53, 47, 0.8);
25
+ border-radius: 1px;
26
+ }
27
+
28
+ /* Visual selection highlight */
29
+ .vim-visual-selection {
30
+ background: rgba(59, 130, 246, 0.3);
31
+ }
32
+
33
+ /* Suppress native selection in visual modes (we use decorations instead) */
34
+ .vim-mode-visual ::selection,
35
+ .vim-mode-visual-line ::selection {
36
+ background: transparent;
37
+ }
38
+
39
+ /* Search bar */
40
+ .vim-search-bar {
41
+ display: flex;
42
+ align-items: center;
43
+ padding: 4px 8px;
44
+ background: #f8f8f8;
45
+ border-top: 1px solid #e0e0e0;
46
+ font-family: 'SF Mono', 'Fira Code', 'Fira Mono', 'Roboto Mono', monospace;
47
+ font-size: 14px;
48
+ min-height: 28px;
49
+ }
50
+
51
+ .vim-search-prefix {
52
+ color: #666;
53
+ margin-right: 2px;
54
+ }
55
+
56
+ .vim-search-input {
57
+ color: #333;
58
+ white-space: pre;
59
+ }
60
+
61
+ .vim-search-cursor {
62
+ color: #333;
63
+ animation: vim-cursor-blink 1s step-end infinite;
64
+ }
65
+
66
+ @keyframes vim-cursor-blink {
67
+ 50% {
68
+ opacity: 0;
69
+ }
70
+ }
71
+
72
+ /* Search match highlights */
73
+ .vim-search-match {
74
+ background: rgba(255, 200, 0, 0.4);
75
+ border-radius: 1px;
76
+ }
77
+
78
+ .vim-search-match-current {
79
+ background: rgba(255, 140, 0, 0.5);
80
+ border-radius: 1px;
81
+ }
@@ -0,0 +1,15 @@
1
+ import { EditorState, Transaction } from 'prosemirror-state';
2
+ import { VimState } from './types';
3
+ /**
4
+ * Update the visual selection based on anchor and new head position.
5
+ */
6
+ export declare function updateVisualSelection(state: EditorState, tr: Transaction, vimState: VimState, newHead: number): Transaction;
7
+ /**
8
+ * Get the effective selection range for visual mode operations (y, d, c, x).
9
+ * Returns the from/to positions and whether it's a linewise operation.
10
+ */
11
+ export declare function getVisualRange(state: EditorState, vimState: VimState): {
12
+ from: number;
13
+ to: number;
14
+ linewise: boolean;
15
+ } | null;
@@ -0,0 +1,58 @@
1
+ import { TextSelection } from 'prosemirror-state';
2
+ import { lineBounds } from './utils';
3
+ /**
4
+ * Update the visual selection based on anchor and new head position.
5
+ */
6
+ export function updateVisualSelection(state, tr, vimState, newHead) {
7
+ if (vimState.visualAnchor === null)
8
+ return tr;
9
+ const anchor = vimState.visualAnchor;
10
+ const head = newHead;
11
+ if (vimState.mode === 'visual') {
12
+ // Characterwise visual: select from anchor to head
13
+ // In vim, visual selection is inclusive. We extend head by 1 when head >= anchor
14
+ let selFrom, selTo;
15
+ if (head >= anchor) {
16
+ selFrom = anchor;
17
+ selTo = Math.min(head + 1, state.doc.content.size);
18
+ }
19
+ else {
20
+ selFrom = head;
21
+ selTo = Math.min(anchor + 1, state.doc.content.size);
22
+ }
23
+ try {
24
+ tr.setSelection(TextSelection.create(tr.doc, selFrom, selTo));
25
+ }
26
+ catch {
27
+ // leave as-is
28
+ }
29
+ }
30
+ else if (vimState.mode === 'visual-line') {
31
+ // Linewise visual: select full lines from anchor to head
32
+ const anchorBounds = lineBounds(state, anchor);
33
+ const headBounds = lineBounds(state, newHead);
34
+ const from = Math.min(anchorBounds.from, headBounds.from);
35
+ const to = Math.max(anchorBounds.to, headBounds.to);
36
+ try {
37
+ tr.setSelection(TextSelection.create(tr.doc, from, to));
38
+ }
39
+ catch {
40
+ // leave as-is
41
+ }
42
+ }
43
+ tr.scrollIntoView();
44
+ return tr;
45
+ }
46
+ /**
47
+ * Get the effective selection range for visual mode operations (y, d, c, x).
48
+ * Returns the from/to positions and whether it's a linewise operation.
49
+ */
50
+ export function getVisualRange(state, vimState) {
51
+ if (vimState.visualAnchor === null)
52
+ return null;
53
+ const { from, to } = state.selection;
54
+ if (vimState.mode === 'visual-line') {
55
+ return { from, to, linewise: true };
56
+ }
57
+ return { from, to, linewise: false };
58
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visual.js","sourceRoot":"","sources":["../../../src/extensions/vim/visual.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAE1E,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAE7C;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CACnC,EAAe,EACf,KAAkB,EAClB,EAAY,EACZ,OAAe;IAEf,IAAI,EAAE,CAAC,YAAY,KAAK,IAAI;QAAE,OAAO,EAAE,CAAA;IAEvC,MAAM,MAAM,GAAG,EAAE,CAAC,YAAY,CAAA;IAC9B,6DAA6D;IAC7D,8FAA8F;IAC9F,mDAAmD;IACnD,IAAI,IAAY,CAAA;IAChB,IAAI,EAAU,CAAA;IACd,IAAI,OAAO,IAAI,MAAM,EAAE,CAAC;QACtB,IAAI,GAAG,MAAM,CAAA;QACb,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IACpD,CAAC;SAAM,CAAC;QACN,IAAI,GAAG,OAAO,CAAA;QACd,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IACnD,CAAC;IAED,MAAM,SAAS,GAAG,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;IACxD,OAAO,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC,CAAA;AACnC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,yBAAyB,CACvC,EAAe,EACf,KAAkB,EAClB,EAAY,EACZ,OAAe;IAEf,IAAI,EAAE,CAAC,YAAY,KAAK,IAAI;QAAE,OAAO,EAAE,CAAA;IAEvC,MAAM,aAAa,GAAG,WAAW,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,CAAA;IACzD,MAAM,WAAW,GAAG,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;IAE/C,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,WAAW,CAAC,CAAA;IACzD,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,WAAW,CAAC,CAAA;IAEvD,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,EAAE,YAAY,CAAC,CAAA;IAC7C,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,CAAA;IAEzC,IAAI,CAAC,SAAS,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAA;IAErC,wEAAwE;IACxE,kEAAkE;IAClE,oEAAoE;IACpE,gEAAgE;IAChE,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAA;IAC5B,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAA;IAEtB,MAAM,SAAS,GAAG,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;IACxD,OAAO,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC,CAAA;AACnC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,KAAkB;IAC/C,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,KAAK,CAAC,SAAS,CAAA;IACpC,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,CAAA;AACrB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "vim-prose",
3
+ "author": "KJ Shanks",
4
+ "version": "0.1.0",
5
+ "description": "Vim keybinding extension for Tiptap v3 / ProseMirror",
6
+ "type": "module",
7
+ "main": "dist/extensions/vim/index.js",
8
+ "types": "dist/extensions/vim/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/extensions/vim/index.js",
12
+ "types": "./dist/extensions/vim/index.d.ts"
13
+ },
14
+ "./tiptap": {
15
+ "import": "./dist/extensions/vim/tiptap.js",
16
+ "types": "./dist/extensions/vim/tiptap.d.ts"
17
+ },
18
+ "./style.css": "./dist/extensions/vim/vim-mode.css"
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc && cp src/extensions/vim/vim-mode.css dist/extensions/vim/vim-mode.css",
25
+ "typecheck": "tsc --noEmit",
26
+ "dev": "vite demo",
27
+ "prepublishOnly": "npm run build"
28
+ },
29
+ "peerDependencies": {
30
+ "@tiptap/core": ">=3",
31
+ "prosemirror-model": ">=1",
32
+ "prosemirror-state": ">=1",
33
+ "prosemirror-view": ">=1"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "@tiptap/core": {
37
+ "optional": true
38
+ }
39
+ },
40
+ "devDependencies": {
41
+ "@tiptap/core": "^3.20.3",
42
+ "@tiptap/pm": "^3.20.3",
43
+ "@tiptap/starter-kit": "^3.20.4",
44
+ "prosemirror-model": "^1.25.4",
45
+ "prosemirror-state": "^1.4.4",
46
+ "prosemirror-transform": "^1.11.0",
47
+ "prosemirror-view": "^1.41.6",
48
+ "typescript": "^5.9.3",
49
+ "vite": "^8.0.0"
50
+ },
51
+ "keywords": [
52
+ "vim",
53
+ "tiptap",
54
+ "prosemirror",
55
+ "editor",
56
+ "keybindings",
57
+ "modal"
58
+ ],
59
+ "license": "MIT",
60
+ "repository": {
61
+ "type": "git",
62
+ "url": "https://github.com/Kyle-Shanks/vim-prose"
63
+ }
64
+ }