@finos/legend-application 10.2.10 → 10.2.12

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 (68) hide show
  1. package/lib/application/LegendApplication.d.ts.map +1 -1
  2. package/lib/application/LegendApplication.js +1 -2
  3. package/lib/application/LegendApplication.js.map +1 -1
  4. package/lib/components/ActionAlert.d.ts +1 -0
  5. package/lib/components/ActionAlert.d.ts.map +1 -1
  6. package/lib/components/BlockingAlert.d.ts +1 -0
  7. package/lib/components/BlockingAlert.d.ts.map +1 -1
  8. package/lib/components/LegendApplicationComponentFrameworkProvider.d.ts +6 -0
  9. package/lib/components/LegendApplicationComponentFrameworkProvider.d.ts.map +1 -1
  10. package/lib/components/LegendApplicationComponentFrameworkProvider.js +21 -13
  11. package/lib/components/LegendApplicationComponentFrameworkProvider.js.map +1 -1
  12. package/lib/components/NotificationManager.d.ts +1 -0
  13. package/lib/components/NotificationManager.d.ts.map +1 -1
  14. package/lib/components/VirtualAssistant.d.ts +1 -0
  15. package/lib/components/VirtualAssistant.d.ts.map +1 -1
  16. package/lib/components/shared/TabManager.d.ts +1 -0
  17. package/lib/components/shared/TabManager.d.ts.map +1 -1
  18. package/lib/components/shared/TextSearchAdvancedConfigMenu.d.ts +1 -0
  19. package/lib/components/shared/TextSearchAdvancedConfigMenu.d.ts.map +1 -1
  20. package/lib/index.css +2 -2
  21. package/lib/index.css.map +1 -1
  22. package/lib/stores/ApplicationEvent.d.ts +1 -0
  23. package/lib/stores/ApplicationEvent.d.ts.map +1 -1
  24. package/lib/stores/ApplicationEvent.js +1 -0
  25. package/lib/stores/ApplicationEvent.js.map +1 -1
  26. package/lib/stores/ApplicationStore.d.ts +2 -0
  27. package/lib/stores/ApplicationStore.d.ts.map +1 -1
  28. package/lib/stores/ApplicationStore.js +3 -0
  29. package/lib/stores/ApplicationStore.js.map +1 -1
  30. package/lib/stores/CommandCenter.d.ts +1 -0
  31. package/lib/stores/CommandCenter.d.ts.map +1 -1
  32. package/lib/stores/CommandCenter.js.map +1 -1
  33. package/lib/stores/KeyboardShortcutsService.d.ts +4 -10
  34. package/lib/stores/KeyboardShortcutsService.d.ts.map +1 -1
  35. package/lib/stores/KeyboardShortcutsService.js +36 -33
  36. package/lib/stores/KeyboardShortcutsService.js.map +1 -1
  37. package/lib/stores/LegendApplicationDocumentation.d.ts +1 -0
  38. package/lib/stores/LegendApplicationDocumentation.d.ts.map +1 -1
  39. package/lib/stores/LegendApplicationDocumentation.js +1 -0
  40. package/lib/stores/LegendApplicationDocumentation.js.map +1 -1
  41. package/lib/stores/PureLanguageSupport.d.ts.map +1 -1
  42. package/lib/stores/PureLanguageSupport.js +14 -1
  43. package/lib/stores/PureLanguageSupport.js.map +1 -1
  44. package/lib/stores/TerminalService.d.ts +23 -0
  45. package/lib/stores/TerminalService.d.ts.map +1 -0
  46. package/lib/stores/TerminalService.js +25 -0
  47. package/lib/stores/TerminalService.js.map +1 -0
  48. package/lib/stores/terminal/Terminal.d.ts +154 -0
  49. package/lib/stores/terminal/Terminal.d.ts.map +1 -0
  50. package/lib/stores/terminal/Terminal.js +170 -0
  51. package/lib/stores/terminal/Terminal.js.map +1 -0
  52. package/lib/stores/terminal/XTerm.d.ts +91 -0
  53. package/lib/stores/terminal/XTerm.d.ts.map +1 -0
  54. package/lib/stores/terminal/XTerm.js +671 -0
  55. package/lib/stores/terminal/XTerm.js.map +1 -0
  56. package/package.json +17 -11
  57. package/src/application/LegendApplication.tsx +5 -2
  58. package/src/components/LegendApplicationComponentFrameworkProvider.tsx +24 -18
  59. package/src/stores/ApplicationEvent.ts +1 -0
  60. package/src/stores/ApplicationStore.ts +4 -1
  61. package/src/stores/CommandCenter.ts +1 -0
  62. package/src/stores/KeyboardShortcutsService.ts +43 -48
  63. package/src/stores/LegendApplicationDocumentation.ts +1 -0
  64. package/src/stores/PureLanguageSupport.ts +15 -1
  65. package/src/stores/TerminalService.ts +30 -0
  66. package/src/stores/terminal/Terminal.ts +258 -0
  67. package/src/stores/terminal/XTerm.ts +854 -0
  68. package/tsconfig.json +3 -0
