@theia/notebook 1.49.1 → 1.50.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.
Files changed (151) hide show
  1. package/lib/browser/contributions/cell-operations.js +1 -1
  2. package/lib/browser/contributions/cell-operations.js.map +1 -1
  3. package/lib/browser/contributions/notebook-actions-contribution.d.ts +3 -0
  4. package/lib/browser/contributions/notebook-actions-contribution.d.ts.map +1 -1
  5. package/lib/browser/contributions/notebook-actions-contribution.js +54 -34
  6. package/lib/browser/contributions/notebook-actions-contribution.js.map +1 -1
  7. package/lib/browser/contributions/notebook-cell-actions-contribution.d.ts +2 -2
  8. package/lib/browser/contributions/notebook-cell-actions-contribution.d.ts.map +1 -1
  9. package/lib/browser/contributions/notebook-cell-actions-contribution.js +42 -42
  10. package/lib/browser/contributions/notebook-cell-actions-contribution.js.map +1 -1
  11. package/lib/browser/contributions/notebook-color-contribution.js +2 -2
  12. package/lib/browser/contributions/notebook-color-contribution.js.map +1 -1
  13. package/lib/browser/contributions/notebook-context-keys.d.ts +4 -1
  14. package/lib/browser/contributions/notebook-context-keys.d.ts.map +1 -1
  15. package/lib/browser/contributions/notebook-context-keys.js +8 -2
  16. package/lib/browser/contributions/notebook-context-keys.js.map +1 -1
  17. package/lib/browser/contributions/notebook-label-provider-contribution.d.ts +3 -0
  18. package/lib/browser/contributions/notebook-label-provider-contribution.d.ts.map +1 -1
  19. package/lib/browser/contributions/notebook-label-provider-contribution.js +33 -13
  20. package/lib/browser/contributions/notebook-label-provider-contribution.js.map +1 -1
  21. package/lib/browser/contributions/notebook-outline-contribution.d.ts +0 -2
  22. package/lib/browser/contributions/notebook-outline-contribution.d.ts.map +1 -1
  23. package/lib/browser/contributions/notebook-outline-contribution.js +21 -17
  24. package/lib/browser/contributions/notebook-outline-contribution.js.map +1 -1
  25. package/lib/browser/contributions/notebook-output-action-contribution.js +7 -7
  26. package/lib/browser/contributions/notebook-output-action-contribution.js.map +1 -1
  27. package/lib/browser/index.js +11 -11
  28. package/lib/browser/index.js.map +1 -1
  29. package/lib/browser/notebook-cell-resource-resolver.js +14 -14
  30. package/lib/browser/notebook-cell-resource-resolver.js.map +1 -1
  31. package/lib/browser/notebook-editor-widget-factory.d.ts +3 -2
  32. package/lib/browser/notebook-editor-widget-factory.d.ts.map +1 -1
  33. package/lib/browser/notebook-editor-widget-factory.js +10 -10
  34. package/lib/browser/notebook-editor-widget-factory.js.map +1 -1
  35. package/lib/browser/notebook-editor-widget.d.ts +3 -0
  36. package/lib/browser/notebook-editor-widget.d.ts.map +1 -1
  37. package/lib/browser/notebook-editor-widget.js +32 -26
  38. package/lib/browser/notebook-editor-widget.js.map +1 -1
  39. package/lib/browser/notebook-frontend-module.d.ts.map +1 -1
  40. package/lib/browser/notebook-frontend-module.js +1 -0
  41. package/lib/browser/notebook-frontend-module.js.map +1 -1
  42. package/lib/browser/notebook-open-handler.js +2 -2
  43. package/lib/browser/notebook-open-handler.js.map +1 -1
  44. package/lib/browser/notebook-output-utils.js.map +1 -1
  45. package/lib/browser/notebook-renderer-registry.js +2 -2
  46. package/lib/browser/notebook-renderer-registry.js.map +1 -1
  47. package/lib/browser/notebook-type-registry.js +6 -6
  48. package/lib/browser/notebook-type-registry.js.map +1 -1
  49. package/lib/browser/notebook-types.d.ts +6 -6
  50. package/lib/browser/notebook-types.d.ts.map +1 -1
  51. package/lib/browser/notebook-types.js +1 -1
  52. package/lib/browser/notebook-types.js.map +1 -1
  53. package/lib/browser/renderers/cell-output-webview.d.ts +1 -1
  54. package/lib/browser/renderers/cell-output-webview.d.ts.map +1 -1
  55. package/lib/browser/service/notebook-clipboard-service.js +4 -4
  56. package/lib/browser/service/notebook-clipboard-service.js.map +1 -1
  57. package/lib/browser/service/notebook-context-manager.d.ts.map +1 -1
  58. package/lib/browser/service/notebook-context-manager.js +14 -10
  59. package/lib/browser/service/notebook-context-manager.js.map +1 -1
  60. package/lib/browser/service/notebook-editor-widget-service.js +10 -10
  61. package/lib/browser/service/notebook-editor-widget-service.js.map +1 -1
  62. package/lib/browser/service/notebook-execution-service.d.ts.map +1 -1
  63. package/lib/browser/service/notebook-execution-service.js +22 -12
  64. package/lib/browser/service/notebook-execution-service.js.map +1 -1
  65. package/lib/browser/service/notebook-execution-state-service.d.ts +1 -1
  66. package/lib/browser/service/notebook-execution-state-service.d.ts.map +1 -1
  67. package/lib/browser/service/notebook-execution-state-service.js +22 -22
  68. package/lib/browser/service/notebook-execution-state-service.js.map +1 -1
  69. package/lib/browser/service/notebook-kernel-history-service.js +12 -12
  70. package/lib/browser/service/notebook-kernel-history-service.js.map +1 -1
  71. package/lib/browser/service/notebook-kernel-quick-pick-service.d.ts +7 -7
  72. package/lib/browser/service/notebook-kernel-quick-pick-service.d.ts.map +1 -1
  73. package/lib/browser/service/notebook-kernel-quick-pick-service.js +12 -12
  74. package/lib/browser/service/notebook-kernel-quick-pick-service.js.map +1 -1
  75. package/lib/browser/service/notebook-kernel-service.js +10 -10
  76. package/lib/browser/service/notebook-kernel-service.js.map +1 -1
  77. package/lib/browser/service/notebook-model-resolver-service.d.ts +1 -1
  78. package/lib/browser/service/notebook-model-resolver-service.d.ts.map +1 -1
  79. package/lib/browser/service/notebook-model-resolver-service.js +19 -25
  80. package/lib/browser/service/notebook-model-resolver-service.js.map +1 -1
  81. package/lib/browser/service/notebook-monaco-text-model-service.js +6 -6
  82. package/lib/browser/service/notebook-monaco-text-model-service.js.map +1 -1
  83. package/lib/browser/service/notebook-renderer-messaging-service.js +4 -4
  84. package/lib/browser/service/notebook-renderer-messaging-service.js.map +1 -1
  85. package/lib/browser/service/notebook-service.d.ts +1 -1
  86. package/lib/browser/service/notebook-service.d.ts.map +1 -1
  87. package/lib/browser/service/notebook-service.js +31 -25
  88. package/lib/browser/service/notebook-service.js.map +1 -1
  89. package/lib/browser/view/notebook-cell-editor.d.ts.map +1 -1
  90. package/lib/browser/view/notebook-cell-editor.js +27 -8
  91. package/lib/browser/view/notebook-cell-editor.js.map +1 -1
  92. package/lib/browser/view/notebook-cell-list-view.d.ts +4 -1
  93. package/lib/browser/view/notebook-cell-list-view.d.ts.map +1 -1
  94. package/lib/browser/view/notebook-cell-list-view.js +30 -9
  95. package/lib/browser/view/notebook-cell-list-view.js.map +1 -1
  96. package/lib/browser/view/notebook-cell-toolbar-factory.js +14 -14
  97. package/lib/browser/view/notebook-cell-toolbar-factory.js.map +1 -1
  98. package/lib/browser/view/notebook-cell-toolbar.js.map +1 -1
  99. package/lib/browser/view/notebook-code-cell-view.d.ts +1 -0
  100. package/lib/browser/view/notebook-code-cell-view.d.ts.map +1 -1
  101. package/lib/browser/view/notebook-code-cell-view.js +26 -20
  102. package/lib/browser/view/notebook-code-cell-view.js.map +1 -1
  103. package/lib/browser/view/notebook-main-toolbar.d.ts +18 -2
  104. package/lib/browser/view/notebook-main-toolbar.d.ts.map +1 -1
  105. package/lib/browser/view/notebook-main-toolbar.js +75 -17
  106. package/lib/browser/view/notebook-main-toolbar.js.map +1 -1
  107. package/lib/browser/view/notebook-markdown-cell-view.d.ts +1 -0
  108. package/lib/browser/view/notebook-markdown-cell-view.d.ts.map +1 -1
  109. package/lib/browser/view/notebook-markdown-cell-view.js +14 -8
  110. package/lib/browser/view/notebook-markdown-cell-view.js.map +1 -1
  111. package/lib/browser/view/notebook-viewport-service.js +2 -2
  112. package/lib/browser/view/notebook-viewport-service.js.map +1 -1
  113. package/lib/browser/view-model/notebook-cell-model.d.ts +5 -4
  114. package/lib/browser/view-model/notebook-cell-model.d.ts.map +1 -1
  115. package/lib/browser/view-model/notebook-cell-model.js +19 -20
  116. package/lib/browser/view-model/notebook-cell-model.js.map +1 -1
  117. package/lib/browser/view-model/notebook-cell-output-model.js +7 -7
  118. package/lib/browser/view-model/notebook-cell-output-model.js.map +1 -1
  119. package/lib/browser/view-model/notebook-model.d.ts +16 -6
  120. package/lib/browser/view-model/notebook-model.d.ts.map +1 -1
  121. package/lib/browser/view-model/notebook-model.js +73 -66
  122. package/lib/browser/view-model/notebook-model.js.map +1 -1
  123. package/lib/common/index.js +2 -2
  124. package/lib/common/index.js.map +1 -1
  125. package/lib/common/notebook-common.d.ts +4 -4
  126. package/lib/common/notebook-common.d.ts.map +1 -1
  127. package/lib/common/notebook-common.js +8 -8
  128. package/lib/common/notebook-common.js.map +1 -1
  129. package/package.json +8 -8
  130. package/src/browser/contributions/notebook-actions-contribution.ts +34 -12
  131. package/src/browser/contributions/notebook-cell-actions-contribution.ts +20 -19
  132. package/src/browser/contributions/notebook-context-keys.ts +7 -0
  133. package/src/browser/contributions/notebook-label-provider-contribution.ts +30 -8
  134. package/src/browser/contributions/notebook-outline-contribution.ts +11 -9
  135. package/src/browser/notebook-editor-widget-factory.ts +2 -3
  136. package/src/browser/notebook-editor-widget.tsx +8 -0
  137. package/src/browser/notebook-frontend-module.ts +2 -1
  138. package/src/browser/service/notebook-context-manager.ts +6 -1
  139. package/src/browser/service/notebook-execution-service.ts +12 -0
  140. package/src/browser/service/notebook-model-resolver-service.ts +12 -18
  141. package/src/browser/service/notebook-monaco-text-model-service.ts +2 -2
  142. package/src/browser/service/notebook-service.ts +20 -17
  143. package/src/browser/style/index.css +10 -1
  144. package/src/browser/view/notebook-cell-editor.tsx +27 -6
  145. package/src/browser/view/notebook-cell-list-view.tsx +35 -9
  146. package/src/browser/view/notebook-code-cell-view.tsx +7 -0
  147. package/src/browser/view/notebook-main-toolbar.tsx +82 -7
  148. package/src/browser/view/notebook-markdown-cell-view.tsx +6 -0
  149. package/src/browser/view-model/notebook-cell-model.ts +8 -6
  150. package/src/browser/view-model/notebook-model.ts +60 -49
  151. package/src/common/notebook-common.ts +1 -1
