@jupyterlab/fileeditor-extension 4.0.0-alpha.8 → 4.0.0-beta.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.
@@ -0,0 +1,1382 @@
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
+ return trans.__('Spaces: %1', args.size ?? '');
382
+ } else {
383
+ return trans.__('Indent with Tab');
384
+ }
385
+ },
386
+ execute: async args => {
387
+ config.indentUnit =
388
+ args['size'] !== undefined
389
+ ? ((args['size'] as string) ?? '4').toString()
390
+ : 'Tab';
391
+ try {
392
+ return await settingRegistry.set(id, 'editorConfig', config);
393
+ } catch (reason) {
394
+ console.error(`Failed to set ${id}: ${reason.message}`);
395
+ }
396
+ },
397
+ isToggled: args => {
398
+ const currentIndentUnit =
399
+ config.indentUnit ?? extensions.baseConfiguration.indentUnit;
400
+ return args['size']
401
+ ? args['size'] === currentIndentUnit
402
+ : 'Tab' == currentIndentUnit;
403
+ }
404
+ });
405
+
406
+ /**
407
+ * Add the Match Brackets command
408
+ */
409
+ commands.addCommand(CommandIDs.matchBrackets, {
410
+ execute: async () => {
411
+ config.matchBrackets = !(
412
+ config.matchBrackets ?? extensions.baseConfiguration.matchBrackets
413
+ );
414
+ try {
415
+ return await settingRegistry.set(id, 'editorConfig', config);
416
+ } catch (reason) {
417
+ console.error(`Failed to set ${id}: ${reason.message}`);
418
+ }
419
+ },
420
+ label: trans.__('Match Brackets'),
421
+ isEnabled,
422
+ isToggled: () =>
423
+ config.matchBrackets ?? extensions.baseConfiguration.matchBrackets
424
+ });
425
+
426
+ commands.addCommand(CommandIDs.currentMatchBrackets, {
427
+ label: trans.__('Match Brackets'),
428
+ caption: trans.__('Change match brackets for the current file.'),
429
+ execute: () => {
430
+ const widget = tracker.currentWidget;
431
+ if (!widget) {
432
+ return;
433
+ }
434
+ const matchBrackets = !widget.content.editor.getOption('matchBrackets');
435
+ widget.content.editor.setOption('matchBrackets', matchBrackets);
436
+ },
437
+ isEnabled,
438
+ isToggled: () => {
439
+ const widget = tracker.currentWidget;
440
+ return (
441
+ (widget?.content.editor.getOption('matchBrackets') as
442
+ | boolean
443
+ | undefined) ?? false
444
+ );
445
+ }
446
+ });
447
+
448
+ /**
449
+ * Add the Auto Close Brackets for Text Editor command
450
+ */
451
+ commands.addCommand(CommandIDs.autoClosingBrackets, {
452
+ execute: async args => {
453
+ config.autoClosingBrackets = !!(
454
+ args['force'] ??
455
+ !(
456
+ config.autoClosingBrackets ??
457
+ extensions.baseConfiguration.autoClosingBrackets
458
+ )
459
+ );
460
+ try {
461
+ return await settingRegistry.set(id, 'editorConfig', config);
462
+ } catch (reason) {
463
+ console.error(`Failed to set ${id}: ${reason.message}`);
464
+ }
465
+ },
466
+ label: trans.__('Auto Close Brackets in Text Editor'),
467
+ isToggled: () =>
468
+ config.autoClosingBrackets ??
469
+ extensions.baseConfiguration.autoClosingBrackets
470
+ });
471
+
472
+ commands.addCommand(CommandIDs.autoClosingBracketsUniversal, {
473
+ execute: () => {
474
+ const anyToggled =
475
+ commands.isToggled(CommandIDs.autoClosingBrackets) ||
476
+ commands.isToggled(autoClosingBracketsNotebook) ||
477
+ commands.isToggled(autoClosingBracketsConsole);
478
+ // if any auto closing brackets options is toggled, toggle both off
479
+ if (anyToggled) {
480
+ void commands.execute(CommandIDs.autoClosingBrackets, {
481
+ force: false
482
+ });
483
+ void commands.execute(autoClosingBracketsNotebook, { force: false });
484
+ void commands.execute(autoClosingBracketsConsole, { force: false });
485
+ } else {
486
+ // both are off, turn them on
487
+ void commands.execute(CommandIDs.autoClosingBrackets, {
488
+ force: true
489
+ });
490
+ void commands.execute(autoClosingBracketsNotebook, { force: true });
491
+ void commands.execute(autoClosingBracketsConsole, { force: true });
492
+ }
493
+ },
494
+ label: trans.__('Auto Close Brackets'),
495
+ isToggled: () =>
496
+ commands.isToggled(CommandIDs.autoClosingBrackets) ||
497
+ commands.isToggled(autoClosingBracketsNotebook) ||
498
+ commands.isToggled(autoClosingBracketsConsole)
499
+ });
500
+
501
+ /**
502
+ * Create a menu for the editor.
503
+ */
504
+ commands.addCommand(CommandIDs.changeTheme, {
505
+ label: args =>
506
+ ((args.displayName ?? args.theme) as string) ??
507
+ config.theme ??
508
+ extensions.baseConfiguration.theme ??
509
+ trans.__('Editor Theme'),
510
+ execute: async args => {
511
+ config.theme = (args['theme'] as string) ?? config.theme;
512
+
513
+ try {
514
+ return await settingRegistry.set(id, 'editorConfig', config);
515
+ } catch (reason) {
516
+ console.error(`Failed to set theme - ${reason.message}`);
517
+ }
518
+ },
519
+ isToggled: args =>
520
+ args['theme'] === (config.theme ?? extensions.baseConfiguration.theme)
521
+ });
522
+
523
+ commands.addCommand(CommandIDs.find, {
524
+ label: trans.__('Find…'),
525
+ execute: () => {
526
+ const widget = tracker.currentWidget;
527
+ if (!widget) {
528
+ return;
529
+ }
530
+ const editor = widget.content.editor as CodeMirrorEditor;
531
+ editor.execCommand(findNext);
532
+ },
533
+ isEnabled
534
+ });
535
+
536
+ commands.addCommand(CommandIDs.goToLine, {
537
+ label: trans.__('Go to Line…'),
538
+ execute: args => {
539
+ const widget = tracker.currentWidget;
540
+ if (!widget) {
541
+ return;
542
+ }
543
+ const editor = widget.content.editor as CodeMirrorEditor;
544
+
545
+ const line = args['line'] as number | undefined;
546
+ const column = args['column'] as number | undefined;
547
+ if (line !== undefined || column !== undefined) {
548
+ editor.setCursorPosition({
549
+ line: (line ?? 1) - 1,
550
+ column: (column ?? 1) - 1
551
+ });
552
+ } else {
553
+ editor.execCommand(gotoLine);
554
+ }
555
+ },
556
+ isEnabled
557
+ });
558
+
559
+ commands.addCommand(CommandIDs.changeLanguage, {
560
+ label: args =>
561
+ ((args['displayName'] ?? args['name']) as string) ??
562
+ trans.__('Change editor language.'),
563
+ execute: args => {
564
+ const name = args['name'] as string;
565
+ const widget = tracker.currentWidget;
566
+ if (name && widget) {
567
+ const spec = languages.findByName(name);
568
+ if (spec) {
569
+ widget.content.model.mimeType = spec.mime as string;
570
+ }
571
+ }
572
+ },
573
+ isEnabled,
574
+ isToggled: args => {
575
+ const widget = tracker.currentWidget;
576
+ if (!widget) {
577
+ return false;
578
+ }
579
+ const mime = widget.content.model.mimeType;
580
+ const spec = languages.findByMIME(mime);
581
+ const name = spec && spec.name;
582
+ return args['name'] === name;
583
+ }
584
+ });
585
+
586
+ /**
587
+ * Add the replace selection for text editor command
588
+ */
589
+
590
+ commands.addCommand(CommandIDs.replaceSelection, {
591
+ execute: args => {
592
+ const text: string = (args['text'] as string) || '';
593
+ const widget = tracker.currentWidget;
594
+ if (!widget) {
595
+ return;
596
+ }
597
+ widget.content.editor.replaceSelection?.(text);
598
+ },
599
+ isEnabled,
600
+ label: trans.__('Replace Selection in Editor')
601
+ });
602
+
603
+ /**
604
+ * Add the Create Console for Editor command
605
+ */
606
+ commands.addCommand(CommandIDs.createConsole, {
607
+ execute: args => {
608
+ const widget = tracker.currentWidget;
609
+
610
+ if (!widget) {
611
+ return;
612
+ }
613
+
614
+ return getCreateConsoleFunction(commands, languages)(widget, args);
615
+ },
616
+ isEnabled,
617
+ icon: consoleIcon,
618
+ label: trans.__('Create Console for Editor')
619
+ });
620
+
621
+ /**
622
+ * Restart the Console Kernel linked to the current Editor
623
+ */
624
+ commands.addCommand(CommandIDs.restartConsole, {
625
+ execute: async () => {
626
+ const current = tracker.currentWidget?.content;
627
+
628
+ if (!current || consoleTracker === null) {
629
+ return;
630
+ }
631
+
632
+ const widget = consoleTracker.find(
633
+ widget => widget.sessionContext.session?.path === current.context.path
634
+ );
635
+ if (widget) {
636
+ return sessionDialogs.restart(widget.sessionContext);
637
+ }
638
+ },
639
+ label: trans.__('Restart Kernel'),
640
+ isEnabled: () => consoleTracker !== null && isEnabled()
641
+ });
642
+
643
+ /**
644
+ * Add the Run Code command
645
+ */
646
+ commands.addCommand(CommandIDs.runCode, {
647
+ execute: () => {
648
+ // Run the appropriate code, taking into account a ```fenced``` code block.
649
+ const widget = tracker.currentWidget?.content;
650
+
651
+ if (!widget) {
652
+ return;
653
+ }
654
+
655
+ let code: string | undefined = '';
656
+ const editor = widget.editor;
657
+ const path = widget.context.path;
658
+ const extension = PathExt.extname(path);
659
+ const selection = editor.getSelection();
660
+ const { start, end } = selection;
661
+ let selected = start.column !== end.column || start.line !== end.line;
662
+
663
+ if (selected) {
664
+ // Get the selected code from the editor.
665
+ const start = editor.getOffsetAt(selection.start);
666
+ const end = editor.getOffsetAt(selection.end);
667
+
668
+ code = editor.model.sharedModel.getSource().substring(start, end);
669
+ } else if (MarkdownCodeBlocks.isMarkdown(extension)) {
670
+ const text = editor.model.sharedModel.getSource();
671
+ const blocks = MarkdownCodeBlocks.findMarkdownCodeBlocks(text);
672
+
673
+ for (const block of blocks) {
674
+ if (block.startLine <= start.line && start.line <= block.endLine) {
675
+ code = block.code;
676
+ selected = true;
677
+ break;
678
+ }
679
+ }
680
+ }
681
+
682
+ if (!selected) {
683
+ // no selection, submit whole line and advance
684
+ code = editor.getLine(selection.start.line);
685
+ const cursor = editor.getCursorPosition();
686
+ if (cursor.line + 1 === editor.lineCount) {
687
+ const text = editor.model.sharedModel.getSource();
688
+ editor.model.sharedModel.setSource(text + '\n');
689
+ }
690
+ editor.setCursorPosition({
691
+ line: cursor.line + 1,
692
+ column: cursor.column
693
+ });
694
+ }
695
+
696
+ const activate = false;
697
+ if (code) {
698
+ return commands.execute('console:inject', { activate, code, path });
699
+ } else {
700
+ return Promise.resolve(void 0);
701
+ }
702
+ },
703
+ isEnabled,
704
+ label: trans.__('Run Selected Code')
705
+ });
706
+
707
+ /**
708
+ * Add the Run All Code command
709
+ */
710
+ commands.addCommand(CommandIDs.runAllCode, {
711
+ execute: () => {
712
+ const widget = tracker.currentWidget?.content;
713
+
714
+ if (!widget) {
715
+ return;
716
+ }
717
+
718
+ let code = '';
719
+ const editor = widget.editor;
720
+ const text = editor.model.sharedModel.getSource();
721
+ const path = widget.context.path;
722
+ const extension = PathExt.extname(path);
723
+
724
+ if (MarkdownCodeBlocks.isMarkdown(extension)) {
725
+ // For Markdown files, run only code blocks.
726
+ const blocks = MarkdownCodeBlocks.findMarkdownCodeBlocks(text);
727
+ for (const block of blocks) {
728
+ code += block.code;
729
+ }
730
+ } else {
731
+ code = text;
732
+ }
733
+
734
+ const activate = false;
735
+ if (code) {
736
+ return commands.execute('console:inject', { activate, code, path });
737
+ } else {
738
+ return Promise.resolve(void 0);
739
+ }
740
+ },
741
+ isEnabled,
742
+ label: trans.__('Run All Code')
743
+ });
744
+
745
+ /**
746
+ * Add markdown preview command
747
+ */
748
+ commands.addCommand(CommandIDs.markdownPreview, {
749
+ execute: () => {
750
+ const widget = tracker.currentWidget;
751
+ if (!widget) {
752
+ return;
753
+ }
754
+ const path = widget.context.path;
755
+ return commands.execute('markdownviewer:open', {
756
+ path,
757
+ options: {
758
+ mode: 'split-right'
759
+ }
760
+ });
761
+ },
762
+ isVisible: () => {
763
+ const widget = tracker.currentWidget;
764
+ return (
765
+ (widget && PathExt.extname(widget.context.path) === '.md') || false
766
+ );
767
+ },
768
+ icon: markdownIcon,
769
+ label: trans.__('Show Markdown Preview')
770
+ });
771
+
772
+ /**
773
+ * Add the New File command
774
+ *
775
+ * Defaults to Text/.txt if file type data is not specified
776
+ */
777
+ commands.addCommand(CommandIDs.createNew, {
778
+ label: args => {
779
+ if (args.isPalette) {
780
+ return (args.paletteLabel as string) ?? trans.__('New Text File');
781
+ }
782
+ return (args.launcherLabel as string) ?? trans.__('Text File');
783
+ },
784
+ caption: args =>
785
+ (args.caption as string) ?? trans.__('Create a new text file'),
786
+ icon: args =>
787
+ args.isPalette
788
+ ? undefined
789
+ : LabIcon.resolve({
790
+ icon: (args.iconName as string) ?? textEditorIcon
791
+ }),
792
+ execute: args => {
793
+ const cwd = args.cwd || defaultBrowser.model.path;
794
+ return createNew(
795
+ commands,
796
+ cwd as string,
797
+ (args.fileExt as string) ?? 'txt'
798
+ );
799
+ }
800
+ });
801
+
802
+ /**
803
+ * Add the New Markdown File command
804
+ */
805
+ commands.addCommand(CommandIDs.createNewMarkdown, {
806
+ label: args =>
807
+ args['isPalette']
808
+ ? trans.__('New Markdown File')
809
+ : trans.__('Markdown File'),
810
+ caption: trans.__('Create a new markdown file'),
811
+ icon: args => (args['isPalette'] ? undefined : markdownIcon),
812
+ execute: args => {
813
+ const cwd = args['cwd'] || defaultBrowser.model.path;
814
+ return createNew(commands, cwd as string, 'md');
815
+ }
816
+ });
817
+
818
+ /**
819
+ * Add undo command
820
+ */
821
+ commands.addCommand(CommandIDs.undo, {
822
+ execute: () => {
823
+ const widget = tracker.currentWidget?.content;
824
+
825
+ if (!widget) {
826
+ return;
827
+ }
828
+
829
+ widget.editor.undo();
830
+ },
831
+ isEnabled: () => {
832
+ if (!isEnabled()) {
833
+ return false;
834
+ }
835
+
836
+ const widget = tracker.currentWidget?.content;
837
+
838
+ if (!widget) {
839
+ return false;
840
+ }
841
+ // Ideally enable it when there are undo events stored
842
+ // Reference issue #8590: Code mirror editor could expose the history of undo/redo events
843
+ return true;
844
+ },
845
+ icon: undoIcon.bindprops({ stylesheet: 'menuItem' }),
846
+ label: trans.__('Undo')
847
+ });
848
+
849
+ /**
850
+ * Add redo command
851
+ */
852
+ commands.addCommand(CommandIDs.redo, {
853
+ execute: () => {
854
+ const widget = tracker.currentWidget?.content;
855
+
856
+ if (!widget) {
857
+ return;
858
+ }
859
+
860
+ widget.editor.redo();
861
+ },
862
+ isEnabled: () => {
863
+ if (!isEnabled()) {
864
+ return false;
865
+ }
866
+
867
+ const widget = tracker.currentWidget?.content;
868
+
869
+ if (!widget) {
870
+ return false;
871
+ }
872
+ // Ideally enable it when there are redo events stored
873
+ // Reference issue #8590: Code mirror editor could expose the history of undo/redo events
874
+ return true;
875
+ },
876
+ icon: redoIcon.bindprops({ stylesheet: 'menuItem' }),
877
+ label: trans.__('Redo')
878
+ });
879
+
880
+ /**
881
+ * Add cut command
882
+ */
883
+ commands.addCommand(CommandIDs.cut, {
884
+ execute: () => {
885
+ const widget = tracker.currentWidget?.content;
886
+
887
+ if (!widget) {
888
+ return;
889
+ }
890
+
891
+ const editor = widget.editor as CodeMirrorEditor;
892
+ const text = getTextSelection(editor);
893
+
894
+ Clipboard.copyToSystem(text);
895
+ editor.replaceSelection && editor.replaceSelection('');
896
+ },
897
+ isEnabled: () => {
898
+ if (!isEnabled()) {
899
+ return false;
900
+ }
901
+
902
+ const widget = tracker.currentWidget?.content;
903
+
904
+ if (!widget) {
905
+ return false;
906
+ }
907
+
908
+ // Enable command if there is a text selection in the editor
909
+ return isSelected(widget.editor as CodeMirrorEditor);
910
+ },
911
+ icon: cutIcon.bindprops({ stylesheet: 'menuItem' }),
912
+ label: trans.__('Cut')
913
+ });
914
+
915
+ /**
916
+ * Add copy command
917
+ */
918
+ commands.addCommand(CommandIDs.copy, {
919
+ execute: () => {
920
+ const widget = tracker.currentWidget?.content;
921
+
922
+ if (!widget) {
923
+ return;
924
+ }
925
+
926
+ const editor = widget.editor as CodeMirrorEditor;
927
+ const text = getTextSelection(editor);
928
+
929
+ Clipboard.copyToSystem(text);
930
+ },
931
+ isEnabled: () => {
932
+ if (!isEnabled()) {
933
+ return false;
934
+ }
935
+
936
+ const widget = tracker.currentWidget?.content;
937
+
938
+ if (!widget) {
939
+ return false;
940
+ }
941
+
942
+ // Enable command if there is a text selection in the editor
943
+ return isSelected(widget.editor as CodeMirrorEditor);
944
+ },
945
+ icon: copyIcon.bindprops({ stylesheet: 'menuItem' }),
946
+ label: trans.__('Copy')
947
+ });
948
+
949
+ /**
950
+ * Add paste command
951
+ */
952
+ commands.addCommand(CommandIDs.paste, {
953
+ execute: async () => {
954
+ const widget = tracker.currentWidget?.content;
955
+
956
+ if (!widget) {
957
+ return;
958
+ }
959
+
960
+ const editor: CodeEditor.IEditor = widget.editor;
961
+
962
+ // Get data from clipboard
963
+ const clipboard = window.navigator.clipboard;
964
+ const clipboardData: string = await clipboard.readText();
965
+
966
+ if (clipboardData) {
967
+ // Paste data to the editor
968
+ editor.replaceSelection && editor.replaceSelection(clipboardData);
969
+ }
970
+ },
971
+ isEnabled: () => Boolean(isEnabled() && tracker.currentWidget?.content),
972
+ icon: pasteIcon.bindprops({ stylesheet: 'menuItem' }),
973
+ label: trans.__('Paste')
974
+ });
975
+
976
+ /**
977
+ * Add select all command
978
+ */
979
+ commands.addCommand(CommandIDs.selectAll, {
980
+ execute: () => {
981
+ const widget = tracker.currentWidget?.content;
982
+
983
+ if (!widget) {
984
+ return;
985
+ }
986
+
987
+ const editor = widget.editor as CodeMirrorEditor;
988
+ editor.execCommand(selectAll);
989
+ },
990
+ isEnabled: () => Boolean(isEnabled() && tracker.currentWidget?.content),
991
+ label: trans.__('Select All')
992
+ });
993
+ }
994
+
995
+ export function addCompleterCommands(
996
+ commands: CommandRegistry,
997
+ editorTracker: IEditorTracker,
998
+ manager: ICompletionProviderManager,
999
+ translator: ITranslator | null
1000
+ ): void {
1001
+ const trans = (translator ?? nullTranslator).load('jupyterlab');
1002
+
1003
+ commands.addCommand(CommandIDs.invokeCompleter, {
1004
+ label: trans.__('Display the completion helper.'),
1005
+ execute: () => {
1006
+ const id =
1007
+ editorTracker.currentWidget && editorTracker.currentWidget.id;
1008
+ if (id) {
1009
+ return manager.invoke(id);
1010
+ }
1011
+ }
1012
+ });
1013
+
1014
+ commands.addCommand(CommandIDs.selectCompleter, {
1015
+ label: trans.__('Select the completion suggestion.'),
1016
+ execute: () => {
1017
+ const id =
1018
+ editorTracker.currentWidget && editorTracker.currentWidget.id;
1019
+ if (id) {
1020
+ return manager.select(id);
1021
+ }
1022
+ }
1023
+ });
1024
+
1025
+ commands.addKeyBinding({
1026
+ command: CommandIDs.selectCompleter,
1027
+ keys: ['Enter'],
1028
+ selector: '.jp-FileEditor .jp-mod-completer-active'
1029
+ });
1030
+ }
1031
+
1032
+ /**
1033
+ * Helper function to check if there is a text selection in the editor
1034
+ */
1035
+ function isSelected(editor: CodeMirrorEditor) {
1036
+ const selectionObj = editor.getSelection();
1037
+ const { start, end } = selectionObj;
1038
+ const selected = start.column !== end.column || start.line !== end.line;
1039
+
1040
+ return selected;
1041
+ }
1042
+
1043
+ /**
1044
+ * Helper function to get text selection from the editor
1045
+ */
1046
+ function getTextSelection(editor: CodeMirrorEditor) {
1047
+ const selectionObj = editor.getSelection();
1048
+ const start = editor.getOffsetAt(selectionObj.start);
1049
+ const end = editor.getOffsetAt(selectionObj.end);
1050
+ const text = editor.model.sharedModel.getSource().substring(start, end);
1051
+
1052
+ return text;
1053
+ }
1054
+
1055
+ /**
1056
+ * Function to create a new untitled text file, given the current working directory.
1057
+ */
1058
+ async function createNew(
1059
+ commands: CommandRegistry,
1060
+ cwd: string,
1061
+ ext: string = 'txt'
1062
+ ) {
1063
+ const model = await commands.execute('docmanager:new-untitled', {
1064
+ path: cwd,
1065
+ type: 'file',
1066
+ ext
1067
+ });
1068
+ if (model != undefined) {
1069
+ const widget = (await commands.execute('docmanager:open', {
1070
+ path: model.path,
1071
+ factory: FACTORY
1072
+ })) as unknown as IDocumentWidget;
1073
+ widget.isUntitled = true;
1074
+ return widget;
1075
+ }
1076
+ }
1077
+
1078
+ /**
1079
+ * Wrapper function for adding the default launcher items for File Editor
1080
+ */
1081
+ export function addLauncherItems(
1082
+ launcher: ILauncher,
1083
+ trans: TranslationBundle
1084
+ ): void {
1085
+ addCreateNewToLauncher(launcher, trans);
1086
+
1087
+ addCreateNewMarkdownToLauncher(launcher, trans);
1088
+ }
1089
+
1090
+ /**
1091
+ * Add Create New Text File to the Launcher
1092
+ */
1093
+ export function addCreateNewToLauncher(
1094
+ launcher: ILauncher,
1095
+ trans: TranslationBundle
1096
+ ): void {
1097
+ launcher.add({
1098
+ command: CommandIDs.createNew,
1099
+ category: trans.__('Other'),
1100
+ rank: 1
1101
+ });
1102
+ }
1103
+
1104
+ /**
1105
+ * Add Create New Markdown to the Launcher
1106
+ */
1107
+ export function addCreateNewMarkdownToLauncher(
1108
+ launcher: ILauncher,
1109
+ trans: TranslationBundle
1110
+ ): void {
1111
+ launcher.add({
1112
+ command: CommandIDs.createNewMarkdown,
1113
+ category: trans.__('Other'),
1114
+ rank: 2
1115
+ });
1116
+ }
1117
+
1118
+ /**
1119
+ * Add ___ File items to the Launcher for common file types associated with available kernels
1120
+ */
1121
+ export function addKernelLanguageLauncherItems(
1122
+ launcher: ILauncher,
1123
+ trans: TranslationBundle,
1124
+ availableKernelFileTypes: Iterable<IFileTypeData>
1125
+ ): void {
1126
+ for (let ext of availableKernelFileTypes) {
1127
+ launcher.add({
1128
+ command: CommandIDs.createNew,
1129
+ category: trans.__('Other'),
1130
+ rank: 3,
1131
+ args: ext
1132
+ });
1133
+ }
1134
+ }
1135
+
1136
+ /**
1137
+ * Wrapper function for adding the default items to the File Editor palette
1138
+ */
1139
+ export function addPaletteItems(
1140
+ palette: ICommandPalette,
1141
+ trans: TranslationBundle
1142
+ ): void {
1143
+ addChangeTabsCommandsToPalette(palette, trans);
1144
+
1145
+ addCreateNewCommandToPalette(palette, trans);
1146
+
1147
+ addCreateNewMarkdownCommandToPalette(palette, trans);
1148
+
1149
+ addChangeFontSizeCommandsToPalette(palette, trans);
1150
+ }
1151
+
1152
+ /**
1153
+ * Add commands to change the tab indentation to the File Editor palette
1154
+ */
1155
+ export function addChangeTabsCommandsToPalette(
1156
+ palette: ICommandPalette,
1157
+ trans: TranslationBundle
1158
+ ): void {
1159
+ const paletteCategory = trans.__('Text Editor');
1160
+ const args: JSONObject = {
1161
+ size: 4
1162
+ };
1163
+ const command = CommandIDs.changeTabs;
1164
+ palette.addItem({ command, args, category: paletteCategory });
1165
+
1166
+ for (const size of [1, 2, 4, 8]) {
1167
+ const args: JSONObject = {
1168
+ size
1169
+ };
1170
+ palette.addItem({ command, args, category: paletteCategory });
1171
+ }
1172
+ }
1173
+
1174
+ /**
1175
+ * Add a Create New File command to the File Editor palette
1176
+ */
1177
+ export function addCreateNewCommandToPalette(
1178
+ palette: ICommandPalette,
1179
+ trans: TranslationBundle
1180
+ ): void {
1181
+ const paletteCategory = trans.__('Text Editor');
1182
+ palette.addItem({
1183
+ command: CommandIDs.createNew,
1184
+ args: { isPalette: true },
1185
+ category: paletteCategory
1186
+ });
1187
+ }
1188
+
1189
+ /**
1190
+ * Add a Create New Markdown command to the File Editor palette
1191
+ */
1192
+ export function addCreateNewMarkdownCommandToPalette(
1193
+ palette: ICommandPalette,
1194
+ trans: TranslationBundle
1195
+ ): void {
1196
+ const paletteCategory = trans.__('Text Editor');
1197
+ palette.addItem({
1198
+ command: CommandIDs.createNewMarkdown,
1199
+ args: { isPalette: true },
1200
+ category: paletteCategory
1201
+ });
1202
+ }
1203
+
1204
+ /**
1205
+ * Add commands to change the font size to the File Editor palette
1206
+ */
1207
+ export function addChangeFontSizeCommandsToPalette(
1208
+ palette: ICommandPalette,
1209
+ trans: TranslationBundle
1210
+ ): void {
1211
+ const paletteCategory = trans.__('Text Editor');
1212
+ const command = CommandIDs.changeFontSize;
1213
+
1214
+ let args = { delta: 1 };
1215
+ palette.addItem({ command, args, category: paletteCategory });
1216
+
1217
+ args = { delta: -1 };
1218
+ palette.addItem({ command, args, category: paletteCategory });
1219
+ }
1220
+
1221
+ /**
1222
+ * Add New ___ File commands to the File Editor palette for common file types associated with available kernels
1223
+ */
1224
+ export function addKernelLanguagePaletteItems(
1225
+ palette: ICommandPalette,
1226
+ trans: TranslationBundle,
1227
+ availableKernelFileTypes: Iterable<IFileTypeData>
1228
+ ): void {
1229
+ const paletteCategory = trans.__('Text Editor');
1230
+ for (let ext of availableKernelFileTypes) {
1231
+ palette.addItem({
1232
+ command: CommandIDs.createNew,
1233
+ args: { ...ext, isPalette: true },
1234
+ category: paletteCategory
1235
+ });
1236
+ }
1237
+ }
1238
+
1239
+ /**
1240
+ * Wrapper function for adding the default menu items for File Editor
1241
+ */
1242
+ export function addMenuItems(
1243
+ menu: IMainMenu,
1244
+ tracker: WidgetTracker<IDocumentWidget<FileEditor>>,
1245
+ consoleTracker: IConsoleTracker | null,
1246
+ isEnabled: () => boolean
1247
+ ): void {
1248
+ // Add undo/redo hooks to the edit menu.
1249
+ menu.editMenu.undoers.redo.add({
1250
+ id: CommandIDs.redo,
1251
+ isEnabled
1252
+ });
1253
+ menu.editMenu.undoers.undo.add({
1254
+ id: CommandIDs.undo,
1255
+ isEnabled
1256
+ });
1257
+
1258
+ // Add editor view options.
1259
+ menu.viewMenu.editorViewers.toggleLineNumbers.add({
1260
+ id: CommandIDs.currentLineNumbers,
1261
+ isEnabled
1262
+ });
1263
+ menu.viewMenu.editorViewers.toggleMatchBrackets.add({
1264
+ id: CommandIDs.currentMatchBrackets,
1265
+ isEnabled
1266
+ });
1267
+ menu.viewMenu.editorViewers.toggleWordWrap.add({
1268
+ id: CommandIDs.currentLineWrap,
1269
+ isEnabled
1270
+ });
1271
+
1272
+ // Add a console creator the the file menu.
1273
+ menu.fileMenu.consoleCreators.add({
1274
+ id: CommandIDs.createConsole,
1275
+ isEnabled
1276
+ });
1277
+
1278
+ // Add a code runner to the run menu.
1279
+ if (consoleTracker) {
1280
+ addCodeRunnersToRunMenu(menu, consoleTracker);
1281
+ }
1282
+ }
1283
+
1284
+ /**
1285
+ * Add Create New ___ File commands to the File menu for common file types associated with available kernels
1286
+ */
1287
+ export function addKernelLanguageMenuItems(
1288
+ menu: IMainMenu,
1289
+ availableKernelFileTypes: Iterable<IFileTypeData>
1290
+ ): void {
1291
+ for (let ext of availableKernelFileTypes) {
1292
+ menu.fileMenu.newMenu.addItem({
1293
+ command: CommandIDs.createNew,
1294
+ args: ext,
1295
+ rank: 31
1296
+ });
1297
+ }
1298
+ }
1299
+
1300
+ /**
1301
+ * Add a File Editor code runner to the Run menu
1302
+ */
1303
+ export function addCodeRunnersToRunMenu(
1304
+ menu: IMainMenu,
1305
+ consoleTracker: IConsoleTracker
1306
+ ): void {
1307
+ const isEnabled = (current: IDocumentWidget<FileEditor>) =>
1308
+ current.context &&
1309
+ !!consoleTracker.find(
1310
+ widget => widget.sessionContext.session?.path === current.context.path
1311
+ );
1312
+ menu.runMenu.codeRunners.restart.add({
1313
+ id: CommandIDs.restartConsole,
1314
+ isEnabled
1315
+ });
1316
+ menu.runMenu.codeRunners.run.add({
1317
+ id: CommandIDs.runCode,
1318
+ isEnabled
1319
+ });
1320
+ menu.runMenu.codeRunners.runAll.add({
1321
+ id: CommandIDs.runAllCode,
1322
+ isEnabled
1323
+ });
1324
+ }
1325
+
1326
+ export function addOpenCodeViewerCommand(
1327
+ app: JupyterFrontEnd,
1328
+ editorServices: IEditorServices,
1329
+ tracker: WidgetTracker<MainAreaWidget<CodeViewerWidget>>,
1330
+ trans: TranslationBundle
1331
+ ): void {
1332
+ const openCodeViewer = async (args: {
1333
+ content: string;
1334
+ label?: string;
1335
+ mimeType?: string;
1336
+ extension?: string;
1337
+ widgetId?: string;
1338
+ }): Promise<CodeViewerWidget> => {
1339
+ const func = editorServices.factoryService.newDocumentEditor;
1340
+ const factory: CodeEditor.Factory = options => {
1341
+ return func(options);
1342
+ };
1343
+
1344
+ // Derive mimetype from extension
1345
+ let mimetype = args.mimeType;
1346
+ if (!mimetype && args.extension) {
1347
+ mimetype = editorServices.mimeTypeService.getMimeTypeByFilePath(
1348
+ `temp.${args.extension.replace(/\\.$/, '')}`
1349
+ );
1350
+ }
1351
+
1352
+ const widget = CodeViewerWidget.createCodeViewer({
1353
+ factory,
1354
+ content: args.content,
1355
+ mimeType: mimetype
1356
+ });
1357
+ widget.title.label = args.label || trans.__('Code Viewer');
1358
+ widget.title.caption = widget.title.label;
1359
+
1360
+ // Get the fileType based on the mimetype to determine the icon
1361
+ const fileType = find(app.docRegistry.fileTypes(), fileType =>
1362
+ mimetype ? fileType.mimeTypes.includes(mimetype) : false
1363
+ );
1364
+ widget.title.icon = fileType?.icon ?? textEditorIcon;
1365
+
1366
+ if (args.widgetId) {
1367
+ widget.id = args.widgetId;
1368
+ }
1369
+ const main = new MainAreaWidget({ content: widget });
1370
+ await tracker.add(main);
1371
+ app.shell.add(main, 'main');
1372
+ return widget;
1373
+ };
1374
+
1375
+ app.commands.addCommand(CommandIDs.openCodeViewer, {
1376
+ label: trans.__('Open Code Viewer'),
1377
+ execute: (args: any) => {
1378
+ return openCodeViewer(args);
1379
+ }
1380
+ });
1381
+ }
1382
+ }