@jupyterlab/fileeditor-extension 4.0.0-alpha.9 → 4.0.0-beta.1

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.
@@ -0,0 +1,1383 @@
1
+ // Copyright (c) Jupyter Development Team.
2
+ // Distributed under the terms of the Modified BSD License.
3
+ import { selectAll } from '@codemirror/commands';
4
+ import { findNext, gotoLine } from '@codemirror/search';
5
+ import { JupyterFrontEnd } from '@jupyterlab/application';
6
+ import {
7
+ Clipboard,
8
+ ICommandPalette,
9
+ ISessionContextDialogs,
10
+ MainAreaWidget,
11
+ WidgetTracker
12
+ } from '@jupyterlab/apputils';
13
+ import {
14
+ CodeEditor,
15
+ CodeViewerWidget,
16
+ IEditorServices
17
+ } from '@jupyterlab/codeeditor';
18
+ import {
19
+ CodeMirrorEditor,
20
+ IEditorExtensionRegistry,
21
+ IEditorLanguageRegistry
22
+ } from '@jupyterlab/codemirror';
23
+ import { ICompletionProviderManager } from '@jupyterlab/completer';
24
+ import { IConsoleTracker } from '@jupyterlab/console';
25
+ import { MarkdownCodeBlocks, PathExt } from '@jupyterlab/coreutils';
26
+ import { IDocumentWidget } from '@jupyterlab/docregistry';
27
+ import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
28
+ import { FileEditor, IEditorTracker } from '@jupyterlab/fileeditor';
29
+ import { ILauncher } from '@jupyterlab/launcher';
30
+ import { IMainMenu } from '@jupyterlab/mainmenu';
31
+ import { ISettingRegistry } from '@jupyterlab/settingregistry';
32
+ import {
33
+ ITranslator,
34
+ nullTranslator,
35
+ TranslationBundle
36
+ } from '@jupyterlab/translation';
37
+ import {
38
+ consoleIcon,
39
+ copyIcon,
40
+ cutIcon,
41
+ LabIcon,
42
+ markdownIcon,
43
+ pasteIcon,
44
+ redoIcon,
45
+ textEditorIcon,
46
+ undoIcon
47
+ } from '@jupyterlab/ui-components';
48
+ import { find } from '@lumino/algorithm';
49
+ import { CommandRegistry } from '@lumino/commands';
50
+ import {
51
+ JSONObject,
52
+ ReadonlyJSONObject,
53
+ ReadonlyPartialJSONObject
54
+ } from '@lumino/coreutils';
55
+
56
+ const autoClosingBracketsNotebook = 'notebook:toggle-autoclosing-brackets';
57
+ const autoClosingBracketsConsole = 'console:toggle-autoclosing-brackets';
58
+
59
+ /**
60
+ * The command IDs used by the fileeditor plugin.
61
+ */
62
+ export namespace CommandIDs {
63
+ export const createNew = 'fileeditor:create-new';
64
+
65
+ export const createNewMarkdown = 'fileeditor:create-new-markdown-file';
66
+
67
+ export const changeFontSize = 'fileeditor:change-font-size';
68
+
69
+ export const lineNumbers = 'fileeditor:toggle-line-numbers';
70
+
71
+ export const currentLineNumbers = 'fileeditor:toggle-current-line-numbers';
72
+
73
+ export const lineWrap = 'fileeditor:toggle-line-wrap';
74
+
75
+ export const currentLineWrap = 'fileeditor:toggle-current-line-wrap';
76
+
77
+ export const changeTabs = 'fileeditor:change-tabs';
78
+
79
+ export const matchBrackets = 'fileeditor:toggle-match-brackets';
80
+
81
+ export const currentMatchBrackets =
82
+ 'fileeditor:toggle-current-match-brackets';
83
+
84
+ export const autoClosingBrackets = 'fileeditor:toggle-autoclosing-brackets';
85
+
86
+ export const autoClosingBracketsUniversal =
87
+ 'fileeditor:toggle-autoclosing-brackets-universal';
88
+
89
+ export const createConsole = 'fileeditor:create-console';
90
+
91
+ export const replaceSelection = 'fileeditor:replace-selection';
92
+
93
+ export const restartConsole = 'fileeditor:restart-console';
94
+
95
+ export const runCode = 'fileeditor:run-code';
96
+
97
+ export const runAllCode = 'fileeditor:run-all';
98
+
99
+ export const markdownPreview = 'fileeditor:markdown-preview';
100
+
101
+ export const undo = 'fileeditor:undo';
102
+
103
+ export const redo = 'fileeditor:redo';
104
+
105
+ export const cut = 'fileeditor:cut';
106
+
107
+ export const copy = 'fileeditor:copy';
108
+
109
+ export const paste = 'fileeditor:paste';
110
+
111
+ export const selectAll = 'fileeditor:select-all';
112
+
113
+ export const invokeCompleter = 'completer:invoke-file';
114
+
115
+ export const selectCompleter = 'completer:select-file';
116
+
117
+ export const openCodeViewer = 'code-viewer:open';
118
+
119
+ export const changeTheme = 'fileeditor:change-theme';
120
+
121
+ export const changeLanguage = 'fileeditor:change-language';
122
+
123
+ export const find = 'fileeditor:find';
124
+
125
+ export const goToLine = 'fileeditor:go-to-line';
126
+ }
127
+
128
+ export interface IFileTypeData extends ReadonlyJSONObject {
129
+ fileExt: string;
130
+ iconName: string;
131
+ launcherLabel: string;
132
+ paletteLabel: string;
133
+ caption: string;
134
+ }
135
+
136
+ /**
137
+ * The name of the factory that creates editor widgets.
138
+ */
139
+ export const FACTORY = 'Editor';
140
+
141
+ /**
142
+ * A utility class for adding commands and menu items,
143
+ * for use by the File Editor extension or other Editor extensions.
144
+ */
145
+ export namespace Commands {
146
+ let config: Record<string, any> = {};
147
+ let scrollPastEnd = true;
148
+
149
+ /**
150
+ * Accessor function that returns the createConsole function for use by Create Console commands
151
+ */
152
+ function getCreateConsoleFunction(
153
+ commands: CommandRegistry,
154
+ languages: IEditorLanguageRegistry
155
+ ): (
156
+ widget: IDocumentWidget<FileEditor>,
157
+ args?: ReadonlyPartialJSONObject
158
+ ) => Promise<void> {
159
+ return async function createConsole(
160
+ widget: IDocumentWidget<FileEditor>,
161
+ args?: ReadonlyPartialJSONObject
162
+ ): Promise<void> {
163
+ const options = args || {};
164
+ const console = await commands.execute('console:create', {
165
+ activate: options['activate'],
166
+ name: widget.context.contentsModel?.name,
167
+ path: widget.context.path,
168
+ // Default value is an empty string -> using OR operator
169
+ preferredLanguage:
170
+ widget.context.model.defaultKernelLanguage ||
171
+ (languages.findByFileName(widget.context.path)?.name ?? ''),
172
+ ref: widget.id,
173
+ insertMode: 'split-bottom'
174
+ });
175
+
176
+ widget.context.pathChanged.connect((sender, value) => {
177
+ console.session.setPath(value);
178
+ console.session.setName(widget.context.contentsModel?.name);
179
+ });
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Update the setting values.
185
+ */
186
+ export function updateSettings(
187
+ settings: ISettingRegistry.ISettings,
188
+ commands: CommandRegistry
189
+ ): void {
190
+ config =
191
+ (settings.get('editorConfig').composite as Record<string, any>) ?? {};
192
+ scrollPastEnd = settings.get('scrollPasteEnd').composite as boolean;
193
+
194
+ // Trigger a refresh of the rendered commands
195
+ commands.notifyCommandChanged(CommandIDs.lineNumbers);
196
+ commands.notifyCommandChanged(CommandIDs.currentLineNumbers);
197
+ commands.notifyCommandChanged(CommandIDs.lineWrap);
198
+ commands.notifyCommandChanged(CommandIDs.currentLineWrap);
199
+ commands.notifyCommandChanged(CommandIDs.changeTabs);
200
+ commands.notifyCommandChanged(CommandIDs.matchBrackets);
201
+ commands.notifyCommandChanged(CommandIDs.currentMatchBrackets);
202
+ commands.notifyCommandChanged(CommandIDs.autoClosingBrackets);
203
+ commands.notifyCommandChanged(CommandIDs.changeLanguage);
204
+ }
205
+
206
+ /**
207
+ * Update the settings of the current tracker instances.
208
+ */
209
+ export function updateTracker(
210
+ tracker: WidgetTracker<IDocumentWidget<FileEditor>>
211
+ ): void {
212
+ tracker.forEach(widget => {
213
+ updateWidget(widget.content);
214
+ });
215
+ }
216
+
217
+ /**
218
+ * Update the settings of a widget.
219
+ * Skip global settings for transient editor specific configs.
220
+ */
221
+ export function updateWidget(widget: FileEditor): void {
222
+ const editor = widget.editor;
223
+ editor.setOptions({ ...config, scrollPastEnd });
224
+ }
225
+
226
+ /**
227
+ * Wrapper function for adding the default File Editor commands
228
+ */
229
+ export function addCommands(
230
+ commands: CommandRegistry,
231
+ settingRegistry: ISettingRegistry,
232
+ trans: TranslationBundle,
233
+ id: string,
234
+ isEnabled: () => boolean,
235
+ tracker: WidgetTracker<IDocumentWidget<FileEditor>>,
236
+ defaultBrowser: IDefaultFileBrowser,
237
+ extensions: IEditorExtensionRegistry,
238
+ languages: IEditorLanguageRegistry,
239
+ consoleTracker: IConsoleTracker | null,
240
+ sessionDialogs: ISessionContextDialogs
241
+ ): void {
242
+ /**
243
+ * Add a command to change font size for File Editor
244
+ */
245
+ commands.addCommand(CommandIDs.changeFontSize, {
246
+ execute: args => {
247
+ const delta = Number(args['delta']);
248
+ if (Number.isNaN(delta)) {
249
+ console.error(
250
+ `${CommandIDs.changeFontSize}: delta arg must be a number`
251
+ );
252
+ return;
253
+ }
254
+ const style = window.getComputedStyle(document.documentElement);
255
+ const cssSize = parseInt(
256
+ style.getPropertyValue('--jp-code-font-size'),
257
+ 10
258
+ );
259
+ const currentSize =
260
+ (config['customStyles']['fontSize'] ??
261
+ extensions.baseConfiguration['customStyles']['fontSize']) ||
262
+ cssSize;
263
+ config.fontSize = currentSize + delta;
264
+ return settingRegistry
265
+ .set(id, 'editorConfig', config)
266
+ .catch((reason: Error) => {
267
+ console.error(`Failed to set ${id}: ${reason.message}`);
268
+ });
269
+ },
270
+ label: args => {
271
+ const delta = Number(args['delta']);
272
+ if (Number.isNaN(delta)) {
273
+ console.error(
274
+ `${CommandIDs.changeFontSize}: delta arg must be a number`
275
+ );
276
+ }
277
+ if (delta > 0) {
278
+ return args.isMenu
279
+ ? trans.__('Increase Text Editor Font Size')
280
+ : trans.__('Increase Font Size');
281
+ } else {
282
+ return args.isMenu
283
+ ? trans.__('Decrease Text Editor Font Size')
284
+ : trans.__('Decrease Font Size');
285
+ }
286
+ }
287
+ });
288
+
289
+ /**
290
+ * Add the Line Numbers command
291
+ */
292
+ commands.addCommand(CommandIDs.lineNumbers, {
293
+ execute: async () => {
294
+ config.lineNumbers = !(
295
+ config.lineNumbers ?? extensions.baseConfiguration.lineNumbers
296
+ );
297
+ try {
298
+ return await settingRegistry.set(id, 'editorConfig', config);
299
+ } catch (reason) {
300
+ console.error(`Failed to set ${id}: ${reason.message}`);
301
+ }
302
+ },
303
+ isEnabled,
304
+ isToggled: () =>
305
+ config.lineNumbers ?? extensions.baseConfiguration.lineNumbers,
306
+ label: trans.__('Show Line Numbers')
307
+ });
308
+
309
+ commands.addCommand(CommandIDs.currentLineNumbers, {
310
+ label: trans.__('Show Line Numbers'),
311
+ caption: trans.__('Show the line numbers for the current file.'),
312
+ execute: () => {
313
+ const widget = tracker.currentWidget;
314
+ if (!widget) {
315
+ return;
316
+ }
317
+ const lineNumbers = !widget.content.editor.getOption('lineNumbers');
318
+ widget.content.editor.setOption('lineNumbers', lineNumbers);
319
+ },
320
+ isEnabled,
321
+ isToggled: () => {
322
+ const widget = tracker.currentWidget;
323
+ return (
324
+ (widget?.content.editor.getOption('lineNumbers') as
325
+ | boolean
326
+ | undefined) ?? false
327
+ );
328
+ }
329
+ });
330
+
331
+ /**
332
+ * Add the Word Wrap command
333
+ */
334
+ commands.addCommand(CommandIDs.lineWrap, {
335
+ execute: async args => {
336
+ config.lineWrap = (args['mode'] as boolean) ?? false;
337
+ try {
338
+ return await settingRegistry.set(id, 'editorConfig', config);
339
+ } catch (reason) {
340
+ console.error(`Failed to set ${id}: ${reason.message}`);
341
+ }
342
+ },
343
+ isEnabled,
344
+ isToggled: args => {
345
+ const lineWrap = args['mode'] ?? false;
346
+ return (
347
+ lineWrap ===
348
+ (config.lineWrap ?? extensions.baseConfiguration.lineWrap)
349
+ );
350
+ },
351
+ label: trans.__('Word Wrap')
352
+ });
353
+
354
+ commands.addCommand(CommandIDs.currentLineWrap, {
355
+ label: trans.__('Wrap Words'),
356
+ caption: trans.__('Wrap words for the current file.'),
357
+ execute: () => {
358
+ const widget = tracker.currentWidget;
359
+ if (!widget) {
360
+ return;
361
+ }
362
+ const oldValue = widget.content.editor.getOption('lineWrap');
363
+ widget.content.editor.setOption('lineWrap', !oldValue);
364
+ },
365
+ isEnabled,
366
+ isToggled: () => {
367
+ const widget = tracker.currentWidget;
368
+ return (
369
+ (widget?.content.editor.getOption('lineWrap') as boolean) ?? false
370
+ );
371
+ }
372
+ });
373
+
374
+ /**
375
+ * Add command for changing tabs size or type in File Editor
376
+ */
377
+
378
+ commands.addCommand(CommandIDs.changeTabs, {
379
+ label: args => {
380
+ if (args.size) {
381
+ // Use a context to differentiate with string set as plural in 3.x
382
+ return trans._p('v4', 'Spaces: %1', args.size ?? '');
383
+ } else {
384
+ return trans.__('Indent with Tab');
385
+ }
386
+ },
387
+ execute: async args => {
388
+ config.indentUnit =
389
+ args['size'] !== undefined
390
+ ? ((args['size'] as string) ?? '4').toString()
391
+ : 'Tab';
392
+ try {
393
+ return await settingRegistry.set(id, 'editorConfig', config);
394
+ } catch (reason) {
395
+ console.error(`Failed to set ${id}: ${reason.message}`);
396
+ }
397
+ },
398
+ isToggled: args => {
399
+ const currentIndentUnit =
400
+ config.indentUnit ?? extensions.baseConfiguration.indentUnit;
401
+ return args['size']
402
+ ? args['size'] === currentIndentUnit
403
+ : 'Tab' == currentIndentUnit;
404
+ }
405
+ });
406
+
407
+ /**
408
+ * Add the Match Brackets command
409
+ */
410
+ commands.addCommand(CommandIDs.matchBrackets, {
411
+ execute: async () => {
412
+ config.matchBrackets = !(
413
+ config.matchBrackets ?? extensions.baseConfiguration.matchBrackets
414
+ );
415
+ try {
416
+ return await settingRegistry.set(id, 'editorConfig', config);
417
+ } catch (reason) {
418
+ console.error(`Failed to set ${id}: ${reason.message}`);
419
+ }
420
+ },
421
+ label: trans.__('Match Brackets'),
422
+ isEnabled,
423
+ isToggled: () =>
424
+ config.matchBrackets ?? extensions.baseConfiguration.matchBrackets
425
+ });
426
+
427
+ commands.addCommand(CommandIDs.currentMatchBrackets, {
428
+ label: trans.__('Match Brackets'),
429
+ caption: trans.__('Change match brackets for the current file.'),
430
+ execute: () => {
431
+ const widget = tracker.currentWidget;
432
+ if (!widget) {
433
+ return;
434
+ }
435
+ const matchBrackets = !widget.content.editor.getOption('matchBrackets');
436
+ widget.content.editor.setOption('matchBrackets', matchBrackets);
437
+ },
438
+ isEnabled,
439
+ isToggled: () => {
440
+ const widget = tracker.currentWidget;
441
+ return (
442
+ (widget?.content.editor.getOption('matchBrackets') as
443
+ | boolean
444
+ | undefined) ?? false
445
+ );
446
+ }
447
+ });
448
+
449
+ /**
450
+ * Add the Auto Close Brackets for Text Editor command
451
+ */
452
+ commands.addCommand(CommandIDs.autoClosingBrackets, {
453
+ execute: async args => {
454
+ config.autoClosingBrackets = !!(
455
+ args['force'] ??
456
+ !(
457
+ config.autoClosingBrackets ??
458
+ extensions.baseConfiguration.autoClosingBrackets
459
+ )
460
+ );
461
+ try {
462
+ return await settingRegistry.set(id, 'editorConfig', config);
463
+ } catch (reason) {
464
+ console.error(`Failed to set ${id}: ${reason.message}`);
465
+ }
466
+ },
467
+ label: trans.__('Auto Close Brackets in Text Editor'),
468
+ isToggled: () =>
469
+ config.autoClosingBrackets ??
470
+ extensions.baseConfiguration.autoClosingBrackets
471
+ });
472
+
473
+ commands.addCommand(CommandIDs.autoClosingBracketsUniversal, {
474
+ execute: () => {
475
+ const anyToggled =
476
+ commands.isToggled(CommandIDs.autoClosingBrackets) ||
477
+ commands.isToggled(autoClosingBracketsNotebook) ||
478
+ commands.isToggled(autoClosingBracketsConsole);
479
+ // if any auto closing brackets options is toggled, toggle both off
480
+ if (anyToggled) {
481
+ void commands.execute(CommandIDs.autoClosingBrackets, {
482
+ force: false
483
+ });
484
+ void commands.execute(autoClosingBracketsNotebook, { force: false });
485
+ void commands.execute(autoClosingBracketsConsole, { force: false });
486
+ } else {
487
+ // both are off, turn them on
488
+ void commands.execute(CommandIDs.autoClosingBrackets, {
489
+ force: true
490
+ });
491
+ void commands.execute(autoClosingBracketsNotebook, { force: true });
492
+ void commands.execute(autoClosingBracketsConsole, { force: true });
493
+ }
494
+ },
495
+ label: trans.__('Auto Close Brackets'),
496
+ isToggled: () =>
497
+ commands.isToggled(CommandIDs.autoClosingBrackets) ||
498
+ commands.isToggled(autoClosingBracketsNotebook) ||
499
+ commands.isToggled(autoClosingBracketsConsole)
500
+ });
501
+
502
+ /**
503
+ * Create a menu for the editor.
504
+ */
505
+ commands.addCommand(CommandIDs.changeTheme, {
506
+ label: args =>
507
+ ((args.displayName ?? args.theme) as string) ??
508
+ config.theme ??
509
+ extensions.baseConfiguration.theme ??
510
+ trans.__('Editor Theme'),
511
+ execute: async args => {
512
+ config.theme = (args['theme'] as string) ?? config.theme;
513
+
514
+ try {
515
+ return await settingRegistry.set(id, 'editorConfig', config);
516
+ } catch (reason) {
517
+ console.error(`Failed to set theme - ${reason.message}`);
518
+ }
519
+ },
520
+ isToggled: args =>
521
+ args['theme'] === (config.theme ?? extensions.baseConfiguration.theme)
522
+ });
523
+
524
+ commands.addCommand(CommandIDs.find, {
525
+ label: trans.__('Find…'),
526
+ execute: () => {
527
+ const widget = tracker.currentWidget;
528
+ if (!widget) {
529
+ return;
530
+ }
531
+ const editor = widget.content.editor as CodeMirrorEditor;
532
+ editor.execCommand(findNext);
533
+ },
534
+ isEnabled
535
+ });
536
+
537
+ commands.addCommand(CommandIDs.goToLine, {
538
+ label: trans.__('Go to Line…'),
539
+ execute: args => {
540
+ const widget = tracker.currentWidget;
541
+ if (!widget) {
542
+ return;
543
+ }
544
+ const editor = widget.content.editor as CodeMirrorEditor;
545
+
546
+ const line = args['line'] as number | undefined;
547
+ const column = args['column'] as number | undefined;
548
+ if (line !== undefined || column !== undefined) {
549
+ editor.setCursorPosition({
550
+ line: (line ?? 1) - 1,
551
+ column: (column ?? 1) - 1
552
+ });
553
+ } else {
554
+ editor.execCommand(gotoLine);
555
+ }
556
+ },
557
+ isEnabled
558
+ });
559
+
560
+ commands.addCommand(CommandIDs.changeLanguage, {
561
+ label: args =>
562
+ ((args['displayName'] ?? args['name']) as string) ??
563
+ trans.__('Change editor language.'),
564
+ execute: args => {
565
+ const name = args['name'] as string;
566
+ const widget = tracker.currentWidget;
567
+ if (name && widget) {
568
+ const spec = languages.findByName(name);
569
+ if (spec) {
570
+ widget.content.model.mimeType = spec.mime as string;
571
+ }
572
+ }
573
+ },
574
+ isEnabled,
575
+ isToggled: args => {
576
+ const widget = tracker.currentWidget;
577
+ if (!widget) {
578
+ return false;
579
+ }
580
+ const mime = widget.content.model.mimeType;
581
+ const spec = languages.findByMIME(mime);
582
+ const name = spec && spec.name;
583
+ return args['name'] === name;
584
+ }
585
+ });
586
+
587
+ /**
588
+ * Add the replace selection for text editor command
589
+ */
590
+
591
+ commands.addCommand(CommandIDs.replaceSelection, {
592
+ execute: args => {
593
+ const text: string = (args['text'] as string) || '';
594
+ const widget = tracker.currentWidget;
595
+ if (!widget) {
596
+ return;
597
+ }
598
+ widget.content.editor.replaceSelection?.(text);
599
+ },
600
+ isEnabled,
601
+ label: trans.__('Replace Selection in Editor')
602
+ });
603
+
604
+ /**
605
+ * Add the Create Console for Editor command
606
+ */
607
+ commands.addCommand(CommandIDs.createConsole, {
608
+ execute: args => {
609
+ const widget = tracker.currentWidget;
610
+
611
+ if (!widget) {
612
+ return;
613
+ }
614
+
615
+ return getCreateConsoleFunction(commands, languages)(widget, args);
616
+ },
617
+ isEnabled,
618
+ icon: consoleIcon,
619
+ label: trans.__('Create Console for Editor')
620
+ });
621
+
622
+ /**
623
+ * Restart the Console Kernel linked to the current Editor
624
+ */
625
+ commands.addCommand(CommandIDs.restartConsole, {
626
+ execute: async () => {
627
+ const current = tracker.currentWidget?.content;
628
+
629
+ if (!current || consoleTracker === null) {
630
+ return;
631
+ }
632
+
633
+ const widget = consoleTracker.find(
634
+ widget => widget.sessionContext.session?.path === current.context.path
635
+ );
636
+ if (widget) {
637
+ return sessionDialogs.restart(widget.sessionContext);
638
+ }
639
+ },
640
+ label: trans.__('Restart Kernel'),
641
+ isEnabled: () => consoleTracker !== null && isEnabled()
642
+ });
643
+
644
+ /**
645
+ * Add the Run Code command
646
+ */
647
+ commands.addCommand(CommandIDs.runCode, {
648
+ execute: () => {
649
+ // Run the appropriate code, taking into account a ```fenced``` code block.
650
+ const widget = tracker.currentWidget?.content;
651
+
652
+ if (!widget) {
653
+ return;
654
+ }
655
+
656
+ let code: string | undefined = '';
657
+ const editor = widget.editor;
658
+ const path = widget.context.path;
659
+ const extension = PathExt.extname(path);
660
+ const selection = editor.getSelection();
661
+ const { start, end } = selection;
662
+ let selected = start.column !== end.column || start.line !== end.line;
663
+
664
+ if (selected) {
665
+ // Get the selected code from the editor.
666
+ const start = editor.getOffsetAt(selection.start);
667
+ const end = editor.getOffsetAt(selection.end);
668
+
669
+ code = editor.model.sharedModel.getSource().substring(start, end);
670
+ } else if (MarkdownCodeBlocks.isMarkdown(extension)) {
671
+ const text = editor.model.sharedModel.getSource();
672
+ const blocks = MarkdownCodeBlocks.findMarkdownCodeBlocks(text);
673
+
674
+ for (const block of blocks) {
675
+ if (block.startLine <= start.line && start.line <= block.endLine) {
676
+ code = block.code;
677
+ selected = true;
678
+ break;
679
+ }
680
+ }
681
+ }
682
+
683
+ if (!selected) {
684
+ // no selection, submit whole line and advance
685
+ code = editor.getLine(selection.start.line);
686
+ const cursor = editor.getCursorPosition();
687
+ if (cursor.line + 1 === editor.lineCount) {
688
+ const text = editor.model.sharedModel.getSource();
689
+ editor.model.sharedModel.setSource(text + '\n');
690
+ }
691
+ editor.setCursorPosition({
692
+ line: cursor.line + 1,
693
+ column: cursor.column
694
+ });
695
+ }
696
+
697
+ const activate = false;
698
+ if (code) {
699
+ return commands.execute('console:inject', { activate, code, path });
700
+ } else {
701
+ return Promise.resolve(void 0);
702
+ }
703
+ },
704
+ isEnabled,
705
+ label: trans.__('Run Selected Code')
706
+ });
707
+
708
+ /**
709
+ * Add the Run All Code command
710
+ */
711
+ commands.addCommand(CommandIDs.runAllCode, {
712
+ execute: () => {
713
+ const widget = tracker.currentWidget?.content;
714
+
715
+ if (!widget) {
716
+ return;
717
+ }
718
+
719
+ let code = '';
720
+ const editor = widget.editor;
721
+ const text = editor.model.sharedModel.getSource();
722
+ const path = widget.context.path;
723
+ const extension = PathExt.extname(path);
724
+
725
+ if (MarkdownCodeBlocks.isMarkdown(extension)) {
726
+ // For Markdown files, run only code blocks.
727
+ const blocks = MarkdownCodeBlocks.findMarkdownCodeBlocks(text);
728
+ for (const block of blocks) {
729
+ code += block.code;
730
+ }
731
+ } else {
732
+ code = text;
733
+ }
734
+
735
+ const activate = false;
736
+ if (code) {
737
+ return commands.execute('console:inject', { activate, code, path });
738
+ } else {
739
+ return Promise.resolve(void 0);
740
+ }
741
+ },
742
+ isEnabled,
743
+ label: trans.__('Run All Code')
744
+ });
745
+
746
+ /**
747
+ * Add markdown preview command
748
+ */
749
+ commands.addCommand(CommandIDs.markdownPreview, {
750
+ execute: () => {
751
+ const widget = tracker.currentWidget;
752
+ if (!widget) {
753
+ return;
754
+ }
755
+ const path = widget.context.path;
756
+ return commands.execute('markdownviewer:open', {
757
+ path,
758
+ options: {
759
+ mode: 'split-right'
760
+ }
761
+ });
762
+ },
763
+ isVisible: () => {
764
+ const widget = tracker.currentWidget;
765
+ return (
766
+ (widget && PathExt.extname(widget.context.path) === '.md') || false
767
+ );
768
+ },
769
+ icon: markdownIcon,
770
+ label: trans.__('Show Markdown Preview')
771
+ });
772
+
773
+ /**
774
+ * Add the New File command
775
+ *
776
+ * Defaults to Text/.txt if file type data is not specified
777
+ */
778
+ commands.addCommand(CommandIDs.createNew, {
779
+ label: args => {
780
+ if (args.isPalette) {
781
+ return (args.paletteLabel as string) ?? trans.__('New Text File');
782
+ }
783
+ return (args.launcherLabel as string) ?? trans.__('Text File');
784
+ },
785
+ caption: args =>
786
+ (args.caption as string) ?? trans.__('Create a new text file'),
787
+ icon: args =>
788
+ args.isPalette
789
+ ? undefined
790
+ : LabIcon.resolve({
791
+ icon: (args.iconName as string) ?? textEditorIcon
792
+ }),
793
+ execute: args => {
794
+ const cwd = args.cwd || defaultBrowser.model.path;
795
+ return createNew(
796
+ commands,
797
+ cwd as string,
798
+ (args.fileExt as string) ?? 'txt'
799
+ );
800
+ }
801
+ });
802
+
803
+ /**
804
+ * Add the New Markdown File command
805
+ */
806
+ commands.addCommand(CommandIDs.createNewMarkdown, {
807
+ label: args =>
808
+ args['isPalette']
809
+ ? trans.__('New Markdown File')
810
+ : trans.__('Markdown File'),
811
+ caption: trans.__('Create a new markdown file'),
812
+ icon: args => (args['isPalette'] ? undefined : markdownIcon),
813
+ execute: args => {
814
+ const cwd = args['cwd'] || defaultBrowser.model.path;
815
+ return createNew(commands, cwd as string, 'md');
816
+ }
817
+ });
818
+
819
+ /**
820
+ * Add undo command
821
+ */
822
+ commands.addCommand(CommandIDs.undo, {
823
+ execute: () => {
824
+ const widget = tracker.currentWidget?.content;
825
+
826
+ if (!widget) {
827
+ return;
828
+ }
829
+
830
+ widget.editor.undo();
831
+ },
832
+ isEnabled: () => {
833
+ if (!isEnabled()) {
834
+ return false;
835
+ }
836
+
837
+ const widget = tracker.currentWidget?.content;
838
+
839
+ if (!widget) {
840
+ return false;
841
+ }
842
+ // Ideally enable it when there are undo events stored
843
+ // Reference issue #8590: Code mirror editor could expose the history of undo/redo events
844
+ return true;
845
+ },
846
+ icon: undoIcon.bindprops({ stylesheet: 'menuItem' }),
847
+ label: trans.__('Undo')
848
+ });
849
+
850
+ /**
851
+ * Add redo command
852
+ */
853
+ commands.addCommand(CommandIDs.redo, {
854
+ execute: () => {
855
+ const widget = tracker.currentWidget?.content;
856
+
857
+ if (!widget) {
858
+ return;
859
+ }
860
+
861
+ widget.editor.redo();
862
+ },
863
+ isEnabled: () => {
864
+ if (!isEnabled()) {
865
+ return false;
866
+ }
867
+
868
+ const widget = tracker.currentWidget?.content;
869
+
870
+ if (!widget) {
871
+ return false;
872
+ }
873
+ // Ideally enable it when there are redo events stored
874
+ // Reference issue #8590: Code mirror editor could expose the history of undo/redo events
875
+ return true;
876
+ },
877
+ icon: redoIcon.bindprops({ stylesheet: 'menuItem' }),
878
+ label: trans.__('Redo')
879
+ });
880
+
881
+ /**
882
+ * Add cut command
883
+ */
884
+ commands.addCommand(CommandIDs.cut, {
885
+ execute: () => {
886
+ const widget = tracker.currentWidget?.content;
887
+
888
+ if (!widget) {
889
+ return;
890
+ }
891
+
892
+ const editor = widget.editor as CodeMirrorEditor;
893
+ const text = getTextSelection(editor);
894
+
895
+ Clipboard.copyToSystem(text);
896
+ editor.replaceSelection && editor.replaceSelection('');
897
+ },
898
+ isEnabled: () => {
899
+ if (!isEnabled()) {
900
+ return false;
901
+ }
902
+
903
+ const widget = tracker.currentWidget?.content;
904
+
905
+ if (!widget) {
906
+ return false;
907
+ }
908
+
909
+ // Enable command if there is a text selection in the editor
910
+ return isSelected(widget.editor as CodeMirrorEditor);
911
+ },
912
+ icon: cutIcon.bindprops({ stylesheet: 'menuItem' }),
913
+ label: trans.__('Cut')
914
+ });
915
+
916
+ /**
917
+ * Add copy command
918
+ */
919
+ commands.addCommand(CommandIDs.copy, {
920
+ execute: () => {
921
+ const widget = tracker.currentWidget?.content;
922
+
923
+ if (!widget) {
924
+ return;
925
+ }
926
+
927
+ const editor = widget.editor as CodeMirrorEditor;
928
+ const text = getTextSelection(editor);
929
+
930
+ Clipboard.copyToSystem(text);
931
+ },
932
+ isEnabled: () => {
933
+ if (!isEnabled()) {
934
+ return false;
935
+ }
936
+
937
+ const widget = tracker.currentWidget?.content;
938
+
939
+ if (!widget) {
940
+ return false;
941
+ }
942
+
943
+ // Enable command if there is a text selection in the editor
944
+ return isSelected(widget.editor as CodeMirrorEditor);
945
+ },
946
+ icon: copyIcon.bindprops({ stylesheet: 'menuItem' }),
947
+ label: trans.__('Copy')
948
+ });
949
+
950
+ /**
951
+ * Add paste command
952
+ */
953
+ commands.addCommand(CommandIDs.paste, {
954
+ execute: async () => {
955
+ const widget = tracker.currentWidget?.content;
956
+
957
+ if (!widget) {
958
+ return;
959
+ }
960
+
961
+ const editor: CodeEditor.IEditor = widget.editor;
962
+
963
+ // Get data from clipboard
964
+ const clipboard = window.navigator.clipboard;
965
+ const clipboardData: string = await clipboard.readText();
966
+
967
+ if (clipboardData) {
968
+ // Paste data to the editor
969
+ editor.replaceSelection && editor.replaceSelection(clipboardData);
970
+ }
971
+ },
972
+ isEnabled: () => Boolean(isEnabled() && tracker.currentWidget?.content),
973
+ icon: pasteIcon.bindprops({ stylesheet: 'menuItem' }),
974
+ label: trans.__('Paste')
975
+ });
976
+
977
+ /**
978
+ * Add select all command
979
+ */
980
+ commands.addCommand(CommandIDs.selectAll, {
981
+ execute: () => {
982
+ const widget = tracker.currentWidget?.content;
983
+
984
+ if (!widget) {
985
+ return;
986
+ }
987
+
988
+ const editor = widget.editor as CodeMirrorEditor;
989
+ editor.execCommand(selectAll);
990
+ },
991
+ isEnabled: () => Boolean(isEnabled() && tracker.currentWidget?.content),
992
+ label: trans.__('Select All')
993
+ });
994
+ }
995
+
996
+ export function addCompleterCommands(
997
+ commands: CommandRegistry,
998
+ editorTracker: IEditorTracker,
999
+ manager: ICompletionProviderManager,
1000
+ translator: ITranslator | null
1001
+ ): void {
1002
+ const trans = (translator ?? nullTranslator).load('jupyterlab');
1003
+
1004
+ commands.addCommand(CommandIDs.invokeCompleter, {
1005
+ label: trans.__('Display the completion helper.'),
1006
+ execute: () => {
1007
+ const id =
1008
+ editorTracker.currentWidget && editorTracker.currentWidget.id;
1009
+ if (id) {
1010
+ return manager.invoke(id);
1011
+ }
1012
+ }
1013
+ });
1014
+
1015
+ commands.addCommand(CommandIDs.selectCompleter, {
1016
+ label: trans.__('Select the completion suggestion.'),
1017
+ execute: () => {
1018
+ const id =
1019
+ editorTracker.currentWidget && editorTracker.currentWidget.id;
1020
+ if (id) {
1021
+ return manager.select(id);
1022
+ }
1023
+ }
1024
+ });
1025
+
1026
+ commands.addKeyBinding({
1027
+ command: CommandIDs.selectCompleter,
1028
+ keys: ['Enter'],
1029
+ selector: '.jp-FileEditor .jp-mod-completer-active'
1030
+ });
1031
+ }
1032
+
1033
+ /**
1034
+ * Helper function to check if there is a text selection in the editor
1035
+ */
1036
+ function isSelected(editor: CodeMirrorEditor) {
1037
+ const selectionObj = editor.getSelection();
1038
+ const { start, end } = selectionObj;
1039
+ const selected = start.column !== end.column || start.line !== end.line;
1040
+
1041
+ return selected;
1042
+ }
1043
+
1044
+ /**
1045
+ * Helper function to get text selection from the editor
1046
+ */
1047
+ function getTextSelection(editor: CodeMirrorEditor) {
1048
+ const selectionObj = editor.getSelection();
1049
+ const start = editor.getOffsetAt(selectionObj.start);
1050
+ const end = editor.getOffsetAt(selectionObj.end);
1051
+ const text = editor.model.sharedModel.getSource().substring(start, end);
1052
+
1053
+ return text;
1054
+ }
1055
+
1056
+ /**
1057
+ * Function to create a new untitled text file, given the current working directory.
1058
+ */
1059
+ async function createNew(
1060
+ commands: CommandRegistry,
1061
+ cwd: string,
1062
+ ext: string = 'txt'
1063
+ ) {
1064
+ const model = await commands.execute('docmanager:new-untitled', {
1065
+ path: cwd,
1066
+ type: 'file',
1067
+ ext
1068
+ });
1069
+ if (model != undefined) {
1070
+ const widget = (await commands.execute('docmanager:open', {
1071
+ path: model.path,
1072
+ factory: FACTORY
1073
+ })) as unknown as IDocumentWidget;
1074
+ widget.isUntitled = true;
1075
+ return widget;
1076
+ }
1077
+ }
1078
+
1079
+ /**
1080
+ * Wrapper function for adding the default launcher items for File Editor
1081
+ */
1082
+ export function addLauncherItems(
1083
+ launcher: ILauncher,
1084
+ trans: TranslationBundle
1085
+ ): void {
1086
+ addCreateNewToLauncher(launcher, trans);
1087
+
1088
+ addCreateNewMarkdownToLauncher(launcher, trans);
1089
+ }
1090
+
1091
+ /**
1092
+ * Add Create New Text File to the Launcher
1093
+ */
1094
+ export function addCreateNewToLauncher(
1095
+ launcher: ILauncher,
1096
+ trans: TranslationBundle
1097
+ ): void {
1098
+ launcher.add({
1099
+ command: CommandIDs.createNew,
1100
+ category: trans.__('Other'),
1101
+ rank: 1
1102
+ });
1103
+ }
1104
+
1105
+ /**
1106
+ * Add Create New Markdown to the Launcher
1107
+ */
1108
+ export function addCreateNewMarkdownToLauncher(
1109
+ launcher: ILauncher,
1110
+ trans: TranslationBundle
1111
+ ): void {
1112
+ launcher.add({
1113
+ command: CommandIDs.createNewMarkdown,
1114
+ category: trans.__('Other'),
1115
+ rank: 2
1116
+ });
1117
+ }
1118
+
1119
+ /**
1120
+ * Add ___ File items to the Launcher for common file types associated with available kernels
1121
+ */
1122
+ export function addKernelLanguageLauncherItems(
1123
+ launcher: ILauncher,
1124
+ trans: TranslationBundle,
1125
+ availableKernelFileTypes: Iterable<IFileTypeData>
1126
+ ): void {
1127
+ for (let ext of availableKernelFileTypes) {
1128
+ launcher.add({
1129
+ command: CommandIDs.createNew,
1130
+ category: trans.__('Other'),
1131
+ rank: 3,
1132
+ args: ext
1133
+ });
1134
+ }
1135
+ }
1136
+
1137
+ /**
1138
+ * Wrapper function for adding the default items to the File Editor palette
1139
+ */
1140
+ export function addPaletteItems(
1141
+ palette: ICommandPalette,
1142
+ trans: TranslationBundle
1143
+ ): void {
1144
+ addChangeTabsCommandsToPalette(palette, trans);
1145
+
1146
+ addCreateNewCommandToPalette(palette, trans);
1147
+
1148
+ addCreateNewMarkdownCommandToPalette(palette, trans);
1149
+
1150
+ addChangeFontSizeCommandsToPalette(palette, trans);
1151
+ }
1152
+
1153
+ /**
1154
+ * Add commands to change the tab indentation to the File Editor palette
1155
+ */
1156
+ export function addChangeTabsCommandsToPalette(
1157
+ palette: ICommandPalette,
1158
+ trans: TranslationBundle
1159
+ ): void {
1160
+ const paletteCategory = trans.__('Text Editor');
1161
+ const args: JSONObject = {
1162
+ size: 4
1163
+ };
1164
+ const command = CommandIDs.changeTabs;
1165
+ palette.addItem({ command, args, category: paletteCategory });
1166
+
1167
+ for (const size of [1, 2, 4, 8]) {
1168
+ const args: JSONObject = {
1169
+ size
1170
+ };
1171
+ palette.addItem({ command, args, category: paletteCategory });
1172
+ }
1173
+ }
1174
+
1175
+ /**
1176
+ * Add a Create New File command to the File Editor palette
1177
+ */
1178
+ export function addCreateNewCommandToPalette(
1179
+ palette: ICommandPalette,
1180
+ trans: TranslationBundle
1181
+ ): void {
1182
+ const paletteCategory = trans.__('Text Editor');
1183
+ palette.addItem({
1184
+ command: CommandIDs.createNew,
1185
+ args: { isPalette: true },
1186
+ category: paletteCategory
1187
+ });
1188
+ }
1189
+
1190
+ /**
1191
+ * Add a Create New Markdown command to the File Editor palette
1192
+ */
1193
+ export function addCreateNewMarkdownCommandToPalette(
1194
+ palette: ICommandPalette,
1195
+ trans: TranslationBundle
1196
+ ): void {
1197
+ const paletteCategory = trans.__('Text Editor');
1198
+ palette.addItem({
1199
+ command: CommandIDs.createNewMarkdown,
1200
+ args: { isPalette: true },
1201
+ category: paletteCategory
1202
+ });
1203
+ }
1204
+
1205
+ /**
1206
+ * Add commands to change the font size to the File Editor palette
1207
+ */
1208
+ export function addChangeFontSizeCommandsToPalette(
1209
+ palette: ICommandPalette,
1210
+ trans: TranslationBundle
1211
+ ): void {
1212
+ const paletteCategory = trans.__('Text Editor');
1213
+ const command = CommandIDs.changeFontSize;
1214
+
1215
+ let args = { delta: 1 };
1216
+ palette.addItem({ command, args, category: paletteCategory });
1217
+
1218
+ args = { delta: -1 };
1219
+ palette.addItem({ command, args, category: paletteCategory });
1220
+ }
1221
+
1222
+ /**
1223
+ * Add New ___ File commands to the File Editor palette for common file types associated with available kernels
1224
+ */
1225
+ export function addKernelLanguagePaletteItems(
1226
+ palette: ICommandPalette,
1227
+ trans: TranslationBundle,
1228
+ availableKernelFileTypes: Iterable<IFileTypeData>
1229
+ ): void {
1230
+ const paletteCategory = trans.__('Text Editor');
1231
+ for (let ext of availableKernelFileTypes) {
1232
+ palette.addItem({
1233
+ command: CommandIDs.createNew,
1234
+ args: { ...ext, isPalette: true },
1235
+ category: paletteCategory
1236
+ });
1237
+ }
1238
+ }
1239
+
1240
+ /**
1241
+ * Wrapper function for adding the default menu items for File Editor
1242
+ */
1243
+ export function addMenuItems(
1244
+ menu: IMainMenu,
1245
+ tracker: WidgetTracker<IDocumentWidget<FileEditor>>,
1246
+ consoleTracker: IConsoleTracker | null,
1247
+ isEnabled: () => boolean
1248
+ ): void {
1249
+ // Add undo/redo hooks to the edit menu.
1250
+ menu.editMenu.undoers.redo.add({
1251
+ id: CommandIDs.redo,
1252
+ isEnabled
1253
+ });
1254
+ menu.editMenu.undoers.undo.add({
1255
+ id: CommandIDs.undo,
1256
+ isEnabled
1257
+ });
1258
+
1259
+ // Add editor view options.
1260
+ menu.viewMenu.editorViewers.toggleLineNumbers.add({
1261
+ id: CommandIDs.currentLineNumbers,
1262
+ isEnabled
1263
+ });
1264
+ menu.viewMenu.editorViewers.toggleMatchBrackets.add({
1265
+ id: CommandIDs.currentMatchBrackets,
1266
+ isEnabled
1267
+ });
1268
+ menu.viewMenu.editorViewers.toggleWordWrap.add({
1269
+ id: CommandIDs.currentLineWrap,
1270
+ isEnabled
1271
+ });
1272
+
1273
+ // Add a console creator the the file menu.
1274
+ menu.fileMenu.consoleCreators.add({
1275
+ id: CommandIDs.createConsole,
1276
+ isEnabled
1277
+ });
1278
+
1279
+ // Add a code runner to the run menu.
1280
+ if (consoleTracker) {
1281
+ addCodeRunnersToRunMenu(menu, consoleTracker);
1282
+ }
1283
+ }
1284
+
1285
+ /**
1286
+ * Add Create New ___ File commands to the File menu for common file types associated with available kernels
1287
+ */
1288
+ export function addKernelLanguageMenuItems(
1289
+ menu: IMainMenu,
1290
+ availableKernelFileTypes: Iterable<IFileTypeData>
1291
+ ): void {
1292
+ for (let ext of availableKernelFileTypes) {
1293
+ menu.fileMenu.newMenu.addItem({
1294
+ command: CommandIDs.createNew,
1295
+ args: ext,
1296
+ rank: 31
1297
+ });
1298
+ }
1299
+ }
1300
+
1301
+ /**
1302
+ * Add a File Editor code runner to the Run menu
1303
+ */
1304
+ export function addCodeRunnersToRunMenu(
1305
+ menu: IMainMenu,
1306
+ consoleTracker: IConsoleTracker
1307
+ ): void {
1308
+ const isEnabled = (current: IDocumentWidget<FileEditor>) =>
1309
+ current.context &&
1310
+ !!consoleTracker.find(
1311
+ widget => widget.sessionContext.session?.path === current.context.path
1312
+ );
1313
+ menu.runMenu.codeRunners.restart.add({
1314
+ id: CommandIDs.restartConsole,
1315
+ isEnabled
1316
+ });
1317
+ menu.runMenu.codeRunners.run.add({
1318
+ id: CommandIDs.runCode,
1319
+ isEnabled
1320
+ });
1321
+ menu.runMenu.codeRunners.runAll.add({
1322
+ id: CommandIDs.runAllCode,
1323
+ isEnabled
1324
+ });
1325
+ }
1326
+
1327
+ export function addOpenCodeViewerCommand(
1328
+ app: JupyterFrontEnd,
1329
+ editorServices: IEditorServices,
1330
+ tracker: WidgetTracker<MainAreaWidget<CodeViewerWidget>>,
1331
+ trans: TranslationBundle
1332
+ ): void {
1333
+ const openCodeViewer = async (args: {
1334
+ content: string;
1335
+ label?: string;
1336
+ mimeType?: string;
1337
+ extension?: string;
1338
+ widgetId?: string;
1339
+ }): Promise<CodeViewerWidget> => {
1340
+ const func = editorServices.factoryService.newDocumentEditor;
1341
+ const factory: CodeEditor.Factory = options => {
1342
+ return func(options);
1343
+ };
1344
+
1345
+ // Derive mimetype from extension
1346
+ let mimetype = args.mimeType;
1347
+ if (!mimetype && args.extension) {
1348
+ mimetype = editorServices.mimeTypeService.getMimeTypeByFilePath(
1349
+ `temp.${args.extension.replace(/\\.$/, '')}`
1350
+ );
1351
+ }
1352
+
1353
+ const widget = CodeViewerWidget.createCodeViewer({
1354
+ factory,
1355
+ content: args.content,
1356
+ mimeType: mimetype
1357
+ });
1358
+ widget.title.label = args.label || trans.__('Code Viewer');
1359
+ widget.title.caption = widget.title.label;
1360
+
1361
+ // Get the fileType based on the mimetype to determine the icon
1362
+ const fileType = find(app.docRegistry.fileTypes(), fileType =>
1363
+ mimetype ? fileType.mimeTypes.includes(mimetype) : false
1364
+ );
1365
+ widget.title.icon = fileType?.icon ?? textEditorIcon;
1366
+
1367
+ if (args.widgetId) {
1368
+ widget.id = args.widgetId;
1369
+ }
1370
+ const main = new MainAreaWidget({ content: widget });
1371
+ await tracker.add(main);
1372
+ app.shell.add(main, 'main');
1373
+ return widget;
1374
+ };
1375
+
1376
+ app.commands.addCommand(CommandIDs.openCodeViewer, {
1377
+ label: trans.__('Open Code Viewer'),
1378
+ execute: (args: any) => {
1379
+ return openCodeViewer(args);
1380
+ }
1381
+ });
1382
+ }
1383
+ }