@oh-my-pi/pi-tui 3.15.1 → 3.20.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-tui",
3
- "version": "3.15.1",
3
+ "version": "3.20.0",
4
4
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -22,7 +22,10 @@ import {
22
22
  isEnter,
23
23
  isEscape,
24
24
  isHome,
25
+ isShiftBackspace,
26
+ isShiftDelete,
25
27
  isShiftEnter,
28
+ isShiftSpace,
26
29
  isTab,
27
30
  } from "../keys";
28
31
  import type { SymbolTheme } from "../symbols";
@@ -32,6 +35,186 @@ import { SelectList, type SelectListTheme } from "./select-list";
32
35
 
33
36
  const segmenter = getSegmenter();
34
37
 
38
+ /**
39
+ * Represents a chunk of text for word-wrap layout.
40
+ * Tracks both the text content and its position in the original line.
41
+ */
42
+ interface TextChunk {
43
+ text: string;
44
+ startIndex: number;
45
+ endIndex: number;
46
+ }
47
+
48
+ /**
49
+ * Split a line into word-wrapped chunks.
50
+ * Wraps at word boundaries when possible, falling back to character-level
51
+ * wrapping for words longer than the available width.
52
+ *
53
+ * @param line - The text line to wrap
54
+ * @param maxWidth - Maximum visible width per chunk
55
+ * @returns Array of chunks with text and position information
56
+ */
57
+ function wordWrapLine(line: string, maxWidth: number): TextChunk[] {
58
+ if (!line || maxWidth <= 0) {
59
+ return [{ text: "", startIndex: 0, endIndex: 0 }];
60
+ }
61
+
62
+ const lineWidth = visibleWidth(line);
63
+ if (lineWidth <= maxWidth) {
64
+ return [{ text: line, startIndex: 0, endIndex: line.length }];
65
+ }
66
+
67
+ const chunks: TextChunk[] = [];
68
+
69
+ // Split into tokens (words and whitespace runs)
70
+ const tokens: { text: string; startIndex: number; endIndex: number; isWhitespace: boolean }[] = [];
71
+ let currentToken = "";
72
+ let tokenStart = 0;
73
+ let inWhitespace = false;
74
+ let charIndex = 0;
75
+
76
+ for (const seg of segmenter.segment(line)) {
77
+ const grapheme = seg.segment;
78
+ const graphemeIsWhitespace = isWhitespaceChar(grapheme);
79
+
80
+ if (currentToken === "") {
81
+ inWhitespace = graphemeIsWhitespace;
82
+ tokenStart = charIndex;
83
+ } else if (graphemeIsWhitespace !== inWhitespace) {
84
+ // Token type changed - save current token
85
+ tokens.push({
86
+ text: currentToken,
87
+ startIndex: tokenStart,
88
+ endIndex: charIndex,
89
+ isWhitespace: inWhitespace,
90
+ });
91
+ currentToken = "";
92
+ tokenStart = charIndex;
93
+ inWhitespace = graphemeIsWhitespace;
94
+ }
95
+
96
+ currentToken += grapheme;
97
+ charIndex += grapheme.length;
98
+ }
99
+
100
+ // Push final token
101
+ if (currentToken) {
102
+ tokens.push({
103
+ text: currentToken,
104
+ startIndex: tokenStart,
105
+ endIndex: charIndex,
106
+ isWhitespace: inWhitespace,
107
+ });
108
+ }
109
+
110
+ // Build chunks using word wrapping
111
+ let currentChunk = "";
112
+ let currentWidth = 0;
113
+ let chunkStartIndex = 0;
114
+ let atLineStart = true; // Track if we're at the start of a line (for skipping whitespace)
115
+
116
+ for (const token of tokens) {
117
+ const tokenWidth = visibleWidth(token.text);
118
+
119
+ // Skip leading whitespace at line start
120
+ if (atLineStart && token.isWhitespace) {
121
+ chunkStartIndex = token.endIndex;
122
+ continue;
123
+ }
124
+ atLineStart = false;
125
+
126
+ // If this single token is wider than maxWidth, we need to break it
127
+ if (tokenWidth > maxWidth) {
128
+ // First, push any accumulated chunk
129
+ if (currentChunk) {
130
+ chunks.push({
131
+ text: currentChunk,
132
+ startIndex: chunkStartIndex,
133
+ endIndex: token.startIndex,
134
+ });
135
+ currentChunk = "";
136
+ currentWidth = 0;
137
+ chunkStartIndex = token.startIndex;
138
+ }
139
+
140
+ // Break the long token by grapheme
141
+ let tokenChunk = "";
142
+ let tokenChunkWidth = 0;
143
+ let tokenChunkStart = token.startIndex;
144
+ let tokenCharIndex = token.startIndex;
145
+
146
+ for (const seg of segmenter.segment(token.text)) {
147
+ const grapheme = seg.segment;
148
+ const graphemeWidth = visibleWidth(grapheme);
149
+
150
+ if (tokenChunkWidth + graphemeWidth > maxWidth && tokenChunk) {
151
+ chunks.push({
152
+ text: tokenChunk,
153
+ startIndex: tokenChunkStart,
154
+ endIndex: tokenCharIndex,
155
+ });
156
+ tokenChunk = grapheme;
157
+ tokenChunkWidth = graphemeWidth;
158
+ tokenChunkStart = tokenCharIndex;
159
+ } else {
160
+ tokenChunk += grapheme;
161
+ tokenChunkWidth += graphemeWidth;
162
+ }
163
+ tokenCharIndex += grapheme.length;
164
+ }
165
+
166
+ // Keep remainder as start of next chunk
167
+ if (tokenChunk) {
168
+ currentChunk = tokenChunk;
169
+ currentWidth = tokenChunkWidth;
170
+ chunkStartIndex = tokenChunkStart;
171
+ }
172
+ continue;
173
+ }
174
+
175
+ // Check if adding this token would exceed width
176
+ if (currentWidth + tokenWidth > maxWidth) {
177
+ // Push current chunk (trimming trailing whitespace for display)
178
+ const trimmedChunk = currentChunk.trimEnd();
179
+ if (trimmedChunk || chunks.length === 0) {
180
+ chunks.push({
181
+ text: trimmedChunk,
182
+ startIndex: chunkStartIndex,
183
+ endIndex: chunkStartIndex + currentChunk.length,
184
+ });
185
+ }
186
+
187
+ // Start new line - skip leading whitespace
188
+ atLineStart = true;
189
+ if (token.isWhitespace) {
190
+ currentChunk = "";
191
+ currentWidth = 0;
192
+ chunkStartIndex = token.endIndex;
193
+ } else {
194
+ currentChunk = token.text;
195
+ currentWidth = tokenWidth;
196
+ chunkStartIndex = token.startIndex;
197
+ atLineStart = false;
198
+ }
199
+ } else {
200
+ // Add token to current chunk
201
+ currentChunk += token.text;
202
+ currentWidth += tokenWidth;
203
+ }
204
+ }
205
+
206
+ // Push final chunk
207
+ if (currentChunk) {
208
+ chunks.push({
209
+ text: currentChunk,
210
+ startIndex: chunkStartIndex,
211
+ endIndex: line.length,
212
+ });
213
+ }
214
+
215
+ return chunks.length > 0 ? chunks : [{ text: "", startIndex: 0, endIndex: 0 }];
216
+ }
217
+
35
218
  interface EditorState {
36
219
  lines: string[];
37
220
  cursorLine: number;
@@ -92,6 +275,7 @@ export class Editor implements Component {
92
275
  private historyIndex: number = -1; // -1 = not browsing, 0 = most recent, 1 = older, etc.
93
276
 
94
277
  public onSubmit?: (text: string) => void;
278
+ public onAltEnter?: (text: string) => void;
95
279
  public onChange?: (text: string) => void;
96
280
  public disableSubmit: boolean = false;
97
281
 
@@ -493,13 +677,20 @@ export class Editor implements Component {
493
677
  else if (isCtrlE(data)) {
494
678
  this.moveToLineEnd();
495
679
  }
680
+ // Alt+Enter - special handler if callback exists, otherwise new line
681
+ else if (isAltEnter(data)) {
682
+ if (this.onAltEnter) {
683
+ this.onAltEnter(this.getText());
684
+ } else {
685
+ this.addNewLine();
686
+ }
687
+ }
496
688
  // New line shortcuts (but not plain LF/CR which should be submit)
497
689
  else if (
498
690
  (data.charCodeAt(0) === 10 && data.length > 1) || // Ctrl+Enter with modifiers
499
691
  data === "\x1b\r" || // Option+Enter in some terminals (legacy)
500
692
  data === "\x1b[13;2~" || // Shift+Enter in some terminals (legacy format)
501
693
  isShiftEnter(data) || // Shift+Enter (Kitty protocol, handles lock bits)
502
- isAltEnter(data) || // Alt+Enter (Kitty protocol, handles lock bits)
503
694
  (data.length > 1 && data.includes("\x1b") && data.includes("\r")) ||
504
695
  (data === "\n" && data.length === 1) || // Shift+Enter from iTerm2 mapping
505
696
  data === "\\\r" // Shift+Enter in VS Code terminal
@@ -543,8 +734,8 @@ export class Editor implements Component {
543
734
  this.onSubmit(result);
544
735
  }
545
736
  }
546
- // Backspace
547
- else if (isBackspace(data)) {
737
+ // Backspace (including Shift+Backspace)
738
+ else if (isBackspace(data) || isShiftBackspace(data)) {
548
739
  this.handleBackspace();
549
740
  }
550
741
  // Line navigation shortcuts (Home/End keys)
@@ -553,8 +744,8 @@ export class Editor implements Component {
553
744
  } else if (isEnd(data)) {
554
745
  this.moveToLineEnd();
555
746
  }
556
- // Forward delete (Fn+Backspace or Delete key)
557
- else if (isDelete(data)) {
747
+ // Forward delete (Fn+Backspace or Delete key, including Shift+Delete)
748
+ else if (isDelete(data) || isShiftDelete(data)) {
558
749
  this.handleForwardDelete();
559
750
  }
560
751
  // Word navigation (Option/Alt + Arrow or Ctrl + Arrow)
@@ -589,8 +780,8 @@ export class Editor implements Component {
589
780
  // Left
590
781
  this.moveCursor(0, -1);
591
782
  }
592
- // Shift+Space via Kitty protocol (sends \x1b[32;2u instead of plain space)
593
- else if (data === "\x1b[32;2u" || data.match(/^\x1b\[32;\d+u$/)) {
783
+ // Shift+Space - insert regular space (Kitty protocol sends escape sequence)
784
+ else if (isShiftSpace(data)) {
594
785
  this.insertCharacter(" ");
595
786
  }
596
787
  // Regular characters (printable characters and unicode, but not control characters)
@@ -633,42 +824,8 @@ export class Editor implements Component {
633
824
  });
634
825
  }
635
826
  } else {
636
- // Line needs wrapping - use grapheme-aware chunking
637
- const chunks: { text: string; startIndex: number; endIndex: number }[] = [];
638
- let currentChunk = "";
639
- let currentWidth = 0;
640
- let chunkStartIndex = 0;
641
- let currentIndex = 0;
642
-
643
- for (const seg of segmenter.segment(line)) {
644
- const grapheme = seg.segment;
645
- const graphemeWidth = visibleWidth(grapheme);
646
-
647
- if (currentWidth + graphemeWidth > contentWidth && currentChunk !== "") {
648
- // Start a new chunk
649
- chunks.push({
650
- text: currentChunk,
651
- startIndex: chunkStartIndex,
652
- endIndex: currentIndex,
653
- });
654
- currentChunk = grapheme;
655
- currentWidth = graphemeWidth;
656
- chunkStartIndex = currentIndex;
657
- } else {
658
- currentChunk += grapheme;
659
- currentWidth += graphemeWidth;
660
- }
661
- currentIndex += grapheme.length;
662
- }
663
-
664
- // Push the last chunk
665
- if (currentChunk !== "") {
666
- chunks.push({
667
- text: currentChunk,
668
- startIndex: chunkStartIndex,
669
- endIndex: currentIndex,
670
- });
671
- }
827
+ // Line needs wrapping - use word-aware wrapping
828
+ const chunks = wordWrapLine(line, contentWidth);
672
829
 
673
830
  for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
674
831
  const chunk = chunks[chunkIndex];
@@ -676,17 +833,37 @@ export class Editor implements Component {
676
833
 
677
834
  const cursorPos = this.state.cursorCol;
678
835
  const isLastChunk = chunkIndex === chunks.length - 1;
679
- // For non-last chunks, cursor at endIndex belongs to the next chunk
680
- const hasCursorInChunk =
681
- isCurrentLine &&
682
- cursorPos >= chunk.startIndex &&
683
- (isLastChunk ? cursorPos <= chunk.endIndex : cursorPos < chunk.endIndex);
836
+
837
+ // Determine if cursor is in this chunk
838
+ // For word-wrapped chunks, we need to handle the case where
839
+ // cursor might be in trimmed whitespace at end of chunk
840
+ let hasCursorInChunk = false;
841
+ let adjustedCursorPos = 0;
842
+
843
+ if (isCurrentLine) {
844
+ if (isLastChunk) {
845
+ // Last chunk: cursor belongs here if >= startIndex
846
+ hasCursorInChunk = cursorPos >= chunk.startIndex;
847
+ adjustedCursorPos = cursorPos - chunk.startIndex;
848
+ } else {
849
+ // Non-last chunk: cursor belongs here if in range [startIndex, endIndex)
850
+ // But we need to handle the visual position in the trimmed text
851
+ hasCursorInChunk = cursorPos >= chunk.startIndex && cursorPos < chunk.endIndex;
852
+ if (hasCursorInChunk) {
853
+ adjustedCursorPos = cursorPos - chunk.startIndex;
854
+ // Clamp to text length (in case cursor was in trimmed whitespace)
855
+ if (adjustedCursorPos > chunk.text.length) {
856
+ adjustedCursorPos = chunk.text.length;
857
+ }
858
+ }
859
+ }
860
+ }
684
861
 
685
862
  if (hasCursorInChunk) {
686
863
  layoutLines.push({
687
864
  text: chunk.text,
688
865
  hasCursor: true,
689
- cursorPos: cursorPos - chunk.startIndex,
866
+ cursorPos: adjustedCursorPos,
690
867
  });
691
868
  } else {
692
869
  layoutLines.push({
@@ -767,7 +944,7 @@ export class Editor implements Component {
767
944
  }
768
945
  }
769
946
  // Also auto-trigger when typing letters in a slash command context
770
- else if (/[a-zA-Z0-9]/.test(char)) {
947
+ else if (/[a-zA-Z0-9.\-_]/.test(char)) {
771
948
  const currentLine = this.state.lines[this.state.cursorLine] || "";
772
949
  const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
773
950
  // Check if we're in a slash command (with or without space for arguments)
@@ -1103,36 +1280,13 @@ export class Editor implements Component {
1103
1280
  } else if (lineVisWidth <= width) {
1104
1281
  visualLines.push({ logicalLine: i, startCol: 0, length: line.length });
1105
1282
  } else {
1106
- // Line needs wrapping - use grapheme-aware chunking
1107
- let currentWidth = 0;
1108
- let chunkStartIndex = 0;
1109
- let currentIndex = 0;
1110
-
1111
- for (const seg of segmenter.segment(line)) {
1112
- const grapheme = seg.segment;
1113
- const graphemeWidth = visibleWidth(grapheme);
1114
-
1115
- if (currentWidth + graphemeWidth > width && currentIndex > chunkStartIndex) {
1116
- // Start a new chunk
1117
- visualLines.push({
1118
- logicalLine: i,
1119
- startCol: chunkStartIndex,
1120
- length: currentIndex - chunkStartIndex,
1121
- });
1122
- chunkStartIndex = currentIndex;
1123
- currentWidth = graphemeWidth;
1124
- } else {
1125
- currentWidth += graphemeWidth;
1126
- }
1127
- currentIndex += grapheme.length;
1128
- }
1129
-
1130
- // Push the last chunk
1131
- if (currentIndex > chunkStartIndex) {
1283
+ // Line needs wrapping - use word-aware wrapping
1284
+ const chunks = wordWrapLine(line, width);
1285
+ for (const chunk of chunks) {
1132
1286
  visualLines.push({
1133
1287
  logicalLine: i,
1134
- startCol: chunkStartIndex,
1135
- length: currentIndex - chunkStartIndex,
1288
+ startCol: chunk.startIndex,
1289
+ length: chunk.endIndex - chunk.startIndex,
1136
1290
  });
1137
1291
  }
1138
1292
  }
@@ -1,6 +1,6 @@
1
1
  import { isArrowDown, isArrowUp, isCtrlC, isEnter, isEscape } from "../keys";
2
2
  import type { Component } from "../tui";
3
- import { truncateToWidth, visibleWidth } from "../utils";
3
+ import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "../utils";
4
4
 
5
5
  export interface SettingItem {
6
6
  /** Unique identifier for this setting */
@@ -123,7 +123,10 @@ export class SettingsList implements Component {
123
123
  const selectedItem = this.items[this.selectedIndex];
124
124
  if (selectedItem?.description) {
125
125
  lines.push("");
126
- lines.push(this.theme.description(` ${truncateToWidth(selectedItem.description, width - 4, "")}`));
126
+ const wrappedDesc = wrapTextWithAnsi(selectedItem.description, width - 4);
127
+ for (const line of wrappedDesc) {
128
+ lines.push(this.theme.description(` ${line}`));
129
+ }
127
130
  }
128
131
 
129
132
  // Add hint
package/src/index.ts CHANGED
@@ -21,6 +21,15 @@ export { Spacer } from "./components/spacer";
21
21
  export { type Tab, TabBar, type TabBarTheme } from "./components/tab-bar";
22
22
  export { Text } from "./components/text";
23
23
  export { TruncatedText } from "./components/truncated-text";
24
+ // Keybindings
25
+ export {
26
+ DEFAULT_EDITOR_KEYBINDINGS,
27
+ type EditorAction,
28
+ type EditorKeybindingsConfig,
29
+ EditorKeybindingsManager,
30
+ getEditorKeybindings,
31
+ setEditorKeybindings,
32
+ } from "./keybindings";
24
33
  // Kitty keyboard protocol helpers
25
34
  export {
26
35
  isAltBackspace,
@@ -32,6 +41,7 @@ export {
32
41
  isArrowRight,
33
42
  isArrowUp,
34
43
  isBackspace,
44
+ isCapsLock,
35
45
  isCtrlA,
36
46
  isCtrlC,
37
47
  isCtrlD,
@@ -47,19 +57,26 @@ export {
47
57
  isCtrlU,
48
58
  isCtrlV,
49
59
  isCtrlW,
60
+ isCtrlY,
50
61
  isCtrlZ,
51
62
  isDelete,
52
63
  isEnd,
53
64
  isEnter,
54
65
  isEscape,
55
66
  isHome,
67
+ isShiftBackspace,
56
68
  isShiftCtrlD,
57
69
  isShiftCtrlO,
58
70
  isShiftCtrlP,
71
+ isShiftDelete,
59
72
  isShiftEnter,
73
+ isShiftSpace,
60
74
  isShiftTab,
61
75
  isTab,
62
- Keys,
76
+ Key,
77
+ type KeyId,
78
+ matchesKey,
79
+ parseKey,
63
80
  } from "./keys";
64
81
  export type { BoxSymbols, SymbolTheme } from "./symbols";
65
82
  // Terminal interface and implementations
@@ -0,0 +1,143 @@
1
+ import { type KeyId, matchesKey } from "./keys";
2
+
3
+ /**
4
+ * Editor actions that can be bound to keys.
5
+ */
6
+ export type EditorAction =
7
+ // Cursor movement
8
+ | "cursorUp"
9
+ | "cursorDown"
10
+ | "cursorLeft"
11
+ | "cursorRight"
12
+ | "cursorWordLeft"
13
+ | "cursorWordRight"
14
+ | "cursorLineStart"
15
+ | "cursorLineEnd"
16
+ // Deletion
17
+ | "deleteCharBackward"
18
+ | "deleteCharForward"
19
+ | "deleteWordBackward"
20
+ | "deleteToLineStart"
21
+ | "deleteToLineEnd"
22
+ // Text input
23
+ | "newLine"
24
+ | "submit"
25
+ | "tab"
26
+ // Selection/autocomplete
27
+ | "selectUp"
28
+ | "selectDown"
29
+ | "selectConfirm"
30
+ | "selectCancel"
31
+ // Clipboard
32
+ | "copy";
33
+
34
+ // Re-export KeyId from keys.ts
35
+ export type { KeyId };
36
+
37
+ /**
38
+ * Editor keybindings configuration.
39
+ */
40
+ export type EditorKeybindingsConfig = {
41
+ [K in EditorAction]?: KeyId | KeyId[];
42
+ };
43
+
44
+ /**
45
+ * Default editor keybindings.
46
+ */
47
+ export const DEFAULT_EDITOR_KEYBINDINGS: Required<EditorKeybindingsConfig> = {
48
+ // Cursor movement
49
+ cursorUp: "up",
50
+ cursorDown: "down",
51
+ cursorLeft: "left",
52
+ cursorRight: "right",
53
+ cursorWordLeft: ["alt+left", "ctrl+left"],
54
+ cursorWordRight: ["alt+right", "ctrl+right"],
55
+ cursorLineStart: ["home", "ctrl+a"],
56
+ cursorLineEnd: ["end", "ctrl+e"],
57
+ // Deletion
58
+ deleteCharBackward: "backspace",
59
+ deleteCharForward: "delete",
60
+ deleteWordBackward: ["ctrl+w", "alt+backspace"],
61
+ deleteToLineStart: "ctrl+u",
62
+ deleteToLineEnd: "ctrl+k",
63
+ // Text input
64
+ newLine: ["shift+enter", "alt+enter"],
65
+ submit: "enter",
66
+ tab: "tab",
67
+ // Selection/autocomplete
68
+ selectUp: "up",
69
+ selectDown: "down",
70
+ selectConfirm: "enter",
71
+ selectCancel: ["escape", "ctrl+c"],
72
+ // Clipboard
73
+ copy: "ctrl+c",
74
+ };
75
+
76
+ /**
77
+ * Manages keybindings for the editor.
78
+ */
79
+ export class EditorKeybindingsManager {
80
+ private actionToKeys: Map<EditorAction, KeyId[]>;
81
+
82
+ constructor(config: EditorKeybindingsConfig = {}) {
83
+ this.actionToKeys = new Map();
84
+ this.buildMaps(config);
85
+ }
86
+
87
+ private buildMaps(config: EditorKeybindingsConfig): void {
88
+ this.actionToKeys.clear();
89
+
90
+ // Start with defaults
91
+ for (const [action, keys] of Object.entries(DEFAULT_EDITOR_KEYBINDINGS)) {
92
+ const keyArray = Array.isArray(keys) ? keys : [keys];
93
+ this.actionToKeys.set(action as EditorAction, [...keyArray]);
94
+ }
95
+
96
+ // Override with user config
97
+ for (const [action, keys] of Object.entries(config)) {
98
+ if (keys === undefined) continue;
99
+ const keyArray = Array.isArray(keys) ? keys : [keys];
100
+ this.actionToKeys.set(action as EditorAction, keyArray);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Check if input matches a specific action.
106
+ */
107
+ matches(data: string, action: EditorAction): boolean {
108
+ const keys = this.actionToKeys.get(action);
109
+ if (!keys) return false;
110
+ for (const key of keys) {
111
+ if (matchesKey(data, key)) return true;
112
+ }
113
+ return false;
114
+ }
115
+
116
+ /**
117
+ * Get keys bound to an action.
118
+ */
119
+ getKeys(action: EditorAction): KeyId[] {
120
+ return this.actionToKeys.get(action) ?? [];
121
+ }
122
+
123
+ /**
124
+ * Update configuration.
125
+ */
126
+ setConfig(config: EditorKeybindingsConfig): void {
127
+ this.buildMaps(config);
128
+ }
129
+ }
130
+
131
+ // Global instance
132
+ let globalEditorKeybindings: EditorKeybindingsManager | null = null;
133
+
134
+ export function getEditorKeybindings(): EditorKeybindingsManager {
135
+ if (!globalEditorKeybindings) {
136
+ globalEditorKeybindings = new EditorKeybindingsManager();
137
+ }
138
+ return globalEditorKeybindings;
139
+ }
140
+
141
+ export function setEditorKeybindings(manager: EditorKeybindingsManager): void {
142
+ globalEditorKeybindings = manager;
143
+ }