history.ts 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { KernelMessage } from '@jupyterlab/services';
  4. import { IDisposable } from '@lumino/disposable';
  5. import { Signal } from '@lumino/signaling';
  6. import { ISessionContext } from '@jupyterlab/apputils';
  7. import { CodeEditor } from '@jupyterlab/codeeditor';
  8. /**
  9. * The definition of a console history manager object.
  10. */
  11. export interface IConsoleHistory extends IDisposable {
  12. /**
  13. * The session context used by the foreign handler.
  14. */
  15. readonly sessionContext: ISessionContext;
  16. /**
  17. * The current editor used by the history widget.
  18. */
  19. editor: CodeEditor.IEditor | null;
  20. /**
  21. * The placeholder text that a history session began with.
  22. */
  23. readonly placeholder: string;
  24. /**
  25. * Get the previous item in the console history.
  26. *
  27. * @param placeholder - The placeholder string that gets temporarily added
  28. * to the history only for the duration of one history session. If multiple
  29. * placeholders are sent within a session, only the first one is accepted.
  30. *
  31. * @returns A Promise for console command text or `undefined` if unavailable.
  32. */
  33. back(placeholder: string): Promise<string>;
  34. /**
  35. * Get the next item in the console history.
  36. *
  37. * @param placeholder - The placeholder string that gets temporarily added
  38. * to the history only for the duration of one history session. If multiple
  39. * placeholders are sent within a session, only the first one is accepted.
  40. *
  41. * @returns A Promise for console command text or `undefined` if unavailable.
  42. */
  43. forward(placeholder: string): Promise<string>;
  44. /**
  45. * Add a new item to the bottom of history.
  46. *
  47. * @param item The item being added to the bottom of history.
  48. *
  49. * #### Notes
  50. * If the item being added is undefined or empty, it is ignored. If the item
  51. * being added is the same as the last item in history, it is ignored as well
  52. * so that the console's history will consist of no contiguous repetitions.
  53. */
  54. push(item: string): void;
  55. /**
  56. * Reset the history navigation state, i.e., start a new history session.
  57. */
  58. reset(): void;
  59. }
  60. /**
  61. * A console history manager object.
  62. */
  63. export class ConsoleHistory implements IConsoleHistory {
  64. /**
  65. * Construct a new console history object.
  66. */
  67. constructor(options: ConsoleHistory.IOptions) {
  68. this.sessionContext = options.sessionContext;
  69. void this._handleKernel();
  70. this.sessionContext.kernelChanged.connect(this._handleKernel, this);
  71. }
  72. /**
  73. * The client session used by the foreign handler.
  74. */
  75. readonly sessionContext: ISessionContext;
  76. /**
  77. * The current editor used by the history manager.
  78. */
  79. get editor(): CodeEditor.IEditor | null {
  80. return this._editor;
  81. }
  82. set editor(value: CodeEditor.IEditor | null) {
  83. if (this._editor === value) {
  84. return;
  85. }
  86. const prev = this._editor;
  87. if (prev) {
  88. prev.edgeRequested.disconnect(this.onEdgeRequest, this);
  89. prev.model.value.changed.disconnect(this.onTextChange, this);
  90. }
  91. this._editor = value;
  92. if (value) {
  93. value.edgeRequested.connect(this.onEdgeRequest, this);
  94. value.model.value.changed.connect(this.onTextChange, this);
  95. }
  96. }
  97. /**
  98. * The placeholder text that a history session began with.
  99. */
  100. get placeholder(): string {
  101. return this._placeholder;
  102. }
  103. /**
  104. * Get whether the console history manager is disposed.
  105. */
  106. get isDisposed(): boolean {
  107. return this._isDisposed;
  108. }
  109. /**
  110. * Dispose of the resources held by the console history manager.
  111. */
  112. dispose(): void {
  113. this._isDisposed = true;
  114. this._history.length = 0;
  115. Signal.clearData(this);
  116. }
  117. /**
  118. * Get the previous item in the console history.
  119. *
  120. * @param placeholder - The placeholder string that gets temporarily added
  121. * to the history only for the duration of one history session. If multiple
  122. * placeholders are sent within a session, only the first one is accepted.
  123. *
  124. * @returns A Promise for console command text or `undefined` if unavailable.
  125. */
  126. back(placeholder: string): Promise<string> {
  127. if (!this._hasSession) {
  128. this._hasSession = true;
  129. this._placeholder = placeholder;
  130. // Filter the history with the placeholder string.
  131. this.setFilter(placeholder);
  132. this._cursor = this._filtered.length - 1;
  133. }
  134. --this._cursor;
  135. this._cursor = Math.max(0, this._cursor);
  136. const content = this._filtered[this._cursor];
  137. return Promise.resolve(content);
  138. }
  139. /**
  140. * Get the next item in the console history.
  141. *
  142. * @param placeholder - The placeholder string that gets temporarily added
  143. * to the history only for the duration of one history session. If multiple
  144. * placeholders are sent within a session, only the first one is accepted.
  145. *
  146. * @returns A Promise for console command text or `undefined` if unavailable.
  147. */
  148. forward(placeholder: string): Promise<string> {
  149. if (!this._hasSession) {
  150. this._hasSession = true;
  151. this._placeholder = placeholder;
  152. // Filter the history with the placeholder string.
  153. this.setFilter(placeholder);
  154. this._cursor = this._filtered.length;
  155. }
  156. ++this._cursor;
  157. this._cursor = Math.min(this._filtered.length - 1, this._cursor);
  158. const content = this._filtered[this._cursor];
  159. return Promise.resolve(content);
  160. }
  161. /**
  162. * Add a new item to the bottom of history.
  163. *
  164. * @param item The item being added to the bottom of history.
  165. *
  166. * #### Notes
  167. * If the item being added is undefined or empty, it is ignored. If the item
  168. * being added is the same as the last item in history, it is ignored as well
  169. * so that the console's history will consist of no contiguous repetitions.
  170. */
  171. push(item: string): void {
  172. if (item && item !== this._history[this._history.length - 1]) {
  173. this._history.push(item);
  174. }
  175. this.reset();
  176. }
  177. /**
  178. * Reset the history navigation state, i.e., start a new history session.
  179. */
  180. reset(): void {
  181. this._cursor = this._history.length;
  182. this._hasSession = false;
  183. this._placeholder = '';
  184. }
  185. /**
  186. * Populate the history collection on history reply from a kernel.
  187. *
  188. * @param value The kernel message history reply.
  189. *
  190. * #### Notes
  191. * History entries have the shape:
  192. * [session: number, line: number, input: string]
  193. * Contiguous duplicates are stripped out of the API response.
  194. */
  195. protected onHistory(value: KernelMessage.IHistoryReplyMsg): void {
  196. this._history.length = 0;
  197. let last = '';
  198. let current = '';
  199. if (value.content.status === 'ok') {
  200. for (let i = 0; i < value.content.history.length; i++) {
  201. current = (value.content.history[i] as string[])[2];
  202. if (current !== last) {
  203. this._history.push((last = current));
  204. }
  205. }
  206. }
  207. // Reset the history navigation cursor back to the bottom.
  208. this._cursor = this._history.length;
  209. }
  210. /**
  211. * Handle a text change signal from the editor.
  212. */
  213. protected onTextChange(): void {
  214. if (this._setByHistory) {
  215. this._setByHistory = false;
  216. return;
  217. }
  218. this.reset();
  219. }
  220. /**
  221. * Handle an edge requested signal.
  222. */
  223. protected onEdgeRequest(
  224. editor: CodeEditor.IEditor,
  225. location: CodeEditor.EdgeLocation
  226. ): void {
  227. const model = editor.model;
  228. const source = model.value.text;
  229. if (location === 'top' || location === 'topLine') {
  230. void this.back(source).then(value => {
  231. if (this.isDisposed || !value) {
  232. return;
  233. }
  234. if (model.value.text === value) {
  235. return;
  236. }
  237. this._setByHistory = true;
  238. model.value.text = value;
  239. let columnPos = 0;
  240. columnPos = value.indexOf('\n');
  241. if (columnPos < 0) {
  242. columnPos = value.length;
  243. }
  244. editor.setCursorPosition({ line: 0, column: columnPos });
  245. });
  246. } else {
  247. void this.forward(source).then(value => {
  248. if (this.isDisposed) {
  249. return;
  250. }
  251. const text = value || this.placeholder;
  252. if (model.value.text === text) {
  253. return;
  254. }
  255. this._setByHistory = true;
  256. model.value.text = text;
  257. const pos = editor.getPositionAt(text.length);
  258. if (pos) {
  259. editor.setCursorPosition(pos);
  260. }
  261. });
  262. }
  263. }
  264. /**
  265. * Handle the current kernel changing.
  266. */
  267. private async _handleKernel(): Promise<void> {
  268. const kernel = this.sessionContext.session?.kernel;
  269. if (!kernel) {
  270. this._history.length = 0;
  271. return;
  272. }
  273. return kernel.requestHistory(Private.initialRequest).then(v => {
  274. this.onHistory(v);
  275. });
  276. }
  277. /**
  278. * Set the filter data.
  279. *
  280. * @param filterStr - The string to use when filtering the data.
  281. */
  282. protected setFilter(filterStr: string = ''): void {
  283. // Apply the new filter and remove contiguous duplicates.
  284. this._filtered.length = 0;
  285. let last = '';
  286. let current = '';
  287. for (let i = 0; i < this._history.length; i++) {
  288. current = this._history[i];
  289. if (
  290. current !== last &&
  291. filterStr === current.slice(0, filterStr.length)
  292. ) {
  293. this._filtered.push((last = current));
  294. }
  295. }
  296. this._filtered.push(filterStr);
  297. }
  298. private _cursor = 0;
  299. private _hasSession = false;
  300. private _history: string[] = [];
  301. private _placeholder: string = '';
  302. private _setByHistory = false;
  303. private _isDisposed = false;
  304. private _editor: CodeEditor.IEditor | null = null;
  305. private _filtered: string[] = [];
  306. }
  307. /**
  308. * A namespace for ConsoleHistory statics.
  309. */
  310. export namespace ConsoleHistory {
  311. /**
  312. * The initialization options for a console history object.
  313. */
  314. export interface IOptions {
  315. /**
  316. * The client session used by the foreign handler.
  317. */
  318. sessionContext: ISessionContext;
  319. }
  320. }
  321. /**
  322. * A namespace for private data.
  323. */
  324. namespace Private {
  325. export const initialRequest: KernelMessage.IHistoryRequestMsg['content'] = {
  326. output: false,
  327. raw: true,
  328. hist_access_type: 'tail',
  329. n: 500
  330. };
  331. }