@@ -171,7 +171,7 @@
171
171
  }
172
172
 
173
173
  .theia-notebook-cell-divider {
174
- height: 20px;
174
+ height: 25px;
175
175
  width: 100%;
176
176
  }
177
177
 
@@ -225,6 +225,7 @@
225
225
 
226
226
  .theia-notebook-main-toolbar-item-text {
227
227
  padding: 0 4px;
228
+ white-space: nowrap;
228
229
  }
229
230
 
230
231
  .theia-notebook-toolbar-separator {
@@ -280,3 +281,11 @@
280
281
  line-height: 22px;
281
282
  opacity: 0.7;
282
283
  }
284
+
285
+ .theia-notebook-drag-ghost-image {
286
+ position: absolute;
287
+ top: -99999px;
288
+ left: -99999px;
289
+ height: 500px;
290
+ width: 500px;
291
+ }
@@ -25,6 +25,7 @@ import { NotebookContextManager } from '../service/notebook-context-manager';
25
25
  import { DisposableCollection, OS } from '@theia/core';
26
26
  import { NotebookViewportService } from './notebook-viewport-service';
27
27
  import { BareFontInfo } from '@theia/monaco-editor-core/esm/vs/editor/common/config/fontInfo';
28
+ import { NOTEBOOK_CELL_CURSOR_FIRST_LINE, NOTEBOOK_CELL_CURSOR_LAST_LINE } from '../contributions/notebook-context-keys';
28
29
 
