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

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