@jupyterlab/fileeditor-extension 4.0.0-alpha.2 → 4.0.0-alpha.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,671 @@
1
+ // Copyright (c) Jupyter Development Team.
2
+ // Distributed under the terms of the Modified BSD License.
3
+ /**
4
+ * @packageDocumentation
5
+ * @module fileeditor-extension
6
+ */
7
+
8
+ import {
9
+ ILayoutRestorer,
10
+ JupyterFrontEnd,
11
+ JupyterFrontEndPlugin
12
+ } from '@jupyterlab/application';
13
+ import {
14
+ createToolbarFactory,
15
+ ICommandPalette,
16
+ ISanitizer,
17
+ ISessionContextDialogs,
18
+ IToolbarWidgetRegistry,
19
+ MainAreaWidget,
20
+ Sanitizer,
21
+ WidgetTracker
22
+ } from '@jupyterlab/apputils';
23
+ import {
24
+ CodeViewerWidget,
25
+ IEditorServices,
26
+ IPositionModel
27
+ } from '@jupyterlab/codeeditor';
28
+ import {
29
+ IEditorExtensionRegistry,
30
+ IEditorLanguageRegistry,
31
+ IEditorThemeRegistry
32
+ } from '@jupyterlab/codemirror';
33
+ import { ICompletionProviderManager } from '@jupyterlab/completer';
34
+ import { IConsoleTracker } from '@jupyterlab/console';
35
+ import { DocumentRegistry, IDocumentWidget } from '@jupyterlab/docregistry';
36
+ import { ISearchProviderRegistry } from '@jupyterlab/documentsearch';
37
+ import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
38
+ import {
39
+ FileEditor,
40
+ FileEditorAdapter,
41
+ FileEditorFactory,
42
+ FileEditorSearchProvider,
43
+ IEditorTracker,
44
+ LaTeXTableOfContentsFactory,
45
+ MarkdownTableOfContentsFactory,
46
+ PythonTableOfContentsFactory,
47
+ TabSpaceStatus
48
+ } from '@jupyterlab/fileeditor';
49
+ import { ILauncher } from '@jupyterlab/launcher';
50
+ import {
51
+ ILSPCodeExtractorsManager,
52
+ ILSPDocumentConnectionManager,
53
+ ILSPFeatureManager
54
+ } from '@jupyterlab/lsp';
55
+ import { IMainMenu } from '@jupyterlab/mainmenu';
56
+ import { IObservableList } from '@jupyterlab/observables';
57
+ import { Session } from '@jupyterlab/services';
58
+ import { ISettingRegistry } from '@jupyterlab/settingregistry';
59
+ import { IStatusBar } from '@jupyterlab/statusbar';
60
+ import { ITableOfContentsRegistry } from '@jupyterlab/toc';
61
+ import { ITranslator, nullTranslator } from '@jupyterlab/translation';
62
+ import { IFormRendererRegistry, MenuSvg } from '@jupyterlab/ui-components';
63
+ import { find } from '@lumino/algorithm';
64
+ import { JSONObject } from '@lumino/coreutils';
65
+ import { Widget } from '@lumino/widgets';
66
+
67
+ import { CommandIDs, Commands, FACTORY, IFileTypeData } from './commands';
68
+ import { editorSyntaxStatus } from './syntaxstatus';
69
+
70
+ export { Commands } from './commands';
71
+
72
+ /**
73
+ * The editor tracker extension.
74
+ */
75
+ const plugin: JupyterFrontEndPlugin<IEditorTracker> = {
76
+ activate,
77
+ id: '@jupyterlab/fileeditor-extension:plugin',
78
+ requires: [
79
+ IEditorServices,
80
+ IEditorExtensionRegistry,
81
+ IEditorLanguageRegistry,
82
+ IEditorThemeRegistry,
83
+ IDefaultFileBrowser,
84
+ ISettingRegistry
85
+ ],
86
+ optional: [
87
+ IConsoleTracker,
88
+ ICommandPalette,
89
+ ILauncher,
90
+ IMainMenu,
91
+ ILayoutRestorer,
92
+ ISessionContextDialogs,
93
+ ITableOfContentsRegistry,
94
+ IToolbarWidgetRegistry,
95
+ ITranslator,
96
+ IFormRendererRegistry
97
+ ],
98
+ provides: IEditorTracker,
99
+ autoStart: true
100
+ };
101
+
102
+ /**
103
+ * A plugin that provides a status item allowing the user to
104
+ * switch tabs vs spaces and tab widths for text editors.
105
+ */
106
+ export const tabSpaceStatus: JupyterFrontEndPlugin<void> = {
107
+ id: '@jupyterlab/fileeditor-extension:tab-space-status',
108
+ autoStart: true,
109
+ requires: [
110
+ IEditorTracker,
111
+ IEditorExtensionRegistry,
112
+ ISettingRegistry,
113
+ ITranslator
114
+ ],
115
+ optional: [IStatusBar],
116
+ activate: (
117
+ app: JupyterFrontEnd,
118
+ editorTracker: IEditorTracker,
119
+ extensions: IEditorExtensionRegistry,
120
+ settingRegistry: ISettingRegistry,
121
+ translator: ITranslator,
122
+ statusBar: IStatusBar | null
123
+ ) => {
124
+ const trans = translator.load('jupyterlab');
125
+ if (!statusBar) {
126
+ // Automatically disable if statusbar missing
127
+ return;
128
+ }
129
+ // Create a menu for switching tabs vs spaces.
130
+ const menu = new MenuSvg({ commands: app.commands });
131
+ const command = 'fileeditor:change-tabs';
132
+ const { shell } = app;
133
+ const args: JSONObject = {
134
+ name: trans.__('Indent with Tab')
135
+ };
136
+ menu.addItem({ command, args });
137
+ for (const size of ['1', '2', '4', '8']) {
138
+ const args: JSONObject = {
139
+ size,
140
+ name: trans.__('Spaces: %1', size)
141
+ };
142
+ menu.addItem({ command, args });
143
+ }
144
+
145
+ // Create the status item.
146
+ const item = new TabSpaceStatus({ menu, translator });
147
+
148
+ // Keep a reference to the code editor config from the settings system.
149
+ const updateIndentUnit = (settings: ISettingRegistry.ISettings): void => {
150
+ item.model!.indentUnit =
151
+ (settings.get('editorConfig').composite as any)?.indentUnit ??
152
+ extensions.baseConfiguration.indentUnit ??
153
+ null;
154
+ };
155
+
156
+ void Promise.all([
157
+ settingRegistry.load('@jupyterlab/fileeditor-extension:plugin'),
158
+ app.restored
159
+ ]).then(([settings]) => {
160
+ updateIndentUnit(settings);
161
+ settings.changed.connect(updateIndentUnit);
162
+ });
163
+
164
+ // Add the status item.
165
+ statusBar.registerStatusItem(
166
+ '@jupyterlab/fileeditor-extension:tab-space-status',
167
+ {
168
+ item,
169
+ align: 'right',
170
+ rank: 1,
171
+ isActive: () => {
172
+ return (
173
+ !!shell.currentWidget && editorTracker.has(shell.currentWidget)
174
+ );
175
+ }
176
+ }
177
+ );
178
+ }
179
+ };
180
+
181
+ /**
182
+ * Cursor position.
183
+ */
184
+ const lineColStatus: JupyterFrontEndPlugin<void> = {
185
+ id: '@jupyterlab/fileeditor-extension:cursor-position',
186
+ activate: (
187
+ app: JupyterFrontEnd,
188
+ tracker: IEditorTracker,
189
+ positionModel: IPositionModel
190
+ ) => {
191
+ positionModel.addEditorProvider((widget: Widget | null) =>
192
+ Promise.resolve(
193
+ widget && tracker.has(widget)
194
+ ? (widget as IDocumentWidget<FileEditor>).content.editor
195
+ : null
196
+ )
197
+ );
198
+ },
199
+ requires: [IEditorTracker, IPositionModel],
200
+ autoStart: true
201
+ };
202
+
203
+ const completerPlugin: JupyterFrontEndPlugin<void> = {
204
+ id: '@jupyterlab/fileeditor-extension:completer',
205
+ requires: [IEditorTracker],
206
+ optional: [ICompletionProviderManager, ITranslator, ISanitizer],
207
+ activate: activateFileEditorCompleterService,
208
+ autoStart: true
209
+ };
210
+
211
+ /**
212
+ * A plugin to search file editors
213
+ */
214
+ const searchProvider: JupyterFrontEndPlugin<void> = {
215
+ id: '@jupyterlab/fileeditor-extension:search',
216
+ requires: [ISearchProviderRegistry],
217
+ autoStart: true,
218
+ activate: (app: JupyterFrontEnd, registry: ISearchProviderRegistry) => {
219
+ registry.add('jp-fileeditorSearchProvider', FileEditorSearchProvider);
220
+ }
221
+ };
222
+
223
+ const languageServerPlugin: JupyterFrontEndPlugin<void> = {
224
+ id: '@jupyterlab/fileeditor-extension:language-server',
225
+ requires: [
226
+ IEditorTracker,
227
+ ILSPDocumentConnectionManager,
228
+ ILSPFeatureManager,
229
+ ILSPCodeExtractorsManager
230
+ ],
231
+
232
+ activate: activateFileEditorLanguageServer,
233
+ autoStart: true
234
+ };
235
+
236
+ /**
237
+ * Export the plugins as default.
238
+ */
239
+ const plugins: JupyterFrontEndPlugin<any>[] = [
240
+ plugin,
241
+ lineColStatus,
242
+ completerPlugin,
243
+ languageServerPlugin,
244
+ searchProvider,
245
+ editorSyntaxStatus,
246
+ tabSpaceStatus
247
+ ];
248
+ export default plugins;
249
+
250
+ /**
251
+ * Activate the editor tracker plugin.
252
+ */
253
+ function activate(
254
+ app: JupyterFrontEnd,
255
+ editorServices: IEditorServices,
256
+ extensions: IEditorExtensionRegistry,
257
+ languages: IEditorLanguageRegistry,
258
+ themes: IEditorThemeRegistry,
259
+ fileBrowser: IDefaultFileBrowser,
260
+ settingRegistry: ISettingRegistry,
261
+ consoleTracker: IConsoleTracker | null,
262
+ palette: ICommandPalette | null,
263
+ launcher: ILauncher | null,
264
+ menu: IMainMenu | null,
265
+ restorer: ILayoutRestorer | null,
266
+ sessionDialogs: ISessionContextDialogs | null,
267
+ tocRegistry: ITableOfContentsRegistry | null,
268
+ toolbarRegistry: IToolbarWidgetRegistry | null,
269
+ translator: ITranslator | null,
270
+ formRegistry: IFormRendererRegistry | null
271
+ ): IEditorTracker {
272
+ const id = plugin.id;
273
+ translator = translator ?? nullTranslator;
274
+ const trans = translator.load('jupyterlab');
275
+ const namespace = 'editor';
276
+ let toolbarFactory:
277
+ | ((
278
+ widget: IDocumentWidget<FileEditor>
279
+ ) => IObservableList<DocumentRegistry.IToolbarItem>)
280
+ | undefined;
281
+
282
+ if (toolbarRegistry) {
283
+ toolbarFactory = createToolbarFactory(
284
+ toolbarRegistry,
285
+ settingRegistry,
286
+ FACTORY,
287
+ id,
288
+ translator
289
+ );
290
+ }
291
+
292
+ const factory = new FileEditorFactory({
293
+ editorServices,
294
+ factoryOptions: {
295
+ name: FACTORY,
296
+ label: trans.__('Editor'),
297
+ fileTypes: ['markdown', '*'], // Explicitly add the markdown fileType so
298
+ defaultFor: ['markdown', '*'], // it outranks the defaultRendered viewer.
299
+ toolbarFactory,
300
+ translator
301
+ }
302
+ });
303
+ const { commands, restored, shell } = app;
304
+ const tracker = new WidgetTracker<IDocumentWidget<FileEditor>>({
305
+ namespace
306
+ });
307
+ const isEnabled = () =>
308
+ tracker.currentWidget !== null &&
309
+ tracker.currentWidget === shell.currentWidget;
310
+
311
+ const commonLanguageFileTypeData = new Map<string, IFileTypeData[]>([
312
+ [
313
+ 'python',
314
+ [
315
+ {
316
+ fileExt: 'py',
317
+ iconName: 'ui-components:python',
318
+ launcherLabel: trans.__('Python File'),
319
+ paletteLabel: trans.__('New Python File'),
320
+ caption: trans.__('Create a new Python file')
321
+ }
322
+ ]
323
+ ],
324
+ [
325
+ 'julia',
326
+ [
327
+ {
328
+ fileExt: 'jl',
329
+ iconName: 'ui-components:julia',
330
+ launcherLabel: trans.__('Julia File'),
331
+ paletteLabel: trans.__('New Julia File'),
332
+ caption: trans.__('Create a new Julia file')
333
+ }
334
+ ]
335
+ ],
336
+ [
337
+ 'R',
338
+ [
339
+ {
340
+ fileExt: 'r',
341
+ iconName: 'ui-components:r-kernel',
342
+ launcherLabel: trans.__('R File'),
343
+ paletteLabel: trans.__('New R File'),
344
+ caption: trans.__('Create a new R file')
345
+ }
346
+ ]
347
+ ]
348
+ ]);
349
+
350
+ // Use available kernels to determine which common file types should have 'Create New' options in the Launcher, File Editor palette, and File menu
351
+ const getAvailableKernelFileTypes = async (): Promise<Set<IFileTypeData>> => {
352
+ const specsManager = app.serviceManager.kernelspecs;
353
+ await specsManager.ready;
354
+ let fileTypes = new Set<IFileTypeData>();
355
+ const specs = specsManager.specs?.kernelspecs ?? {};
356
+ Object.keys(specs).forEach(spec => {
357
+ const specModel = specs[spec];
358
+ if (specModel) {
359
+ const exts = commonLanguageFileTypeData.get(specModel.language);
360
+ exts?.forEach(ext => fileTypes.add(ext));
361
+ }
362
+ });
363
+ return fileTypes;
364
+ };
365
+
366
+ // Handle state restoration.
367
+ if (restorer) {
368
+ void restorer.restore(tracker, {
369
+ command: 'docmanager:open',
370
+ args: widget => ({ path: widget.context.path, factory: FACTORY }),
371
+ name: widget => widget.context.path
372
+ });
373
+ }
374
+
375
+ // Add a console creator to the File menu
376
+ // Fetch the initial state of the settings.
377
+ Promise.all([settingRegistry.load(id), restored])
378
+ .then(([settings]) => {
379
+ // As the menu are defined in the settings we must ensure they are loaded
380
+ // before updating dynamically the submenu
381
+ if (menu) {
382
+ const languageMenu = menu.viewMenu.items.find(
383
+ item =>
384
+ item.type === 'submenu' &&
385
+ item.submenu?.id === 'jp-mainmenu-view-codemirror-language'
386
+ )?.submenu;
387
+
388
+ if (languageMenu) {
389
+ languages
390
+ .getLanguages()
391
+ .sort((a, b) => {
392
+ const aName = a.name;
393
+ const bName = b.name;
394
+ return aName.localeCompare(bName);
395
+ })
396
+ .forEach(spec => {
397
+ // Avoid mode name with a curse word.
398
+ if (spec.name.indexOf('brainf') === 0) {
399
+ return;
400
+ }
401
+ languageMenu.addItem({
402
+ command: CommandIDs.changeLanguage,
403
+ args: { ...spec } as any // TODO: Casting to `any` until lumino typings are fixed
404
+ });
405
+ });
406
+ }
407
+ const themeMenu = menu.settingsMenu.items.find(
408
+ item =>
409
+ item.type === 'submenu' &&
410
+ item.submenu?.id === 'jp-mainmenu-settings-codemirror-theme'
411
+ )?.submenu;
412
+
413
+ if (themeMenu) {
414
+ for (const theme of themes.themes) {
415
+ themeMenu.addItem({
416
+ command: CommandIDs.changeTheme,
417
+ args: {
418
+ theme: theme.name,
419
+ displayName: theme.displayName ?? theme.name
420
+ }
421
+ });
422
+ }
423
+ }
424
+
425
+ // Add go to line capabilities to the edit menu.
426
+ menu.editMenu.goToLiners.add({
427
+ id: CommandIDs.goToLine,
428
+ isEnabled: (w: Widget) =>
429
+ tracker.currentWidget !== null && tracker.has(w)
430
+ });
431
+ }
432
+
433
+ Commands.updateSettings(settings, commands);
434
+ Commands.updateTracker(tracker);
435
+ settings.changed.connect(() => {
436
+ Commands.updateSettings(settings, commands);
437
+ Commands.updateTracker(tracker);
438
+ });
439
+ })
440
+ .catch((reason: Error) => {
441
+ console.error(reason.message);
442
+ Commands.updateTracker(tracker);
443
+ });
444
+
445
+ if (formRegistry) {
446
+ const CMRenderer = formRegistry.getRenderer(
447
+ '@jupyterlab/codemirror-extension:plugin.defaultConfig'
448
+ );
449
+ if (CMRenderer) {
450
+ formRegistry.addRenderer(
451
+ '@jupyterlab/fileeditor-extension:plugin.editorConfig',
452
+ CMRenderer
453
+ );
454
+ }
455
+ }
456
+
457
+ factory.widgetCreated.connect((sender, widget) => {
458
+ // Notify the widget tracker if restore data needs to update.
459
+ widget.context.pathChanged.connect(() => {
460
+ void tracker.save(widget);
461
+ });
462
+ void tracker.add(widget);
463
+ Commands.updateWidget(widget.content);
464
+ });
465
+ app.docRegistry.addWidgetFactory(factory);
466
+
467
+ // Handle the settings of new widgets.
468
+ tracker.widgetAdded.connect((sender, widget) => {
469
+ Commands.updateWidget(widget.content);
470
+ });
471
+
472
+ Commands.addCommands(
473
+ app.commands,
474
+ settingRegistry,
475
+ trans,
476
+ id,
477
+ isEnabled,
478
+ tracker,
479
+ fileBrowser,
480
+ extensions,
481
+ languages,
482
+ themes,
483
+ consoleTracker,
484
+ sessionDialogs,
485
+ menu
486
+ );
487
+
488
+ const codeViewerTracker = new WidgetTracker<MainAreaWidget<CodeViewerWidget>>(
489
+ {
490
+ namespace: 'codeviewer'
491
+ }
492
+ );
493
+
494
+ // Handle state restoration for code viewers
495
+ if (restorer) {
496
+ void restorer.restore(codeViewerTracker, {
497
+ command: CommandIDs.openCodeViewer,
498
+ args: widget => ({
499
+ content: widget.content.content,
500
+ label: widget.content.title.label,
501
+ mimeType: widget.content.mimeType,
502
+ widgetId: widget.content.id
503
+ }),
504
+ name: widget => widget.content.id
505
+ });
506
+ }
507
+
508
+ Commands.addOpenCodeViewerCommand(
509
+ app,
510
+ editorServices,
511
+ codeViewerTracker,
512
+ trans
513
+ );
514
+
515
+ // Add a launcher item if the launcher is available.
516
+ if (launcher) {
517
+ Commands.addLauncherItems(launcher, trans);
518
+ }
519
+
520
+ if (palette) {
521
+ Commands.addPaletteItems(palette, trans);
522
+ }
523
+
524
+ if (menu) {
525
+ Commands.addMenuItems(menu, tracker, consoleTracker, isEnabled);
526
+ }
527
+
528
+ getAvailableKernelFileTypes()
529
+ .then(availableKernelFileTypes => {
530
+ if (launcher) {
531
+ Commands.addKernelLanguageLauncherItems(
532
+ launcher,
533
+ trans,
534
+ availableKernelFileTypes
535
+ );
536
+ }
537
+
538
+ if (palette) {
539
+ Commands.addKernelLanguagePaletteItems(
540
+ palette,
541
+ trans,
542
+ availableKernelFileTypes
543
+ );
544
+ }
545
+
546
+ if (menu) {
547
+ Commands.addKernelLanguageMenuItems(menu, availableKernelFileTypes);
548
+ }
549
+ })
550
+ .catch((reason: Error) => {
551
+ console.error(reason.message);
552
+ });
553
+
554
+ if (tocRegistry) {
555
+ tocRegistry.add(new LaTeXTableOfContentsFactory(tracker));
556
+ tocRegistry.add(new MarkdownTableOfContentsFactory(tracker));
557
+ tocRegistry.add(new PythonTableOfContentsFactory(tracker));
558
+ }
559
+
560
+ return tracker;
561
+ }
562
+
563
+ /**
564
+ * Activate the completer service for file editor.
565
+ */
566
+ function activateFileEditorCompleterService(
567
+ app: JupyterFrontEnd,
568
+ editorTracker: IEditorTracker,
569
+ manager: ICompletionProviderManager | null,
570
+ translator: ITranslator | null,
571
+ appSanitizer: ISanitizer | null
572
+ ): void {
573
+ if (!manager) {
574
+ return;
575
+ }
576
+
577
+ Commands.addCompleterCommands(
578
+ app.commands,
579
+ editorTracker,
580
+ manager,
581
+ translator
582
+ );
583
+ const sessionManager = app.serviceManager.sessions;
584
+ const sanitizer = appSanitizer ?? new Sanitizer();
585
+ const _activeSessions = new Map<string, Session.ISessionConnection>();
586
+ const updateCompleter = async (
587
+ _: IEditorTracker,
588
+ widget: IDocumentWidget<FileEditor>
589
+ ) => {
590
+ const completerContext = {
591
+ editor: widget.content.editor,
592
+ widget
593
+ };
594
+
595
+ await manager.updateCompleter(completerContext);
596
+ const onRunningChanged = (
597
+ _: Session.IManager,
598
+ models: Session.IModel[]
599
+ ) => {
600
+ const oldSession = _activeSessions.get(widget.id);
601
+ // Search for a matching path.
602
+ const model = find(models, m => m.path === widget.context.path);
603
+ if (model) {
604
+ // If there is a matching path, but it is the same
605
+ // session as we previously had, do nothing.
606
+ if (oldSession && oldSession.id === model.id) {
607
+ return;
608
+ }
609
+ // Otherwise, dispose of the old session and reset to
610
+ // a new CompletionConnector.
611
+ if (oldSession) {
612
+ _activeSessions.delete(widget.id);
613
+ oldSession.dispose();
614
+ }
615
+ const session = sessionManager.connectTo({ model });
616
+ const newCompleterContext = {
617
+ editor: widget.content.editor,
618
+ widget,
619
+ session,
620
+ sanitizer
621
+ };
622
+ manager.updateCompleter(newCompleterContext).catch(console.error);
623
+ _activeSessions.set(widget.id, session);
624
+ } else {
625
+ // If we didn't find a match, make sure
626
+ // the connector is the contextConnector and
627
+ // dispose of any previous connection.
628
+ if (oldSession) {
629
+ _activeSessions.delete(widget.id);
630
+ oldSession.dispose();
631
+ }
632
+ }
633
+ };
634
+
635
+ onRunningChanged(sessionManager, Array.from(sessionManager.running()));
636
+ sessionManager.runningChanged.connect(onRunningChanged);
637
+
638
+ widget.disposed.connect(() => {
639
+ sessionManager.runningChanged.disconnect(onRunningChanged);
640
+ const session = _activeSessions.get(widget.id);
641
+ if (session) {
642
+ _activeSessions.delete(widget.id);
643
+ session.dispose();
644
+ }
645
+ });
646
+ };
647
+ editorTracker.widgetAdded.connect(updateCompleter);
648
+ manager.activeProvidersChanged.connect(() => {
649
+ editorTracker.forEach(editorWidget => {
650
+ updateCompleter(editorTracker, editorWidget).catch(console.error);
651
+ });
652
+ });
653
+ }
654
+
655
+ function activateFileEditorLanguageServer(
656
+ app: JupyterFrontEnd,
657
+ editors: IEditorTracker,
658
+ connectionManager: ILSPDocumentConnectionManager,
659
+ featureManager: ILSPFeatureManager,
660
+ extractorManager: ILSPCodeExtractorsManager
661
+ ): void {
662
+ editors.widgetAdded.connect(async (_, editor) => {
663
+ const adapter = new FileEditorAdapter(editor, {
664
+ connectionManager,
665
+ featureManager,
666
+ foreignCodeExtractorsManager: extractorManager,
667
+ docRegistry: app.docRegistry
668
+ });
669
+ connectionManager.registerAdapter(editor.context.path, adapter);
670
+ });
671
+ }