29
30
  interface CellEditorProps {
30
31
  notebookModel: NotebookModel,
@@ -54,8 +55,19 @@ export class CellEditor extends React.Component<CellEditorProps, {}> {
54
55
 
55
56
  override componentDidMount(): void {
56
57
  this.disposeEditor();
57
- this.toDispose.push(this.props.cell.onWillFocusCellEditor(() => {
58
+ this.toDispose.push(this.props.cell.onWillFocusCellEditor(focusRequest => {
58
59
  this.editor?.getControl().focus();
60
+ const lineCount = this.editor?.getControl().getModel()?.getLineCount();
61
+ if (focusRequest && lineCount) {
62
+ this.editor?.getControl().setPosition(focusRequest === 'lastLine' ?
63
+ { lineNumber: lineCount, column: 1 } :
64
+ { lineNumber: focusRequest, column: 1 },
65
+ 'keyboard');
66
+ }
67
+ const currentLine = this.editor?.getControl().getPosition()?.lineNumber;
68
+ this.props.notebookContextManager.scopedStore.setContext(NOTEBOOK_CELL_CURSOR_FIRST_LINE, currentLine === 1);
69
+ this.props.notebookContextManager.scopedStore.setContext(NOTEBOOK_CELL_CURSOR_LAST_LINE, currentLine === lineCount);
70
+
59
71
  }));
60
72
 
61
73
  this.toDispose.push(this.props.cell.onDidChangeEditorOptions(options => {
@@ -66,11 +78,9 @@ export class CellEditor extends React.Component<CellEditorProps, {}> {
66
78
  this.editor?.setLanguage(language);
67
79
  }));
68
80
 
69
- this.toDispose.push(this.props.notebookModel.onDidChangeSelectedCell(() => {
70
- if (this.props.notebookModel.selectedCell !== this.props.cell && this.editor?.getControl().hasTextFocus()) {
71
- if (document.activeElement && 'blur' in document.activeElement) {
72
- (document.activeElement as HTMLElement).blur();
73
- }
81
+ this.toDispose.push(this.props.notebookModel.onDidChangeSelectedCell(e => {
82
+ if (e.cell !== this.props.cell && this.editor?.getControl().hasTextFocus()) {
83
+ this.props.notebookContextManager.context?.focus();
74
84
  }
75
85
  }));
76
86
  if (!this.props.notebookViewportService || (this.container && this.props.notebookViewportService.isElementInViewport(this.container))) {
@@ -119,10 +129,21 @@ export class CellEditor extends React.Component<CellEditorProps, {}> {
119
129
  }));
120
130
  this.toDispose.push(this.editor.getControl().onDidFocusEditorText(() => {
121
131
  this.props.notebookContextManager.onDidEditorTextFocus(true);
132
+ this.props.notebookModel.setSelectedCell(cell, false);
122
133
  }));
123
134
  this.toDispose.push(this.editor.getControl().onDidBlurEditorText(() => {
124
135
  this.props.notebookContextManager.onDidEditorTextFocus(false);
125
136
  }));
137
+ this.toDispose.push(this.editor.getControl().onDidChangeCursorPosition(e => {
138
+ if (e.secondaryPositions.length === 0) {
139
+ this.props.notebookContextManager.scopedStore.setContext(NOTEBOOK_CELL_CURSOR_FIRST_LINE, e.position.lineNumber === 1);
140
+ this.props.notebookContextManager.scopedStore.setContext(NOTEBOOK_CELL_CURSOR_LAST_LINE,
141
+ e.position.lineNumber === this.editor!.getControl().getModel()!.getLineCount());
142
+ } else {
143
+ this.props.notebookContextManager.scopedStore.setContext(NOTEBOOK_CELL_CURSOR_FIRST_LINE, false);
144
+ this.props.notebookContextManager.scopedStore.setContext(NOTEBOOK_CELL_CURSOR_LAST_LINE, false);
145
+ }
146
+ }));
126
147
  if (cell.editing && notebookModel.selectedCell === cell) {
127
148
  this.editor.getControl().focus();
128
149
  }
@@ -25,6 +25,7 @@ import { NotebookCellActionContribution } from '../contributions/notebook-cell-a
25
25
 
26
26
  export interface CellRenderer {
27
27
  render(notebookData: NotebookModel, cell: NotebookCellModel, index: number): React.ReactNode
28
+ renderDragImage(cell: NotebookCellModel): HTMLElement
28
29
  }
29
30
 
30
31
  interface CellListProps {
@@ -36,6 +37,7 @@ interface CellListProps {
36
37
 
37
38
  interface NotebookCellListState {
38
39
  selectedCell?: NotebookCellModel;
40
+ scrollIntoView: boolean;
39
41
  dragOverIndicator: { cell: NotebookCellModel, position: 'top' | 'bottom' } | undefined;
40
42
  }
41
43
 
@@ -43,19 +45,33 @@ export class NotebookCellListView extends React.Component<CellListProps, Noteboo
43
45
 
44
46
  protected toDispose = new DisposableCollection();
45
47
 
48
+ protected dragGhost: HTMLElement | undefined;
49
+
46
50
  constructor(props: CellListProps) {
47
51
  super(props);
48
- this.state = { selectedCell: props.notebookModel.selectedCell, dragOverIndicator: undefined };
52
+ this.state = { selectedCell: props.notebookModel.selectedCell, dragOverIndicator: undefined, scrollIntoView: true };
49
53
  this.toDispose.push(props.notebookModel.onDidAddOrRemoveCell(e => {
50
54
  if (e.newCellIds && e.newCellIds.length > 0) {
51
- this.setState({ ...this.state, selectedCell: this.props.notebookModel.cells.find(model => model.handle === e.newCellIds![e.newCellIds!.length - 1]) });
55
+ this.setState({
56
+ ...this.state,
57
+ selectedCell: this.props.notebookModel.cells.find(model => model.handle === e.newCellIds![e.newCellIds!.length - 1]),
58
+ scrollIntoView: true
59
+ });
52
60
  } else {
53
- this.setState({ ...this.state, selectedCell: this.props.notebookModel.cells.find(cell => cell === this.state.selectedCell) });
61
+ this.setState({
62
+ ...this.state,
63
+ selectedCell: this.props.notebookModel.cells.find(cell => cell === this.state.selectedCell),
64
+ scrollIntoView: false
65
+ });
54
66
  }
55
67
  }));
56
68
 
57
- this.toDispose.push(props.notebookModel.onDidChangeSelectedCell(cell => {
58
- this.setState({ ...this.state, selectedCell: cell });
69
+ this.toDispose.push(props.notebookModel.onDidChangeSelectedCell(e => {
70
+ this.setState({
71
+ ...this.state,
72
+ selectedCell: e.cell,
73
+ scrollIntoView: e.scrollIntoView
74
+ });
59
75
  }));
60
76
  }
61
77
 
@@ -77,13 +93,13 @@ export class NotebookCellListView extends React.Component<CellListProps, Noteboo
77
93
  <li className={'theia-notebook-cell' + (this.state.selectedCell === cell ? ' focused' : '') + (this.isEnabled() ? ' draggable' : '')}
78
94
  onClick={e => {
79
95
  this.setState({ ...this.state, selectedCell: cell });
80
- this.props.notebookModel.setSelectedCell(cell);
96
+ this.props.notebookModel.setSelectedCell(cell, false);
81
97
  }}
82
- onDragStart={e => this.onDragStart(e, index)}
98
+ onDragStart={e => this.onDragStart(e, index, cell)}
83
99
  onDragOver={e => this.onDragOver(e, cell)}
84
100
  onDrop={e => this.onDrop(e, index)}
85
101
  draggable={true}
86
- ref={ref => cell === this.state.selectedCell && ref?.scrollIntoView({ block: 'nearest' })}>
102
+ ref={ref => cell === this.state.selectedCell && this.state.scrollIntoView && ref?.scrollIntoView({ block: 'nearest' })}>
87
103
  <div className={'theia-notebook-cell-marker' + (this.state.selectedCell === cell ? ' theia-notebook-cell-marker-selected' : '')}></div>
88
104
  <div className='theia-notebook-cell-content'>
89
105
  {this.renderCellContent(cell, index)}
@@ -114,12 +130,22 @@ export class NotebookCellListView extends React.Component<CellListProps, Noteboo
114
130
  return renderer.render(this.props.notebookModel, cell, index);
115
131
  }
116
132
 
117
- protected onDragStart(event: React.DragEvent<HTMLLIElement>, index: number): void {
133
+ protected onDragStart(event: React.DragEvent<HTMLLIElement>, index: number, cell: NotebookCellModel): void {
118
134
  event.stopPropagation();
119
135
  if (!this.isEnabled()) {
120
136
  event.preventDefault();
121
137
  return;
122
138
  }
139
+
140
+ if (this.dragGhost) {
141
+ this.dragGhost.remove();
142
+ }
143
+ this.dragGhost = document.createElement('div');
144
+ this.dragGhost.classList.add('theia-notebook-drag-ghost-image');
145
+ this.dragGhost.appendChild(this.props.renderers.get(cell.cellKind)?.renderDragImage(cell) ?? document.createElement('div'));
146
+ document.body.appendChild(this.dragGhost);
147
+ event.dataTransfer.setDragImage(this.dragGhost, -10, 0);
148
+
123
149
  event.dataTransfer.setData('text/theia-notebook-cell-index', index.toString());
124
150
  event.dataTransfer.setData('text/plain', this.props.notebookModel.cells[index].source);
125
151
  }
@@ -99,6 +99,13 @@ export class NotebookCodeCellRenderer implements CellRenderer {
99
99
  </div >;
100
100
  }
101
101
 
102
+ renderDragImage(cell: NotebookCellModel): HTMLElement {
103
+ const dragImage = document.createElement('div');
104
+ dragImage.className = 'theia-notebook-drag-image';
105
+ dragImage.textContent = nls.localize('theia/notebook/dragGhostImage/codeText', 'Code cell selected');
106
+ return dragImage;
107
+ }
108
+
102
109
  protected getOrCreateMonacoFontInfo(): BareFontInfo {
103
110
  if (!this.fontInfo) {
104
111
  this.fontInfo = this.createFontInfo();
@@ -15,7 +15,7 @@
15
15
  // *****************************************************************************
16
16
  import { ArrayUtils, CommandRegistry, CompoundMenuNodeRole, DisposableCollection, MenuModelRegistry, MenuNode, nls } from '@theia/core';
17
17
  import * as React from '@theia/core/shared/react';
18
- import { codicon } from '@theia/core/lib/browser';
18
+ import { codicon, ContextMenuRenderer } from '@theia/core/lib/browser';
19
19
  import { NotebookCommands, NotebookMenus } from '../contributions/notebook-actions-contribution';
20
20
  import { NotebookModel } from '../view-model/notebook-model';
21
21
  import { NotebookKernelService } from '../service/notebook-kernel-service';
@@ -32,6 +32,7 @@ export interface NotebookMainToolbarProps {
32
32
  contextKeyService: ContextKeyService;
33
33
  editorNode: HTMLElement;
34
34
  notebookContextManager: NotebookContextManager;
35
+ contextMenuRenderer: ContextMenuRenderer;
35
36
  }
36
37
 
37
38
  @injectable()
@@ -41,6 +42,7 @@ export class NotebookMainToolbarRenderer {
41
42
  @inject(MenuModelRegistry) protected readonly menuRegistry: MenuModelRegistry;
42
43
  @inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService;
43
44
  @inject(NotebookContextManager) protected readonly notebookContextManager: NotebookContextManager;
45
+ @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer;
44
46
 
45
47
  render(notebookModel: NotebookModel, editorNode: HTMLElement): React.ReactNode {
46
48
  return <NotebookMainToolbar notebookModel={notebookModel}
@@ -49,11 +51,20 @@ export class NotebookMainToolbarRenderer {
49
51
  commandRegistry={this.commandRegistry}
50
52
  contextKeyService={this.contextKeyService}
51
53
  editorNode={editorNode}
52
- notebookContextManager={this.notebookContextManager} />;
54
+ notebookContextManager={this.notebookContextManager}
55
+ contextMenuRenderer={this.contextMenuRenderer} />;
53
56
  }
54
57
  }
55
58
 
56
- export class NotebookMainToolbar extends React.Component<NotebookMainToolbarProps, { selectedKernelLabel?: string }> {
59
+ interface NotebookMainToolbarState {
60
+ selectedKernelLabel?: string;
61
+ numberOfHiddenItems: number;
62
+ }
63
+
64
+ export class NotebookMainToolbar extends React.Component<NotebookMainToolbarProps, NotebookMainToolbarState> {
65
+
66
+ // The minimum area between items and kernel select before hiding items in a context menu
67
+ static readonly MIN_FREE_AREA = 10;
57
68
 
58
69
  protected toDispose = new DisposableCollection();
59
70
 
@@ -61,10 +72,18 @@ export class NotebookMainToolbar extends React.Component<NotebookMainToolbarProp
61
72
  NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_CELL_ADD_GROUP[NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_CELL_ADD_GROUP.length - 1],
62
73
  NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_EXECUTION_GROUP[NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_EXECUTION_GROUP.length - 1]];
63
74
 
75
+ protected gapElement: HTMLDivElement | undefined;
76
+ protected lastGapElementWidth: number = 0;
77
+
78
+ protected resizeObserver: ResizeObserver = new ResizeObserver(() => this.calculateItemsToHide());
79
+
64
80
  constructor(props: NotebookMainToolbarProps) {
65
81
  super(props);
66
82
 
67
- this.state = { selectedKernelLabel: props.notebookKernelService.getSelectedOrSuggestedKernel(props.notebookModel)?.label };
83
+ this.state = {
84
+ selectedKernelLabel: props.notebookKernelService.getSelectedOrSuggestedKernel(props.notebookModel)?.label,
85
+ numberOfHiddenItems: 0,
86
+ };
68
87
  this.toDispose.push(props.notebookKernelService.onDidChangeSelectedKernel(event => {
69
88
  if (props.notebookModel.uri.isEqual(event.notebook)) {
70
89
  this.setState({ selectedKernelLabel: props.notebookKernelService.getKernel(event.newKernel ?? '')?.label });
@@ -97,10 +116,49 @@ export class NotebookMainToolbar extends React.Component<NotebookMainToolbarProp
97
116
  this.toDispose.dispose();
98
117
  }
99
118
 
119
+ override componentDidUpdate(): void {
120
+ this.calculateItemsToHide();
121
+ }
122
+
123
+ override componentDidMount(): void {
124
+ this.calculateItemsToHide();
125
+ }
126
+
127
+ protected calculateItemsToHide(): void {
128
+ const numberOfMenuItems = this.getMenuItems().length;
129
+ if (this.gapElement && this.gapElement.getBoundingClientRect().width < NotebookMainToolbar.MIN_FREE_AREA && this.state.numberOfHiddenItems < numberOfMenuItems) {
130
+ this.setState({ ...this.state, numberOfHiddenItems: this.state.numberOfHiddenItems + 1 });
131
+ this.lastGapElementWidth = this.gapElement.getBoundingClientRect().width;
132
+ } else if (this.gapElement && this.gapElement.getBoundingClientRect().width > this.lastGapElementWidth && this.state.numberOfHiddenItems > 0) {
133
+ this.setState({ ...this.state, numberOfHiddenItems: 0 });
134
+ this.lastGapElementWidth = this.gapElement.getBoundingClientRect().width;
135
+ }
136
+ }
137
+
138
+ protected renderContextMenu(event: MouseEvent, menuItems: readonly MenuNode[]): void {
139
+ const hiddenItems = menuItems.slice(menuItems.length - this.calculateNumberOfHiddenItems(menuItems));
140
+ const contextMenu = this.props.menuRegistry.getMenu([NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_HIDDEN_ITEMS_CONTEXT_MENU]);
141
+
142
+ contextMenu.children.map(item => item.id).forEach(id => contextMenu.removeNode(id));
143
+ hiddenItems.forEach(item => contextMenu.addNode(item));
144
+
145
+ this.props.contextMenuRenderer.render({
146
+ anchor: event,
147
+ menuPath: [NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_HIDDEN_ITEMS_CONTEXT_MENU],
148
+ context: this.props.editorNode,
149
+ args: [this.props.notebookModel.uri]
150
+ });
151
+ }
152
+
100
153
  override render(): React.ReactNode {
154
+ const menuItems = this.getMenuItems();
101
155
  return <div className='theia-notebook-main-toolbar'>
102
- {this.getMenuItems().map(item => this.renderMenuItem(item))}
103
- <div style={{ flexGrow: 1 }}></div>
156
+ {menuItems.slice(0, menuItems.length - this.calculateNumberOfHiddenItems(menuItems)).map(item => this.renderMenuItem(item))}
157
+ {
158
+ this.state.numberOfHiddenItems > 0 &&
159
+ <span className={`${codicon('ellipsis')} action-label theia-notebook-main-toolbar-item`} onClick={e => this.renderContextMenu(e.nativeEvent, menuItems)} />
160
+ }
161
+ <div ref={element => this.gapElementChanged(element)} style={{ flexGrow: 1 }}></div>
104
162
  <div className='theia-notebook-main-toolbar-item action-label'
105
163
  onClick={() => this.props.commandRegistry.executeCommand(NotebookCommands.SELECT_KERNEL_COMMAND.id, this.props.notebookModel)}>
106
164
  <span className={codicon('server-environment')} />
@@ -108,7 +166,18 @@ export class NotebookMainToolbar extends React.Component<NotebookMainToolbarProp
108
166
  {this.state.selectedKernelLabel ?? nls.localizeByDefault('Select Kernel')}
109
167
  </span>
110
168
  </div>
111
- </div>;
169
+ </div >;
170
+ }
171
+
172
+ protected gapElementChanged(element: HTMLDivElement | null): void {
173
+ if (this.gapElement) {
174
+ this.resizeObserver.unobserve(this.gapElement);
175
+ }
176
+ this.gapElement = element ?? undefined;
177
+ if (this.gapElement) {
178
+ this.lastGapElementWidth = this.gapElement.getBoundingClientRect().width;
179
+ this.resizeObserver.observe(this.gapElement);
180
+ }
112
181
  }
113
182
 
114
183
  protected renderMenuItem(item: MenuNode, submenu?: string): React.ReactNode {
@@ -157,4 +226,10 @@ export class NotebookMainToolbar extends React.Component<NotebookMainToolbarProp
157
226
  menus.filter(item => item.children && item.children.length > 0)
158
227
  .forEach(item => this.getAllContextKeys(item.children!, keySet));
159
228
  }
229
+
230
+ protected calculateNumberOfHiddenItems(allMenuItems: readonly MenuNode[]): number {
231
+ return this.state.numberOfHiddenItems >= allMenuItems.length ?
232
+ allMenuItems.length :
233
+ this.state.numberOfHiddenItems % allMenuItems.length;
234
+ }
160
235
  }
@@ -42,6 +42,12 @@ export class NotebookMarkdownCellRenderer implements CellRenderer {
42
42
  cell={cell} notebookModel={notebookModel} notebookContextManager={this.notebookContextManager} />;
43
43
  }
44
44
 
45
+ renderDragImage(cell: NotebookCellModel): HTMLElement {
46
+ const dragImage = document.createElement('div');
47
+ dragImage.className = 'theia-notebook-drag-image';
48
+ dragImage.textContent = nls.localize('theia/notebook/dragGhostImage/markdownText', 'Mardown cell selected');
49
+ return dragImage;
50
+ }
45
51
  }
46
52
 
47
53
  interface MarkdownCellProps {
@@ -36,6 +36,8 @@ import { LanguageService } from '@theia/core/lib/browser/language-service';
36
36
  export const NotebookCellModelFactory = Symbol('NotebookModelFactory');
37
37
  export type NotebookCellModelFactory = (props: NotebookCellModelProps) => NotebookCellModel;
38
38
 
39
+ export type CellEditorFocusRequest = number | 'lastLine' | undefined;
40
+
39
41
  export function createNotebookCellModelContainer(parent: interfaces.Container, props: NotebookCellModelProps): interfaces.Container {
40
42
  const child = parent.createChild();
41
43
 
@@ -104,7 +106,7 @@ export class NotebookCellModel implements NotebookCell, Disposable {
104
106
  protected readonly onDidRequestCellEditChangeEmitter = new Emitter<boolean>();
105
107
  readonly onDidRequestCellEditChange = this.onDidRequestCellEditChangeEmitter.event;
106
108
 
107
- protected readonly onWillFocusCellEditorEmitter = new Emitter<void>();
109
+ protected readonly onWillFocusCellEditorEmitter = new Emitter<CellEditorFocusRequest>();
108
110
  readonly onWillFocusCellEditor = this.onWillFocusCellEditorEmitter.event;
109
111
 
110
112
  protected readonly onWillBlurCellEditorEmitter = new Emitter<void>();
@@ -262,7 +264,6 @@ export class NotebookCellModel implements NotebookCell, Disposable {
262
264
  this.onDidChangeMetadataEmitter.dispose();
263
265
  this.onDidChangeInternalMetadataEmitter.dispose();
264
266
  this.onDidChangeLanguageEmitter.dispose();
265
- this.textModel?.dispose();
266
267
  this.toDispose.dispose();
267
268
  }
268
269
 
@@ -278,9 +279,9 @@ export class NotebookCellModel implements NotebookCell, Disposable {
278
279
  this.onDidRequestCellEditChangeEmitter.fire(false);
279
280
  }
280
281
 
281
- requestFocusEditor(): void {
282
+ requestFocusEditor(focusRequest?: CellEditorFocusRequest): void {
282
283
  this.requestEdit();
283
- this.onWillFocusCellEditorEmitter.fire();
284
+ this.onWillFocusCellEditorEmitter.fire(focusRequest);
284
285
  }
285
286
 
286
287
  requestBlurEditor(): void {
@@ -354,9 +355,10 @@ export class NotebookCellModel implements NotebookCell, Disposable {
354
355
 
355
356
  const ref = await this.textModelService.getOrCreateNotebookCellModelReference(this.uri);
356
357
  this.textModel = ref.object;
357
- this.textModel.onDidChangeContent(e => {
358
+ this.toDispose.push(ref);
359
+ this.toDispose.push(this.textModel.onDidChangeContent(e => {
358
360
  this.props.source = e.model.getText();
359
- });
361
+ }));
360
362
  return ref.object;
361
363
  }
362
364
 
@@ -33,6 +33,7 @@ import { NotebookCellModel, NotebookCellModelFactory } from './notebook-cell-mod
33
33
  import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify';
34
34
  import { UndoRedoService } from '@theia/editor/lib/browser/undo-redo-service';
35
35
  import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
36
+ import type { NotebookModelResolverService } from '../service/notebook-model-resolver-service';
36
37
 
37
38
  export const NotebookModelFactory = Symbol('NotebookModelFactory');
38
39
 
@@ -45,6 +46,8 @@ export function createNotebookModelContainer(parent: interfaces.Container, props
45
46
  return child;
46
47
  }
47
48
 
49
+ export const NotebookModelResolverServiceProxy = Symbol('NotebookModelResolverServiceProxy');
50
+
48
51
  const NotebookModelProps = Symbol('NotebookModelProps');
49
52
  export interface NotebookModelProps {
50
53
  data: NotebookData;
@@ -53,6 +56,11 @@ export interface NotebookModelProps {
53
56
  serializer: NotebookSerializer;
54
57
  }
55
58
 
59
+ export interface SelectedCellChangeEvent {
60
+ cell: NotebookCellModel | undefined;
61
+ scrollIntoView: boolean;
62
+ }
63
+
56
64
  @injectable()
57
65
  export class NotebookModel implements Saveable, Disposable {
58
66
 
@@ -68,7 +76,10 @@ export class NotebookModel implements Saveable, Disposable {
68
76
  protected readonly onDidChangeContentEmitter = new QueueableEmitter<NotebookContentChangedEvent>();
69
77
  readonly onDidChangeContent = this.onDidChangeContentEmitter.event;
70
78
 
71
- protected readonly onDidChangeSelectedCellEmitter = new Emitter<NotebookCellModel | undefined>();
79
+ protected readonly onContentChangedEmitter = new Emitter<void>();
80
+ readonly onContentChanged = this.onContentChangedEmitter.event;
81
+
82
+ protected readonly onDidChangeSelectedCellEmitter = new Emitter<SelectedCellChangeEvent>();
72
83
  readonly onDidChangeSelectedCell = this.onDidChangeSelectedCellEmitter.event;
73
84
 
74
85
  protected readonly onDidDisposeEmitter = new Emitter<void>();
@@ -89,15 +100,20 @@ export class NotebookModel implements Saveable, Disposable {
89
100
 
90
101
  @inject(NotebookCellModelFactory)
91
102
  protected cellModelFactory: NotebookCellModelFactory;
92
- readonly autoSave: 'off' | 'afterDelay' | 'onFocusChange' | 'onWindowChange';
103
+
104
+ @inject(NotebookModelResolverServiceProxy)
105
+ protected modelResolverService: NotebookModelResolverService;
93
106
 
94
107
  protected nextHandle: number = 0;
95
108
 
96
109
  protected _dirty = false;
97
110
 
98
111
  set dirty(dirty: boolean) {
112
+ const oldState = this._dirty;
99
113
  this._dirty = dirty;
100
- this.onDirtyChangedEmitter.fire();
114
+ if (oldState !== dirty) {
115
+ this.onDirtyChangedEmitter.fire();
116
+ }
101
117
  }
102
118
 
103
119
  get dirty(): boolean {
@@ -156,28 +172,20 @@ export class NotebookModel implements Saveable, Disposable {
156
172
  this.onDidDisposeEmitter.fire();
157
173
  }
158
174
 
159
- async save(options: SaveOptions): Promise<void> {
175
+ async save(options?: SaveOptions): Promise<void> {
160
176
  this.dirtyCells = [];
161
177
  this.dirty = false;
162
178
 
163
- const serializedNotebook = await this.props.serializer.fromNotebook({
164
- cells: this.cells.map(cell => cell.getData()),
165
- metadata: this.metadata
166
- });
179
+ const data = this.getData();
180
+ const serializedNotebook = await this.props.serializer.fromNotebook(data);
167
181
  this.fileService.writeFile(this.uri, serializedNotebook);
168
182
 
169
183
  this.onDidSaveNotebookEmitter.fire();
170
184
  }
171
185
 
172
186
  createSnapshot(): Saveable.Snapshot {
173
- const model = this;
174
187
  return {
175
- read(): string {
176
- return JSON.stringify({
177
- cells: model.cells.map(cell => cell.getData()),
178
- metadata: model.metadata
179
- });
180
- }
188
+ read: () => JSON.stringify(this.getData())
181
189
  };
182
190
  }
183
191
 
@@ -186,28 +194,20 @@ export class NotebookModel implements Saveable, Disposable {
186
194
  if (!rawData) {
187
195
  throw new Error('could not read notebook snapshot');
188
196
  }
189
- const data = JSON.parse(rawData);
190
- const cells = data.cells.map((cell: CellData, index: number) => {
191
- const handle = this.nextHandle++;
192
- return this.cellModelFactory({
193
- uri: CellUri.generate(this.uri, handle),
194
- handle: handle,
195
- source: cell.source,
196
- language: cell.language,
197
- cellKind: cell.cellKind,
198
- outputs: cell.outputs,
199
- metadata: cell.metadata,
200
- internalMetadata: cell.internalMetadata,
201
- collapseState: cell.collapseState
202
- });
203
- });
204
- this.addCellOutputListeners(cells);
205
-
206
- this.metadata = data.metadata;
207
-
197
+ const data = JSON.parse(rawData) as NotebookData;
198
+ this.setData(data);
208
199
  }
209
200
 
210
201
  async revert(options?: Saveable.RevertOptions): Promise<void> {
202
+ if (!options?.soft) {
203
+ // Load the data from the file again
204
+ try {
205
+ const data = await this.modelResolverService.resolveExistingNotebookData(this.props.resource, this.props.viewType);
206
+ this.setData(data, false);
207
+ } catch (err) {
208
+ console.error('Failed to revert notebook', err);
209
+ }
210
+ }
211
211
  this.dirty = false;
212
212
  }
213
213
 
@@ -222,11 +222,23 @@ export class NotebookModel implements Saveable, Disposable {
222
222
  this.dirtyCells.splice(this.dirtyCells.indexOf(cell), 1);
223
223
  }
224
224
 
225
- const oldDirtyState = this._dirty;
226
- this._dirty = this.dirtyCells.length > 0;
227
- if (this.dirty !== oldDirtyState) {
228
- this.onDirtyChangedEmitter.fire();
229
- }
225
+ this.dirty = this.dirtyCells.length > 0;
226
+ }
227
+
228
+ setData(data: NotebookData, markDirty = true): void {
229
+ // Replace all cells in the model
230
+ this.dirtyCells = [];
231
+ this.replaceCells(0, this.cells.length, data.cells, false);
232
+ this.metadata = data.metadata;
233
+ this.dirty = markDirty;
234
+ this.onDidChangeContentEmitter.fire();
235
+ }
236
+
237
+ getData(): NotebookData {
238
+ return {
239
+ cells: this.cells.map(cell => cell.getData()),
240
+ metadata: this.metadata
241
+ };
230
242
  }
231
243
 
232
244
  undo(): void {
@@ -239,10 +251,10 @@ export class NotebookModel implements Saveable, Disposable {
239
251
  this.undoRedoService.redo(this.uri);
240
252
  }
241
253
 
242
- setSelectedCell(cell: NotebookCellModel): void {
254
+ setSelectedCell(cell: NotebookCellModel, scrollIntoView?: boolean): void {
243
255
  if (this.selectedCell !== cell) {
244
256
  this.selectedCell = cell;
245
- this.onDidChangeSelectedCellEmitter.fire(cell);
257
+ this.onDidChangeSelectedCellEmitter.fire({ cell, scrollIntoView: scrollIntoView ?? true });
246
258
  }
247
259
  }
248
260
 
@@ -271,16 +283,19 @@ export class NotebookModel implements Saveable, Disposable {
271
283
  end: edit.editType === CellEditType.Replace ? edit.index + edit.count : cellIndex,
272
284
  originalIndex: index
273
285
  };
274
- }).filter(edit => !!edit);
286
+ });
275
287
 
276
288
  for (const { edit, cellIndex } of editsWithDetails) {
277
289
  const cell = this.cells[cellIndex];
278
290
  if (cell) {
279
291
  this.cellDirtyChanged(cell, true);
280
292
  }
293
+
294
+ let scrollIntoView = true;
281
295
  switch (edit.editType) {
282
296
  case CellEditType.Replace:
283
297
  this.replaceCells(edit.index, edit.count, edit.cells, computeUndoRedo);
298
+ scrollIntoView = edit.cells.length > 0;
284
299
  break;
285
300
  case CellEditType.Output: {
286
301
  if (edit.append) {
@@ -323,15 +338,15 @@ export class NotebookModel implements Saveable, Disposable {
323
338
 
324
339
  // if selected cell is affected update it because it can potentially have been replaced
325
340
  if (cell === this.selectedCell) {
326
- this.setSelectedCell(this.cells[cellIndex]);
341
+ this.setSelectedCell(this.cells[Math.min(cellIndex, this.cells.length - 1)], scrollIntoView);
327
342
  }
328
343
  }
329
344
 
330
345
  this.onDidChangeContentEmitter.fire();
331
-
346
+ this.onContentChangedEmitter.fire();
332
347
  }
333
348
 
334
- protected async replaceCells(start: number, deleteCount: number, newCells: CellData[], computeUndoRedo: boolean): Promise<void> {
349
+ protected replaceCells(start: number, deleteCount: number, newCells: CellData[], computeUndoRedo: boolean): void {
335
350
  const cells = newCells.map(cell => {
336
351
  const handle = this.nextHandle++;
337
352
  return this.cellModelFactory({
@@ -362,10 +377,6 @@ export class NotebookModel implements Saveable, Disposable {
362
377
  async () => this.replaceCells(start, deleteCount, newCells, false));
363
378
  }
364
379
 
365
- // Ensure that all text model have been created
366
- // Otherwise we run into a race condition once we fire `onDidChangeContent`
367
- await Promise.all(cells.map(cell => cell.resolveTextModel()));
368
-
369
380
  this.onDidAddOrRemoveCellEmitter.fire({ rawEvent: { kind: NotebookCellsChangeType.ModelChange, changes }, newCellIds: cells.map(cell => cell.handle) });
370
381
  this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.ModelChange, changes });
371
382
  if (cells.length > 0) {
@@ -273,7 +273,7 @@ export namespace CellUri {
273
273
  const s = handle.toString(_radix);
274
274
  const p = s.length < _lengths.length ? _lengths[s.length - 1] : 'z';
275
275
 
276
- const fragment = `${p}${s}s${Buffer.from(BinaryBuffer.fromString(notebook.scheme).buffer).toString('base64')} `;
276
+ const fragment = `${p}${s}s${Buffer.from(BinaryBuffer.fromString(notebook.scheme).buffer).toString('base64')}`;
277
277
  return notebook.withScheme(cellUriScheme).withFragment(fragment);
278
278
  }
279
279