1 |
|
2 |
|
3 |
|
4 | import { Cell } from '@jupyterlab/cells';
|
5 | import { ISessionContext } from '@jupyterlab/apputils';
|
6 | import { CodeEditor } from '@jupyterlab/codeeditor';
|
7 | import { KernelMessage } from '@jupyterlab/services';
|
8 | import {
|
9 | ITranslator,
|
10 | nullTranslator,
|
11 | TranslationBundle
|
12 | } from '@jupyterlab/translation';
|
13 | import { IDisposable } from '@lumino/disposable';
|
14 | import { Signal } from '@lumino/signaling';
|
15 | import { IKernelConnection } from '@jupyterlab/services/lib/kernel/kernel';
|
16 |
|
17 |
|
18 |
|
19 |
|
20 | export interface INotebookHistory extends IDisposable {
|
21 | |
22 |
|
23 |
|
24 | editor: CodeEditor.IEditor | null;
|
25 |
|
26 | |
27 |
|
28 |
|
29 | readonly placeholder: string;
|
30 |
|
31 | |
32 |
|
33 |
|
34 | readonly kernelSession: string;
|
35 |
|
36 | |
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 | back(activeCell: Cell): Promise<string | undefined>;
|
44 |
|
45 | |
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 | forward(activeCell: Cell): Promise<string | undefined>;
|
53 |
|
54 | |
55 |
|
56 |
|
57 | reset(): void;
|
58 |
|
59 | |
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 | updateEditor(activeCell: Cell, content: string | undefined): void;
|
66 | }
|
67 |
|
68 |
|
69 |
|
70 |
|
71 | export class NotebookHistory implements INotebookHistory {
|
72 | |
73 |
|
74 |
|
75 | constructor(options: NotebookHistory.IOptions) {
|
76 | this._sessionContext = options.sessionContext;
|
77 | this._trans = (options.translator || nullTranslator).load('jupyterlab');
|
78 | void this._handleKernel().then(() => {
|
79 | this._sessionContext.kernelChanged.connect(this._handleKernel, this);
|
80 | });
|
81 | this._toRequest = this._requestBatchSize;
|
82 | }
|
83 |
|
84 | |
85 |
|
86 |
|
87 | private _sessionContext: ISessionContext;
|
88 |
|
89 | |
90 |
|
91 |
|
92 | private _trans: TranslationBundle;
|
93 |
|
94 | |
95 |
|
96 |
|
97 | private _toRequest: number;
|
98 |
|
99 | |
100 |
|
101 |
|
102 | private _requestBatchSize: number = 10;
|
103 |
|
104 | |
105 |
|
106 |
|
107 | get editor(): CodeEditor.IEditor | null {
|
108 | return this._editor;
|
109 | }
|
110 |
|
111 | set editor(value: CodeEditor.IEditor | null) {
|
112 | if (this._editor === value) {
|
113 | return;
|
114 | }
|
115 |
|
116 | const prev = this._editor;
|
117 | if (prev) {
|
118 | prev.model.sharedModel.changed.disconnect(this.onTextChange, this);
|
119 | }
|
120 |
|
121 | this._editor = value;
|
122 |
|
123 | if (value) {
|
124 | value.model.sharedModel.changed.connect(this.onTextChange, this);
|
125 | }
|
126 | }
|
127 |
|
128 | |
129 |
|
130 |
|
131 | get placeholder(): string {
|
132 | return this._placeholder;
|
133 | }
|
134 |
|
135 | |
136 |
|
137 |
|
138 | get kernelSession(): string {
|
139 | return this._kernelSession;
|
140 | }
|
141 |
|
142 | |
143 |
|
144 |
|
145 | get isDisposed(): boolean {
|
146 | return this._isDisposed;
|
147 | }
|
148 |
|
149 | |
150 |
|
151 |
|
152 | dispose(): void {
|
153 | this._isDisposed = true;
|
154 | this._history.length = 0;
|
155 | Signal.clearData(this);
|
156 | }
|
157 |
|
158 | |
159 |
|
160 |
|
161 |
|
162 |
|
163 | protected async checkSession(activeCell: Cell): Promise<void> {
|
164 | if (!this._hasSession) {
|
165 | await this._retrieveHistory();
|
166 | this._hasSession = true;
|
167 | this.editor = activeCell.editor;
|
168 | this._placeholder = this._editor?.model.sharedModel.getSource() || '';
|
169 |
|
170 | this.setFilter(this._placeholder);
|
171 | this._cursor = this._filtered.length - 1;
|
172 | }
|
173 | }
|
174 |
|
175 | |
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 | async back(activeCell: Cell): Promise<string | undefined> {
|
183 | await this.checkSession(activeCell);
|
184 | --this._cursor;
|
185 | if (this._cursor < 0) {
|
186 | await this.fetchBatch();
|
187 | }
|
188 | this._cursor = Math.max(0, this._cursor);
|
189 | const content = this._filtered[this._cursor];
|
190 |
|
191 | return content;
|
192 | }
|
193 |
|
194 | |
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 |
|
201 | async forward(activeCell: Cell): Promise<string | undefined> {
|
202 | await this.checkSession(activeCell);
|
203 | ++this._cursor;
|
204 | this._cursor = Math.min(this._filtered.length - 1, this._cursor);
|
205 | const content = this._filtered[this._cursor];
|
206 |
|
207 | return content;
|
208 | }
|
209 |
|
210 | |
211 |
|
212 |
|
213 |
|
214 |
|
215 |
|
216 | updateEditor(activeCell: Cell, content: string | undefined): void {
|
217 | if (activeCell) {
|
218 | const model = activeCell.editor?.model;
|
219 | const source = model?.sharedModel.getSource();
|
220 | if (this.isDisposed || !content) {
|
221 | return;
|
222 | }
|
223 | if (source === content) {
|
224 | return;
|
225 | }
|
226 | this._setByHistory = true;
|
227 | model?.sharedModel.setSource(content);
|
228 | let columnPos = 0;
|
229 | columnPos = content.indexOf('\n');
|
230 | if (columnPos < 0) {
|
231 | columnPos = content.length;
|
232 | }
|
233 | activeCell.editor?.setCursorPosition({ line: 0, column: columnPos });
|
234 | }
|
235 | }
|
236 |
|
237 | |
238 |
|
239 |
|
240 | reset(): void {
|
241 | this._hasSession = false;
|
242 | this._placeholder = '';
|
243 | this._toRequest = this._requestBatchSize;
|
244 | }
|
245 |
|
246 | |
247 |
|
248 |
|
249 |
|
250 | private async fetchBatch() {
|
251 | this._toRequest += this._requestBatchSize;
|
252 | let oldFilteredReversed = this._filtered.slice().reverse();
|
253 | let oldHistory = this._history.slice();
|
254 | await this._retrieveHistory().then(() => {
|
255 | this.setFilter(this._placeholder);
|
256 | let cursorOffset = 0;
|
257 | let filteredReversed = this._filtered.slice().reverse();
|
258 | for (let i = 0; i < oldFilteredReversed.length; i++) {
|
259 | let item = oldFilteredReversed[i];
|
260 | for (let ij = i + cursorOffset; ij < filteredReversed.length; ij++) {
|
261 | if (item === filteredReversed[ij]) {
|
262 | break;
|
263 | } else {
|
264 | cursorOffset += 1;
|
265 | }
|
266 | }
|
267 | }
|
268 | this._cursor =
|
269 | this._filtered.length - (oldFilteredReversed.length + 1) - cursorOffset;
|
270 | });
|
271 | if (this._cursor < 0) {
|
272 | if (this._history.length > oldHistory.length) {
|
273 | await this.fetchBatch();
|
274 | }
|
275 | }
|
276 | }
|
277 |
|
278 | |
279 |
|
280 |
|
281 |
|
282 |
|
283 |
|
284 |
|
285 |
|
286 |
|
287 |
|
288 | protected onHistory(
|
289 | value: KernelMessage.IHistoryReplyMsg,
|
290 | cell?: Cell
|
291 | ): void {
|
292 | this._history.length = 0;
|
293 | let last = ['', '', ''];
|
294 | let current = ['', '', ''];
|
295 | let kernelSession = '';
|
296 | if (value.content.status === 'ok') {
|
297 | for (let i = 0; i < value.content.history.length; i++) {
|
298 | current = value.content.history[i] as string[];
|
299 | if (current !== last) {
|
300 | kernelSession = (value.content.history[i] as string[])[0];
|
301 | this._history.push((last = current));
|
302 | }
|
303 | }
|
304 |
|
305 | if (!this.kernelSession) {
|
306 | if (current[2] == cell?.model.sharedModel.getSource()) {
|
307 | this._kernelSession = kernelSession;
|
308 | }
|
309 | }
|
310 | }
|
311 | }
|
312 |
|
313 | |
314 |
|
315 |
|
316 | protected onTextChange(): void {
|
317 | if (this._setByHistory) {
|
318 | this._setByHistory = false;
|
319 | return;
|
320 | }
|
321 | this.reset();
|
322 | }
|
323 |
|
324 | |
325 |
|
326 |
|
327 | private async _handleKernel(): Promise<void> {
|
328 | this._kernel = this._sessionContext.session?.kernel;
|
329 | if (!this._kernel) {
|
330 | this._history.length = 0;
|
331 | return;
|
332 | }
|
333 | await this._retrieveHistory().catch();
|
334 | return;
|
335 | }
|
336 |
|
337 | |
338 |
|
339 |
|
340 |
|
341 |
|
342 | private async _retrieveHistory(cell?: Cell): Promise<void> {
|
343 | return await this._kernel
|
344 | ?.requestHistory(request(this._toRequest))
|
345 | .then(v => {
|
346 | this.onHistory(v, cell);
|
347 | })
|
348 | .catch(() => {
|
349 | console.warn(this._trans.__('History was unable to be retrieved'));
|
350 | });
|
351 | }
|
352 |
|
353 | |
354 |
|
355 |
|
356 |
|
357 |
|
358 | protected setFilter(filterStr: string = ''): void {
|
359 |
|
360 | this._filtered.length = 0;
|
361 |
|
362 | let last = '';
|
363 | let current = '';
|
364 | for (let i = 0; i < this._history.length; i++) {
|
365 | current = this._history[i][2] as string;
|
366 | if (current !== last && filterStr !== current) {
|
367 | this._filtered.push((last = current));
|
368 | }
|
369 | }
|
370 | this._filtered.push(filterStr);
|
371 | }
|
372 |
|
373 | private _cursor = 0;
|
374 | private _hasSession = false;
|
375 | private _history: Array<Array<string>> = [];
|
376 | private _placeholder: string = '';
|
377 | private _kernelSession: string = '';
|
378 | private _setByHistory = false;
|
379 | private _isDisposed = false;
|
380 | private _editor: CodeEditor.IEditor | null = null;
|
381 | private _filtered: string[] = [];
|
382 | private _kernel: IKernelConnection | null | undefined = null;
|
383 | }
|
384 |
|
385 |
|
386 |
|
387 |
|
388 | export namespace NotebookHistory {
|
389 | |
390 |
|
391 |
|
392 | export interface IOptions {
|
393 | |
394 |
|
395 |
|
396 | sessionContext: ISessionContext;
|
397 |
|
398 | |
399 |
|
400 |
|
401 | translator?: ITranslator;
|
402 | }
|
403 | }
|
404 |
|
405 | function request(n: number): KernelMessage.IHistoryRequestMsg['content'] {
|
406 | return {
|
407 | output: false,
|
408 | raw: true,
|
409 | hist_access_type: 'tail',
|
410 | n: n
|
411 | };
|
412 | }
|