@@ -0,0 +1,671 @@
1
+ /**
2
+ * Copyright (c) 2020-present, Goldman Sachs
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { Terminal as XTermTerminal, } from 'xterm';
17
+ import { WebLinksAddon as XTermWebLinksAddon } from 'xterm-addon-web-links';
18
+ import { FitAddon as XTermFitAddon } from 'xterm-addon-fit';
19
+ import { SearchAddon as XTermSearchAddon, } from 'xterm-addon-search';
20
+ import { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11';
21
+ import { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl';
22
+ import { MONOSPACED_FONT_FAMILY, TAB_SIZE } from '../../const.js';
23
+ import { Terminal, DISPLAY_ANSI_ESCAPE, ANSI_moveCursor, } from './Terminal.js';
24
+ import { ActionState, guaranteeNonNullable, IllegalStateError, isMatchingKeyCombination, LogEvent, prettyCONSTName, uniqBy, } from '@finos/legend-shared';
25
+ import { APPLICATION_EVENT } from '../ApplicationEvent.js';
26
+ import { forceDispatchKeyboardEvent } from '../../components/LegendApplicationComponentFrameworkProvider.js';
27
+ const LEGEND_XTERM_THEME = {
28
+ foreground: '#cccccc',
29
+ background: '#1e1e1e',
30
+ cursor: '#cccccc',
31
+ /** The accent color of the cursor (fg color for a block cursor) */
32
+ // cursorAccent?: string;
33
+ /** The selection background color when the terminal does not have focus (can be transparent) */
34
+ // selectionInactiveBackground?: string;
35
+ selectionBackground: '#264f78',
36
+ black: '#000000',
37
+ red: '#cd3131',
38
+ green: '#0dbc79',
39
+ yellow: '#e5e510',
40
+ blue: '#2472c8',
41
+ magenta: '#bc3fbc',
42
+ cyan: '#11a8cd',
43
+ white: '#e5e5e5',
44
+ brightBlack: '#666666',
45
+ brightRed: '#f14c4c',
46
+ brightGreen: '#23d18b',
47
+ brightYellow: '#f5f543',
48
+ brightBlue: '#3b8eea',
49
+ brightMagenta: '#d670d6',
50
+ brightCyan: '#29b8db',
51
+ brightWhite: '#e5e5e5',
52
+ };
53
+ const LEGEND_XTERM_SEARCH_THEME = {
54
+ matchOverviewRuler: '#d186167e',
55
+ activeMatchColorOverviewRuler: '#A0A0A0CC',
56
+ matchBackground: '#62331c',
57
+ activeMatchBackground: '#515C6A',
58
+ };
59
+ // robot acsii art
60
+ // See https://asciiartist.com/ascii-art-micro-robot/
61
+ const getHelpCommandContent = (commandRegistry) => `
62
+ ${DISPLAY_ANSI_ESCAPE.BRIGHT_BLACK}+-------------------------------------------------------+${DISPLAY_ANSI_ESCAPE.RESET}
63
+ ${DISPLAY_ANSI_ESCAPE.BRIGHT_BLACK}|${DISPLAY_ANSI_ESCAPE.RESET} ${DISPLAY_ANSI_ESCAPE.BRIGHT_GREEN}[@@]${DISPLAY_ANSI_ESCAPE.RESET} "Hi! Welcome to the HELP menu of Pure IDE" ${DISPLAY_ANSI_ESCAPE.BRIGHT_BLACK}|${DISPLAY_ANSI_ESCAPE.RESET}
64
+ ${DISPLAY_ANSI_ESCAPE.BRIGHT_BLACK}|${DISPLAY_ANSI_ESCAPE.RESET} ${DISPLAY_ANSI_ESCAPE.BRIGHT_GREEN}/|__|\\${DISPLAY_ANSI_ESCAPE.RESET} ${DISPLAY_ANSI_ESCAPE.BRIGHT_BLACK}|${DISPLAY_ANSI_ESCAPE.RESET}
65
+ ${DISPLAY_ANSI_ESCAPE.BRIGHT_BLACK}+--${DISPLAY_ANSI_ESCAPE.RESET} ${DISPLAY_ANSI_ESCAPE.BRIGHT_GREEN}d b${DISPLAY_ANSI_ESCAPE.RESET} ${DISPLAY_ANSI_ESCAPE.BRIGHT_BLACK}-----------------------------------------------+${DISPLAY_ANSI_ESCAPE.RESET}
66
+
67
+ Following is the list of supported commands:
68
+
69
+ ${uniqBy(Array.from(commandRegistry.values()), (config) => config.command)
70
+ .map((config) => `${DISPLAY_ANSI_ESCAPE.BRIGHT_GREEN}${config.command.padEnd(30)}${DISPLAY_ANSI_ESCAPE.RESET}${config.description}${config.aliases?.length
71
+ ? `\n${''.padEnd(30)}Aliases: ${config.aliases.join(', ')}`
72
+ : ''}\n${''.padEnd(30)}Usage: ${DISPLAY_ANSI_ESCAPE.DIM}${config.usage}${DISPLAY_ANSI_ESCAPE.RESET}`)
73
+ .join('\n')}`;
74
+ const getCommonANSIEscapeSequencesForStyling = () => `
75
+ Common ANSI Escape Sequences for Styling:
76
+
77
+ ${Object.entries(DISPLAY_ANSI_ESCAPE)
78
+ .map(([key, value]) => `${value}${prettyCONSTName(key).padEnd(20)}${DISPLAY_ANSI_ESCAPE.RESET
79
+ // NOTE: since these are recommended ANSI escape sequences which can be used
80
+ // by users in strings input in Pure IDE, they have to be Unicode escape, if we send
81
+ // the original hexadecimal escape as part of the string, some string escape handling
82
+ // in Pure seems to escape the leading slash of the ANSI escape sequence \x1B; however
83
+ // this is not the case of the escape sequence for Unicode, \u001b hence our logic here
84
+ } ${value.replace('\x1b', '\\u001b')}`)
85
+ .join('\n')}`;
86
+ const DEFAULT_USER = 'purist';
87
+ const DEFAULT_COMMAND_HEADER = `
88
+ ${DISPLAY_ANSI_ESCAPE.BOLD}${DISPLAY_ANSI_ESCAPE.BRIGHT_BLUE}$${DEFAULT_USER}${DISPLAY_ANSI_ESCAPE.RESET}
89
+ ${DISPLAY_ANSI_ESCAPE.BOLD}${DISPLAY_ANSI_ESCAPE.MAGENTA}\u276f${DISPLAY_ANSI_ESCAPE.RESET} `;
90
+ const COMMAND_START = '\u276f ';
91
+ export class XTerm extends Terminal {
92
+ instance;
93
+ resizer;
94
+ renderer;
95
+ searcher;
96
+ webLinkProvider;
97
+ _TEMPORARY__onKeyListener;
98
+ _TEMPORARY__onDataListener;
99
+ command = '';
100
+ commandHistory = [];
101
+ currentCommandSearchString = '';
102
+ commandHistoryNavigationIdx = undefined;
103
+ isRunningCommand = false;
104
+ setupState = ActionState.create();
105
+ constructor(applicationStore) {
106
+ super(applicationStore);
107
+ this.instance = new XTermTerminal({
108
+ allowProposedApi: true,
109
+ fontSize: 12,
110
+ letterSpacing: 2,
111
+ fontWeight: 400,
112
+ fontWeightBold: 700,
113
+ fontFamily: `"${MONOSPACED_FONT_FAMILY}", Menlo, Consolas, monospace`,
114
+ tabStopWidth: TAB_SIZE,
115
+ theme: LEGEND_XTERM_THEME,
116
+ overviewRulerWidth: 14,
117
+ scrollback: 10000,
118
+ convertEol: true, // treat \n as new line
119
+ });
120
+ this.resizer = new XTermFitAddon();
121
+ this.searcher = new XTermSearchAddon();
122
+ this.renderer = new XTermWebglAddon();
123
+ }
124
+ setup(configuration) {
125
+ if (this.setupState.hasCompleted) {
126
+ throw new IllegalStateError(`Terminal is already set up`);
127
+ }
128
+ this.setupState.complete();
129
+ // Handling context loss: The browser may drop WebGL contexts for various reasons like OOM or after the system has been suspended.
130
+ // An easy, but suboptimal way, to handle this is by disposing of WebglAddon when the `webglcontextlost` event fires
131
+ // NOTE: we don't really have a resilient way to fallback right now, hopefully, the fallback is to render in DOM
132
+ this.renderer.onContextLoss(() => {
133
+ this.renderer.dispose();
134
+ });
135
+ this.instance.loadAddon(this.resizer);
136
+ this.instance.loadAddon(this.searcher);
137
+ this.instance.loadAddon(this.renderer);
138
+ this.instance.loadAddon(new XTermUnicode11Addon());
139
+ this.instance.unicode.activeVersion = '11';
140
+ // NOTE: since we render the terminal using webgl/canvas, event is not bubbled
141
+ // naturally through the DOM tree, we have to manually force this
142
+ this.instance.attachCustomKeyEventHandler((event) => {
143
+ // NOTE: this is a cheap way to handle hotkey, but this is really the only
144
+ // hotkey we want to support at local scope of the terminal
145
+ // also, since here we have prevent default and stop propagation, we have to do
146
+ // this here instead at in `onKey` handler
147
+ if (isMatchingKeyCombination(event, 'Control+KeyF') ||
148
+ isMatchingKeyCombination(event, 'Meta+KeyF')) {
149
+ // prevent default so as to not trigger browser platform search command
150
+ event.preventDefault();
151
+ event.stopPropagation();
152
+ this.searchConfig.focus();
153
+ return false;
154
+ }
155
+ return true; // return true to indicate the event should still be handled by xterm
156
+ });
157
+ this.webLinkProvider = configuration?.webLinkProvider
158
+ ? new XTermWebLinksAddon(configuration.webLinkProvider.handler, {
159
+ urlRegex: configuration.webLinkProvider.regex,
160
+ })
161
+ : new XTermWebLinksAddon();
162
+ this.instance.loadAddon(this.webLinkProvider);
163
+ (configuration?.commands ?? []).forEach((commandConfig) => {
164
+ [commandConfig.command, ...(commandConfig.aliases ?? [])].forEach((command) => {
165
+ if (!this.commandRegistry.has(command)) {
166
+ this.commandRegistry.set(command, commandConfig);
167
+ }
168
+ else {
169
+ this.applicationStore.log.warn(LogEvent.create(APPLICATION_EVENT.APPLICATION_TERMINAL_COMMAND_CONFIGURATION_CHECK_FAILURE), `Found multiple duplicated terminal commands '${command}'`);
170
+ }
171
+ });
172
+ });
173
+ this.searcher.onDidChangeResults((result) => {
174
+ if (result) {
175
+ this.setSearchResultCount(result.resultCount);
176
+ this.setSearchCurrentResultIndex(result.resultIndex);
177
+ }
178
+ else {
179
+ this.setSearchResultCount(undefined);
180
+ this.setSearchCurrentResultIndex(undefined);
181
+ }
182
+ });
183
+ // NOTE: `xterm` expects to be attached to a proper terminal program which handles
184
+ // input, since we can't do that yet, we implement a fairly basic input handling flow
185
+ // See https://github.com/xtermjs/xterm.js/issues/617#issuecomment-288849502
186
+ this._TEMPORARY__onKeyListener = this.instance.onKey(({ key, domEvent }) => {
187
+ // take care of command history navigation
188
+ if (domEvent.code === 'ArrowUp') {
189
+ this.setCommandFromHistory(this.commandHistoryNavigationIdx !== undefined
190
+ ? this.commandHistoryNavigationIdx + 1
191
+ : 0);
192
+ return;
193
+ // reset current command in place
194
+ }
195
+ else if (domEvent.code === 'ArrowDown') {
196
+ if (this.commandHistoryNavigationIdx !== undefined) {
197
+ this.setCommandFromHistory(this.commandHistoryNavigationIdx === 0
198
+ ? undefined
199
+ : this.commandHistoryNavigationIdx - 1);
200
+ }
201
+ return;
202
+ }
203
+ else {
204
+ // reset navigation history the moment any other key is pressed
205
+ this.commandHistoryNavigationIdx = undefined;
206
+ }
207
+ if (domEvent.code === 'Enter') {
208
+ // run command
209
+ if (this.command.trim()) {
210
+ const text = this.command;
211
+ const [command, ...args] = text.replaceAll(/\s+/g, ' ').split(' ');
212
+ this.addCommandToHistory(this.command);
213
+ if (!command) {
214
+ return;
215
+ }
216
+ const matchingCommand = this.commandRegistry.get(command);
217
+ if (!matchingCommand) {
218
+ this.fail(`command not found: ${command}`);
219
+ return;
220
+ }
221
+ if (this.isRunningCommand) {
222
+ return;
223
+ }
224
+ this.isRunningCommand = true;
225
+ matchingCommand.handler(args, command, text).finally(() => {
226
+ this.isRunningCommand = false;
227
+ if (!this.isFlushed) {
228
+ this.abort();
229
+ }
230
+ });
231
+ }
232
+ }
233
+ else if (isMatchingKeyCombination(domEvent, 'Control+KeyC') ||
234
+ isMatchingKeyCombination(domEvent, 'Control+KeyD')) {
235
+ // abort command
236
+ this.abort();
237
+ }
238
+ else if (domEvent.code === 'Backspace') {
239
+ // Alt: jump word only, Ctrl: jump to end
240
+ // this would apply for Delete, ArrowLeft, ArrowRight
241
+ this.deleteFromCommand(domEvent.altKey
242
+ ? this.computeCursorJumpMovement(true, true)
243
+ : domEvent.ctrlKey
244
+ ? this.computeCursorJumpMovement(true, false)
245
+ : -1);
246
+ }
247
+ else if (domEvent.code === 'Delete') {
248
+ this.deleteFromCommand(domEvent.altKey
249
+ ? this.computeCursorJumpMovement(false, true)
250
+ : domEvent.ctrlKey
251
+ ? this.computeCursorJumpMovement(false, false)
252
+ : 1);
253
+ }
254
+ else if (domEvent.code === 'ArrowLeft') {
255
+ this.instance.write(this.generateMoveCursorANSISeq(domEvent.altKey
256
+ ? this.computeCursorJumpMovement(true, true)
257
+ : domEvent.ctrlKey
258
+ ? this.computeCursorJumpMovement(true, false)
259
+ : -1));
260
+ }
261
+ else if (domEvent.code === 'ArrowRight') {
262
+ this.instance.write(this.generateMoveCursorANSISeq(domEvent.altKey
263
+ ? this.computeCursorJumpMovement(false, true)
264
+ : domEvent.ctrlKey
265
+ ? this.computeCursorJumpMovement(false, false)
266
+ : 1));
267
+ }
268
+ else if (
269
+ // use key here so we absolute do not allow any characters other than these
270
+ // being added to the input command
271
+ key.match(/^[A-Za-z0-9!@#$%^&*()-_=+"':;,.<>/?[\]{}|\\~` \\t]$/)) {
272
+ // commonly supported keys
273
+ this.writeToCommand(key);
274
+ }
275
+ else {
276
+ // for the rest, allow the keyboard event to be bubbled to
277
+ // application keyboard shortcuts handler
278
+ forceDispatchKeyboardEvent(domEvent);
279
+ }
280
+ });
281
+ // this is needed to support copy-pasting
282
+ this._TEMPORARY__onDataListener = this.instance.onData((val) => {
283
+ // only support pasting (not meant for 1 character though) and special functions starting with special
284
+ // ANSI escape sequence
285
+ if (val.length > 1 && !val.startsWith('\x1b')) {
286
+ this.writeToCommand(val
287
+ // remove all unsupported characters, including newline
288
+ .replaceAll(/[^A-Za-z0-9!@#$%^&*()-_=+"':;,.<>/?[\]{}|\\~` \\t]/g, '')
289
+ .trimEnd());
290
+ }
291
+ });
292
+ }
293
+ // NOTE: this is fairly HACKY way to detect command
294
+ // we don't really have a better solution at the moment,
295
+ // but we should come with more systematic way of persisting the start line of command
296
+ // the challenge with this is due to text-reflow
297
+ //
298
+ // there is also a quriky known issue with text-reflow and the line with cursor
299
+ // See https://github.com/xtermjs/xterm.js/issues/1941#issuecomment-463660633
300
+ getCommandRange() {
301
+ const buffer = this.instance.buffer.active;
302
+ const cols = this.instance.cols;
303
+ const commandText = `${COMMAND_START}${this.command}`;
304
+ const commandFirstLine = `${COMMAND_START}${this.command.substring(0, cols - COMMAND_START.length)}${this.command.length < cols - COMMAND_START.length
305
+ ? ' '.repeat(cols - this.command.length - COMMAND_START.length)
306
+ : ''}`;
307
+ let startY = 0;
308
+ let cursorIdx = 0;
309
+ for (let i = buffer.baseY + buffer.cursorY; i > -1; --i) {
310
+ const line = guaranteeNonNullable(buffer.getLine(i));
311
+ const lineText = line.translateToString();
312
+ if (lineText === commandFirstLine) {
313
+ startY = i;
314
+ cursorIdx +=
315
+ (i === buffer.baseY + buffer.cursorY ? buffer.cursorX : cols) -
316
+ COMMAND_START.length;
317
+ break;
318
+ }
319
+ else {
320
+ cursorIdx +=
321
+ i === buffer.baseY + buffer.cursorY ? buffer.cursorX : cols;
322
+ }
323
+ }
324
+ // start line == -1 is the rare case where the command is too long and exceeds the buffer length
325
+ // leading to incomplete command being captured
326
+ return {
327
+ startY,
328
+ startX: COMMAND_START.length,
329
+ endY: startY + (commandText.length - (commandText.length % cols)) / cols,
330
+ endX: commandText.length % cols,
331
+ cursorIdx,
332
+ };
333
+ }
334
+ computeCursorJumpMovement(back, jumpWordOnly) {
335
+ const range = this.getCommandRange();
336
+ let distance = undefined;
337
+ let foundWord = false;
338
+ // scan for the boundary of the closest word to the cursor position
339
+ if (jumpWordOnly) {
340
+ if (back) {
341
+ for (let i = range.cursorIdx - 1; i > -1; --i) {
342
+ const char = this.command.charAt(i);
343
+ if (char.match(/\w/)) {
344
+ if (!foundWord) {
345
+ foundWord = true;
346
+ }
347
+ }
348
+ else {
349
+ if (foundWord) {
350
+ distance = range.cursorIdx - i - 1;
351
+ break;
352
+ }
353
+ }
354
+ }
355
+ }
356
+ else {
357
+ for (let i = range.cursorIdx + 1; i < this.command.length; ++i) {
358
+ const char = this.command.charAt(i);
359
+ if (char.match(/\w/)) {
360
+ if (!foundWord) {
361
+ foundWord = true;
362
+ }
363
+ }
364
+ else {
365
+ if (foundWord) {
366
+ distance = i - range.cursorIdx - 1;
367
+ break;
368
+ }
369
+ }
370
+ }
371
+ }
372
+ }
373
+ if (distance === undefined) {
374
+ distance = back ? range.cursorIdx : this.command.length - range.cursorIdx;
375
+ }
376
+ return back ? -distance : distance;
377
+ }
378
+ /**
379
+ * Generate the ANSI escape sequence for new cursor position
380
+ * after being moved by the the number of cells.
381
+ *
382
+ * @param val a number (negative means cursor move leftwards)
383
+ * @param limit whether to limit the movement of the cursor by the command range
384
+ * @returns ANSI escape sequence for new cursor position
385
+ */
386
+ generateMoveCursorANSISeq(val, limit = true) {
387
+ const buffer = this.instance.buffer.active;
388
+ const cols = this.instance.cols;
389
+ const range = this.getCommandRange();
390
+ const maxDistance = limit
391
+ ? val < 0
392
+ ? range.cursorIdx
393
+ : this.command.length - range.cursorIdx
394
+ : val;
395
+ const distance = Math.min(Math.abs(val), maxDistance);
396
+ let newCursorX = buffer.cursorX;
397
+ let newCursorY = buffer.cursorY;
398
+ if (val < 0) {
399
+ // move leftwards
400
+ newCursorX = (cols + ((buffer.cursorX - distance) % cols)) % cols;
401
+ newCursorY =
402
+ buffer.baseY +
403
+ buffer.cursorY -
404
+ (distance > buffer.cursorX ? Math.ceil(distance / cols) : 0);
405
+ }
406
+ else if (val > 0) {
407
+ // move rightwards
408
+ newCursorX = (buffer.cursorX + distance) % cols;
409
+ newCursorY =
410
+ buffer.baseY +
411
+ buffer.cursorY +
412
+ (buffer.cursorX + distance >= cols
413
+ ? Math.floor((buffer.cursorX + distance) / cols)
414
+ : 0);
415
+ }
416
+ return ANSI_moveCursor(newCursorY + 1, newCursorX + 1);
417
+ }
418
+ /**
419
+ * Write value to command with awareness of the current cursor position
420
+ */
421
+ writeToCommand(val) {
422
+ const range = this.getCommandRange();
423
+ const left = this.command.slice(0, range.cursorIdx);
424
+ const right = this.command.slice(range.cursorIdx);
425
+ this.instance.write(val +
426
+ right +
427
+ // update the cursor
428
+ this.generateMoveCursorANSISeq(val.length, false));
429
+ this.setCommand(left + val + right);
430
+ }
431
+ /**
432
+ * Remove number of characters from command with awareness of the current cursor position
433
+ * NOTE: negative number means backward deleting (i.e. backspace)
434
+ */
435
+ deleteFromCommand(val) {
436
+ const range = this.getCommandRange();
437
+ const maxDistance = val < 0 ? range.cursorIdx : this.command.length - range.cursorIdx;
438
+ const distance = Math.min(Math.abs(val), maxDistance);
439
+ let left;
440
+ let right;
441
+ let cursorMovement;
442
+ if (val === 0) {
443
+ return;
444
+ }
445
+ else if (val < 0) {
446
+ // remove leftwards
447
+ left = this.command.slice(0, range.cursorIdx - distance);
448
+ right = this.command.slice(range.cursorIdx, this.command.length);
449
+ cursorMovement = -distance;
450
+ }
451
+ else {
452
+ // remove rightwards
453
+ left = this.command.slice(0, range.cursorIdx);
454
+ right = this.command.slice(range.cursorIdx + distance, this.command.length);
455
+ cursorMovement = 0;
456
+ }
457
+ this.instance.write(
458
+ // reset cursor to start of command, basically here, we're rewriting the entire command
459
+ ANSI_moveCursor(range.startY + 1, range.startX + 1) +
460
+ left +
461
+ right +
462
+ // fill space to erase cells rendered from previous command
463
+ ' '.repeat(distance) +
464
+ // move the cursor as well
465
+ this.generateMoveCursorANSISeq(cursorMovement));
466
+ this.setCommand(left + right);
467
+ }
468
+ get isSetup() {
469
+ return this.setupState.hasCompleted;
470
+ }
471
+ isFocused() {
472
+ return document.activeElement === this.instance.textarea;
473
+ }
474
+ mount(container) {
475
+ if (!this.setupState.hasCompleted) {
476
+ throw new IllegalStateError(`XTerm terminal has not been set up yet`);
477
+ }
478
+ this.instance.open(container);
479
+ }
480
+ dispose() {
481
+ this.searcher.dispose();
482
+ this.resizer.dispose();
483
+ this.renderer.dispose();
484
+ this.webLinkProvider?.dispose();
485
+ this._TEMPORARY__onKeyListener?.dispose();
486
+ this._TEMPORARY__onDataListener?.dispose();
487
+ this.instance.dispose();
488
+ }
489
+ autoResize() {
490
+ this.resizer.fit();
491
+ }
492
+ focus() {
493
+ this.instance.focus();
494
+ }
495
+ addCommandToHistory(val) {
496
+ // if this is the same as previous command, do not push it to the history stack
497
+ if (val === this.commandHistory.at(0)) {
498
+ return;
499
+ }
500
+ // history command is essentially a stack, so we only insert at the beginning
501
+ this.commandHistory.unshift(val);
502
+ }
503
+ /**
504
+ * This methods help update the current command to a command in history
505
+ * stack, it does the necessary resetting and helps properly update
506
+ * the history navigation index
507
+ */
508
+ setCommandFromHistory(idx) {
509
+ const val = idx === undefined
510
+ ? this.currentCommandSearchString
511
+ : // NOTE: only consider commands starting with the original command
512
+ // also note that empty string naturaly match all history command
513
+ this.commandHistory
514
+ .filter((command) => command.startsWith(this.currentCommandSearchString))
515
+ .at(idx);
516
+ if (val !== undefined) {
517
+ let range = this.getCommandRange();
518
+ this.instance.write(
519
+ // reset cursor to start of command and rewrite the entire command
520
+ ANSI_moveCursor(range.startY + 1, range.startX + 1) +
521
+ val.padEnd(this.command.length));
522
+ this.command = val;
523
+ range = this.getCommandRange();
524
+ this.instance.write(
525
+ // reset cursor to command end
526
+ ANSI_moveCursor(range.endY + 1, range.endX + 1));
527
+ this.commandHistoryNavigationIdx = idx;
528
+ }
529
+ }
530
+ setCommand(val) {
531
+ this.command = val;
532
+ this.currentCommandSearchString = val;
533
+ this.commandHistoryNavigationIdx = undefined;
534
+ }
535
+ newCommand() {
536
+ this.instance.write(DEFAULT_COMMAND_HEADER);
537
+ this.setCommand('');
538
+ }
539
+ newSystemCommand(command) {
540
+ // if another command is already running, we don't need to print the command header anymore
541
+ // the potential pitfall here is that we could have another process prints to the
542
+ // terminal while the command is being run. Nothing much we can do here for now.
543
+ if (!this.isRunningCommand) {
544
+ if (this.command) {
545
+ this.abort();
546
+ this.newCommand();
547
+ }
548
+ this.instance.write(`${DISPLAY_ANSI_ESCAPE.DIM}(system: ${command})\n${DISPLAY_ANSI_ESCAPE.RESET}`);
549
+ }
550
+ }
551
+ /**
552
+ * Flush the terminal screen completely
553
+ *
554
+ * Probably due to write buffer batching, calling `reset` or `clear` on xterm terminal immediately after
555
+ * write commands will not work. To solve this, we can either promisify the `reset` call or write the ANSI
556
+ * reset sequence \x1bc
557
+ */
558
+ flushScreen() {
559
+ this.instance.write('\x1bc');
560
+ this.instance.reset();
561
+ }
562
+ get isFlushed() {
563
+ const buffer = this.instance.buffer.active;
564
+ let isLastLineEmpty = true;
565
+ for (let i = buffer.baseY + buffer.cursorY; i > -1; --i) {
566
+ const line = guaranteeNonNullable(buffer.getLine(i));
567
+ const lineText = line.translateToString();
568
+ // skip empty lines
569
+ if (!lineText.trim()) {
570
+ continue;
571
+ }
572
+ else {
573
+ isLastLineEmpty = lineText !== COMMAND_START;
574
+ break;
575
+ }
576
+ }
577
+ return this.command === '' && isLastLineEmpty;
578
+ }
579
+ clear() {
580
+ this.flushScreen();
581
+ this.instance.scrollToTop();
582
+ this.newCommand();
583
+ }
584
+ resetANSIStyling() {
585
+ this.instance.write(DISPLAY_ANSI_ESCAPE.RESET);
586
+ }
587
+ showHelp() {
588
+ this.resetANSIStyling();
589
+ this.instance.scrollToBottom();
590
+ if (!this.isFlushed && !this.isRunningCommand) {
591
+ this.abort();
592
+ }
593
+ this.instance.write(getHelpCommandContent(this.commandRegistry));
594
+ this.abort();
595
+ }
596
+ showCommonANSIEscapeSequences() {
597
+ this.resetANSIStyling();
598
+ this.instance.scrollToBottom();
599
+ if (!this.isFlushed && !this.isRunningCommand) {
600
+ this.abort();
601
+ }
602
+ this.instance.write(getCommonANSIEscapeSequencesForStyling());
603
+ this.abort();
604
+ }
605
+ abort() {
606
+ this.resetANSIStyling();
607
+ this.instance.write('\n');
608
+ this.newCommand();
609
+ this.instance.scrollToBottom();
610
+ this.isRunningCommand = false;
611
+ }
612
+ fail(error, opts) {
613
+ if (opts?.systemCommand) {
614
+ this.newSystemCommand(opts.systemCommand);
615
+ }
616
+ this.instance.write(`\n${DISPLAY_ANSI_ESCAPE.RED}${error}${DISPLAY_ANSI_ESCAPE.RED}`);
617
+ this.abort();
618
+ }
619
+ output(val, opts) {
620
+ this.resetANSIStyling();
621
+ if (!opts?.clear && opts?.systemCommand) {
622
+ this.newSystemCommand(opts.systemCommand);
623
+ }
624
+ if (!this.preserveLog && opts?.clear) {
625
+ this.flushScreen();
626
+ }
627
+ else if (this.preserveLog || this.isRunningCommand) {
628
+ this.instance.write('\n');
629
+ }
630
+ this.instance.write(val);
631
+ this.resetANSIStyling();
632
+ this.instance.write('\n');
633
+ this.instance.scrollToBottom();
634
+ this.newCommand();
635
+ }
636
+ search(val) {
637
+ this.searcher.findNext(val, {
638
+ decorations: LEGEND_XTERM_SEARCH_THEME,
639
+ regex: this.searchConfig.useRegex,
640
+ wholeWord: this.searchConfig.matchWholeWord,
641
+ caseSensitive: this.searchConfig.matchCaseSensitive,
642
+ // do incremental search so that the expansion will be expanded the selection if it
643
+ // still matches the term the user typed.
644
+ incremental: true,
645
+ });
646
+ }
647
+ clearSearch() {
648
+ this.searcher.clearDecorations();
649
+ this.instance.clearSelection();
650
+ this.setSearchText('');
651
+ this.setSearchResultCount(undefined);
652
+ this.setSearchCurrentResultIndex(undefined);
653
+ }
654
+ findPrevious() {
655
+ this.searcher.findPrevious(this.searchConfig.searchText, {
656
+ decorations: LEGEND_XTERM_SEARCH_THEME,
657
+ regex: this.searchConfig.useRegex,
658
+ wholeWord: this.searchConfig.matchWholeWord,
659
+ caseSensitive: this.searchConfig.matchCaseSensitive,
660
+ });
661
+ }
662
+ findNext() {
663
+ this.searcher.findNext(this.searchConfig.searchText, {
664
+ decorations: LEGEND_XTERM_SEARCH_THEME,
665
+ regex: this.searchConfig.useRegex,
666
+ wholeWord: this.searchConfig.matchWholeWord,
667
+ caseSensitive: this.searchConfig.matchCaseSensitive,
668
+ });
669
+ }
670
+ }
671
+ //# sourceMappingURL=XTerm.js.map