UNPKG

11.4 kBPlain TextView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3
4import { Cell } from '@jupyterlab/cells';
5import { ISessionContext } from '@jupyterlab/apputils';
6import { CodeEditor } from '@jupyterlab/codeeditor';
7import { KernelMessage } from '@jupyterlab/services';
8import {
9 ITranslator,
10 nullTranslator,
11 TranslationBundle
12} from '@jupyterlab/translation';
13import { IDisposable } from '@lumino/disposable';
14import { Signal } from '@lumino/signaling';
15import { IKernelConnection } from '@jupyterlab/services/lib/kernel/kernel';
16
17/**
18 * The definition of a console history manager object.
19 */
20export interface INotebookHistory extends IDisposable {
21 /**
22 * The current editor used by the history widget.
23 */
24 editor: CodeEditor.IEditor | null;
25
26 /**
27 * The placeholder text that a history session began with.
28 */
29 readonly placeholder: string;
30
31 /**
32 * The session number of the current kernel session
33 */
34 readonly kernelSession: string;
35
36 /**
37 * Get the previous item in the console history.
38 *
39 * @param activeCell - The currently selected Cell in the notebook.
40 *
41 * @returns A Promise for console command text or `undefined` if unavailable.
42 */
43 back(activeCell: Cell): Promise<string | undefined>;
44
45 /**
46 * Get the next item in the console history.
47 *
48 * @param activeCell - The currently selected Cell in the notebook.
49 *
50 * @returns A Promise for console command text or `undefined` if unavailable.
51 */
52 forward(activeCell: Cell): Promise<string | undefined>;
53
54 /**
55 * Reset the history navigation state, i.e., start a new history session.
56 */
57 reset(): void;
58
59 /**
60 * Get the next item in the console history.
61 *
62 * @param activeCell - The currently selected Cell in the notebook.
63 * @param content - the result from back or forward
64 */
65 updateEditor(activeCell: Cell, content: string | undefined): void;
66}
67
68/**
69 * A console history manager object.
70 */
71export class NotebookHistory implements INotebookHistory {
72 /**
73 * Construct a new console history object.
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 * The client session used to query history.
86 */
87 private _sessionContext: ISessionContext;
88
89 /**
90 * Translator to be used for warnings
91 */
92 private _trans: TranslationBundle;
93
94 /**
95 * The number of history items to request.
96 */
97 private _toRequest: number;
98
99 /**
100 * The number of history items to increase a batch size by per subsequent request.
101 */
102 private _requestBatchSize: number = 10;
103
104 /**
105 * The current editor used by the history manager.
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 * The placeholder text that a history session began with.
130 */
131 get placeholder(): string {
132 return this._placeholder;
133 }
134
135 /**
136 * Kernel session number for filtering
137 */
138 get kernelSession(): string {
139 return this._kernelSession;
140 }
141
142 /**
143 * Get whether the notebook history manager is disposed.
144 */
145 get isDisposed(): boolean {
146 return this._isDisposed;
147 }
148
149 /**
150 * Dispose of the resources held by the notebook history manager.
151 */
152 dispose(): void {
153 this._isDisposed = true;
154 this._history.length = 0;
155 Signal.clearData(this);
156 }
157
158 /**
159 * Set placeholder and editor. Start session if one is not already started.
160 *
161 * @param activeCell - The currently selected Cell in the notebook.
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 // Filter the history with the placeholder string.
170 this.setFilter(this._placeholder);
171 this._cursor = this._filtered.length - 1;
172 }
173 }
174
175 /**
176 * Get the previous item in the notebook history.
177 *
178 * @param activeCell - The currently selected Cell in the notebook.
179 *
180 * @returns A Promise resolving to the historical cell content text.
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 // This shouldn't ever be undefined as `setFilter` will always be run first
191 return content;
192 }
193
194 /**
195 * Get the next item in the notebook history.
196 *
197 * @param activeCell - The currently selected Cell in the notebook.
198 *
199 * @returns A Promise resolving to the historical cell content text.
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 // This shouldn't ever be undefined as `setFilter` will always be run first
207 return content;
208 }
209
210 /**
211 * Update the editor of the cell with provided text content.
212 *
213 * @param activeCell - The currently selected Cell in the notebook.
214 * @param content - the result from back or forward
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 * Reset the history navigation state, i.e., start a new history session.
239 */
240 reset(): void {
241 this._hasSession = false;
242 this._placeholder = '';
243 this._toRequest = this._requestBatchSize;
244 }
245
246 /**
247 * Fetches a subsequent batch of history. Updates the filtered history and cursor to correct place in history,
248 * accounting for potentially new history items above it.
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 * Populate the history collection on history reply from a kernel.
280 *
281 * @param value The kernel message history reply.
282 *
283 * #### Notes
284 * History entries have the shape:
285 * [session: number, line: number, input: string]
286 * Contiguous duplicates are stripped out of the API response.
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 // set the kernel session for filtering
305 if (!this.kernelSession) {
306 if (current[2] == cell?.model.sharedModel.getSource()) {
307 this._kernelSession = kernelSession;
308 }
309 }
310 }
311 }
312
313 /**
314 * Handle a text change signal from the editor.
315 */
316 protected onTextChange(): void {
317 if (this._setByHistory) {
318 this._setByHistory = false;
319 return;
320 }
321 this.reset();
322 }
323
324 /**
325 * Handle the current kernel changing.
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 * retrieve the history from the kernel
339 *
340 * @param cell - The string to use when filtering the data.
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 * Set the filter data.
355 *
356 * @param filterStr - The string to use when filtering the data.
357 */
358 protected setFilter(filterStr: string = ''): void {
359 // Apply the new filter and remove contiguous duplicates.
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 * A namespace for NotebookHistory statics.
387 */
388export namespace NotebookHistory {
389 /**
390 * The initialization options for a console history object.
391 */
392 export interface IOptions {
393 /**
394 * The client session used by the foreign handler.
395 */
396 sessionContext: ISessionContext;
397
398 /**
399 * The application language translator.
400 */
401 translator?: ITranslator;
402 }
403}
404
405function request(n: number): KernelMessage.IHistoryRequestMsg['content'] {
406 return {
407 output: false,
408 raw: true,
409 hist_access_type: 'tail',
410 n: n
411 };
412}