widget.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. IKernel, ISession, KernelMessage
  5. } from 'jupyter-js-services';
  6. import {
  7. showDialog
  8. } from '../dialog';
  9. import {
  10. RenderMime, MimeMap
  11. } from '../rendermime';
  12. import {
  13. Message
  14. } from 'phosphor-messaging';
  15. import {
  16. clearSignalData
  17. } from 'phosphor-signaling';
  18. import {
  19. Widget
  20. } from 'phosphor-widget';
  21. import {
  22. PanelLayout, Panel
  23. } from 'phosphor-panel';
  24. import {
  25. CodeCellWidget, CodeCellModel, RawCellModel, RawCellWidget
  26. } from '../notebook/cells';
  27. import {
  28. EdgeLocation, CellEditorWidget, ITextChange, ICompletionRequest
  29. } from '../notebook/cells/editor';
  30. import {
  31. mimetypeForLanguage
  32. } from '../notebook/common/mimetype';
  33. import {
  34. nbformat
  35. } from '../notebook';
  36. import {
  37. ConsoleTooltip
  38. } from './tooltip';
  39. import {
  40. ConsoleHistory, IConsoleHistory
  41. } from './history';
  42. import {
  43. CompletionWidget, CompletionModel, CellCompletionHandler
  44. } from '../notebook/completion';
  45. /**
  46. * The class name added to console widgets.
  47. */
  48. const CONSOLE_CLASS = 'jp-Console';
  49. /**
  50. * The class name added to console panels.
  51. */
  52. const CONSOLE_PANEL = 'jp-Console-panel';
  53. /**
  54. * The class name added to the console banner.
  55. */
  56. const BANNER_CLASS = 'jp-Console-banner';
  57. /**
  58. * A panel which contains a toolbar and a console.
  59. */
  60. export
  61. class ConsolePanel extends Panel {
  62. /**
  63. * Create a new console widget for the panel.
  64. */
  65. static createConsole(session: ISession, rendermime: RenderMime<Widget>): ConsoleWidget {
  66. return new ConsoleWidget(session, rendermime);
  67. }
  68. /**
  69. * Construct a console panel.
  70. */
  71. constructor(session: ISession, rendermime: RenderMime<Widget>) {
  72. super();
  73. this.addClass(CONSOLE_PANEL);
  74. let constructor = this.constructor as typeof ConsolePanel;
  75. this._console = constructor.createConsole(session, rendermime);
  76. this.addChild(this._console);
  77. }
  78. /**
  79. * The console widget used by the panel.
  80. *
  81. * #### Notes
  82. * This is a read-only property.
  83. */
  84. get content(): ConsoleWidget {
  85. return this._console;
  86. }
  87. /**
  88. * Dispose of the resources held by the widget.
  89. */
  90. dispose(): void {
  91. this._console = null;
  92. super.dispose();
  93. }
  94. /**
  95. * Handle the DOM events for the widget.
  96. *
  97. * @param event - The DOM event sent to the widget.
  98. *
  99. * #### Notes
  100. * This method implements the DOM `EventListener` interface and is
  101. * called in response to events on the dock panel's node. It should
  102. * not be called directly by user code.
  103. */
  104. handleEvent(event: Event): void {
  105. switch (event.type) {
  106. case 'click':
  107. let prompt = this.content.prompt;
  108. if (prompt) {
  109. prompt.focus();
  110. }
  111. break;
  112. default:
  113. break;
  114. }
  115. }
  116. /**
  117. * Handle `after_attach` messages for the widget.
  118. */
  119. protected onAfterAttach(msg: Message): void {
  120. this.content.node.addEventListener('click', this);
  121. }
  122. /**
  123. * Handle `before_detach` messages for the widget.
  124. */
  125. protected onBeforeDetach(msg: Message): void {
  126. this.content.node.removeEventListener('click', this);
  127. }
  128. /**
  129. * Handle `'close-request'` messages.
  130. */
  131. protected onCloseRequest(msg: Message): void {
  132. let session = this.content.session;
  133. if (!session.kernel) {
  134. this.dispose();
  135. }
  136. session.kernel.getKernelSpec().then(spec => {
  137. let name = spec.display_name;
  138. return showDialog({
  139. title: 'Shut down kernel?',
  140. body: `Shut down ${name}?`,
  141. host: this.node
  142. });
  143. }).then(value => {
  144. if (value && value.text === 'OK') {
  145. return session.shutdown();
  146. }
  147. }).then(() => {
  148. super.onCloseRequest(msg);
  149. this.dispose();
  150. });
  151. }
  152. private _console: ConsoleWidget = null;
  153. }
  154. /**
  155. * A widget containing a Jupyter console.
  156. */
  157. export
  158. class ConsoleWidget extends Widget {
  159. /**
  160. * Create a new banner widget given a banner model.
  161. */
  162. static createBanner() {
  163. let widget = new RawCellWidget();
  164. widget.model = new RawCellModel();
  165. return widget;
  166. }
  167. /**
  168. * Create a new prompt widget given a prompt model and a rendermime.
  169. */
  170. static createPrompt(rendermime: RenderMime<Widget>): CodeCellWidget {
  171. let widget = new CodeCellWidget({ rendermime });
  172. widget.model = new CodeCellModel();
  173. return widget;
  174. }
  175. /**
  176. * Create a new completion widget.
  177. */
  178. static createCompletion(): CompletionWidget {
  179. let model = new CompletionModel();
  180. return new CompletionWidget({ model });
  181. }
  182. /**
  183. * Create a console history.
  184. */
  185. static createHistory(kernel: IKernel): IConsoleHistory {
  186. return new ConsoleHistory(kernel);
  187. }
  188. /**
  189. * Create a new tooltip widget.
  190. *
  191. * @returns A ConsoleTooltip widget.
  192. */
  193. static createTooltip(): ConsoleTooltip {
  194. return new ConsoleTooltip();
  195. }
  196. /**
  197. * Construct a console widget.
  198. */
  199. constructor(session: ISession, rendermime: RenderMime<Widget>) {
  200. super();
  201. this.addClass(CONSOLE_CLASS);
  202. let constructor = this.constructor as typeof ConsoleWidget;
  203. let layout = new PanelLayout();
  204. this.layout = layout;
  205. this._rendermime = rendermime;
  206. this._session = session;
  207. this._history = constructor.createHistory(session.kernel);
  208. // Instantiate tab completion widget.
  209. this._completion = constructor.createCompletion();
  210. this._completion.anchor = this.node;
  211. this._completion.attach(document.body);
  212. this._completionHandler = new CellCompletionHandler(this._completion);
  213. this._completionHandler.kernel = session.kernel;
  214. // Instantiate tooltip widget.
  215. this._tooltip = constructor.createTooltip();
  216. this._tooltip.attach(document.body);
  217. // Create the banner.
  218. let banner = constructor.createBanner();
  219. banner.addClass(BANNER_CLASS);
  220. banner.readOnly = true;
  221. banner.model.source = '...';
  222. layout.addChild(banner);
  223. // Set the banner text and the mimetype.
  224. this.initialize();
  225. // Create the prompt.
  226. this.newPrompt();
  227. // Handle changes to the kernel.
  228. session.kernelChanged.connect((s, kernel) => {
  229. this.clear();
  230. this.newPrompt();
  231. this.initialize();
  232. this._history = constructor.createHistory(kernel);
  233. this._completionHandler.kernel = kernel;
  234. });
  235. }
  236. /*
  237. * The last cell in a console is always a `CodeCellWidget` prompt.
  238. */
  239. get prompt(): CodeCellWidget {
  240. let layout = this.layout as PanelLayout;
  241. let last = layout.childCount() - 1;
  242. return last > 0 ? layout.childAt(last) as CodeCellWidget : null;
  243. }
  244. /**
  245. * Get the session used by the console.
  246. *
  247. * #### Notes
  248. * This is a read-only property.
  249. */
  250. get session(): ISession {
  251. return this._session;
  252. }
  253. /**
  254. * Dispose of the resources held by the widget.
  255. */
  256. dispose() {
  257. // Do nothing if already disposed.
  258. if (this.isDisposed) {
  259. return;
  260. }
  261. this._tooltip.dispose();
  262. this._tooltip = null;
  263. this._history.dispose();
  264. this._history = null;
  265. this._completionHandler.dispose();
  266. this._completionHandler = null;
  267. this._completion.dispose();
  268. this._completion = null;
  269. this._session.dispose();
  270. this._session = null;
  271. super.dispose();
  272. }
  273. /**
  274. * Execute the current prompt.
  275. */
  276. execute(): Promise<void> {
  277. if (this._session.status === 'dead') {
  278. return;
  279. }
  280. let prompt = this.prompt;
  281. prompt.trusted = true;
  282. this._history.push(prompt.model.source);
  283. return prompt.execute(this._session.kernel).then(
  284. () => this.newPrompt(),
  285. () => this.newPrompt()
  286. );
  287. }
  288. /**
  289. * Clear the code cells.
  290. */
  291. clear(): void {
  292. while (this.prompt) {
  293. this.prompt.dispose();
  294. }
  295. this.newPrompt();
  296. }
  297. /**
  298. * Serialize the output.
  299. */
  300. serialize(): nbformat.ICodeCell[] {
  301. let output: nbformat.ICodeCell[] = [];
  302. let layout = this.layout as PanelLayout;
  303. for (let i = 1; i < layout.childCount(); i++) {
  304. let widget = layout.childAt(i) as CodeCellWidget;
  305. output.push(widget.model.toJSON());
  306. }
  307. return output;
  308. }
  309. /**
  310. * Handle `after_attach` messages for the widget.
  311. */
  312. protected onAfterAttach(msg: Message): void {
  313. let prompt = this.prompt;
  314. if (prompt) {
  315. prompt.focus();
  316. }
  317. }
  318. /**
  319. * Handle `update_request` messages.
  320. */
  321. protected onUpdateRequest(msg: Message): void {
  322. let prompt = this.prompt;
  323. Private.scrollIfNeeded(this.parent.node, prompt.node);
  324. }
  325. /**
  326. * Make a new prompt.
  327. */
  328. protected newPrompt(): void {
  329. // Make the previous editor read-only and clear its signals.
  330. let prompt = this.prompt;
  331. if (prompt) {
  332. prompt.readOnly = true;
  333. clearSignalData(prompt.editor);
  334. }
  335. // Create the new prompt and add to layout.
  336. let layout = this.layout as PanelLayout;
  337. let constructor = this.constructor as typeof ConsoleWidget;
  338. prompt = constructor.createPrompt(this._rendermime);
  339. prompt.mimetype = this._mimetype;
  340. layout.addChild(prompt);
  341. // Hook up completion, tooltip, and history handling.
  342. let editor = prompt.editor;
  343. editor.textChanged.connect(this.onTextChange, this);
  344. editor.edgeRequested.connect(this.onEdgeRequest, this);
  345. // Associate the new prompt with the completion handler.
  346. this._completionHandler.activeCell = prompt;
  347. prompt.focus();
  348. }
  349. /**
  350. * Initialize the banner and mimetype.
  351. */
  352. protected initialize(): void {
  353. let layout = this.layout as PanelLayout;
  354. let banner = layout.childAt(0) as RawCellWidget;
  355. this._session.kernel.kernelInfo().then(msg => {
  356. let info = msg.content;
  357. banner.model.source = info.banner;
  358. this._mimetype = mimetypeForLanguage(info.language_info);
  359. this.prompt.mimetype = this._mimetype;
  360. });
  361. }
  362. /**
  363. * Handle a text changed signal from an editor.
  364. */
  365. protected onTextChange(editor: CellEditorWidget, change: ITextChange): void {
  366. let hasCompletion = !!this._completion.model.original;
  367. if (hasCompletion) {
  368. this._tooltip.hide();
  369. } else if (change.newValue) {
  370. this.updateTooltip(change);
  371. }
  372. }
  373. /**
  374. * Update the tooltip based on a text change.
  375. */
  376. protected updateTooltip(change: ITextChange): void {
  377. let line = change.newValue.split('\n')[change.line];
  378. let contents: KernelMessage.IInspectRequest = {
  379. code: line,
  380. cursor_pos: change.ch,
  381. detail_level: 0
  382. };
  383. let pendingInspect = ++this._pendingInspect;
  384. this._session.kernel.inspect(contents).then(msg => {
  385. let value = msg.content;
  386. // If widget has been disposed, bail.
  387. if (this.isDisposed) {
  388. return;
  389. }
  390. // If a newer text change has created a pending request, bail.
  391. if (pendingInspect !== this._pendingInspect) {
  392. return;
  393. }
  394. // Tooltip request failures or negative results fail silently.
  395. if (value.status !== 'ok' || !value.found) {
  396. return;
  397. }
  398. this.showTooltip(change, value.data as MimeMap<string>);
  399. });
  400. }
  401. /**
  402. * Show the tooltip.
  403. */
  404. protected showTooltip(change: ITextChange, bundle: MimeMap<string>): void {
  405. let { top, bottom, left } = change.coords;
  406. let tooltip = this._tooltip;
  407. let heightAbove = top + 1; // 1px border
  408. let heightBelow = window.innerHeight - bottom - 1; // 1px border
  409. let widthLeft = left;
  410. let widthRight = window.innerWidth - left;
  411. // Add content and measure.
  412. tooltip.content = this._rendermime.render(bundle);
  413. tooltip.show();
  414. let { width, height } = tooltip.node.getBoundingClientRect();
  415. let maxWidth: number;
  416. let maxHeight: number;
  417. // Prefer displaying below.
  418. if (heightBelow >= height || heightBelow >= heightAbove) {
  419. // Offset the height of the tooltip by the height of cursor characters.
  420. top += change.chHeight;
  421. maxHeight = heightBelow;
  422. } else {
  423. maxHeight = heightAbove;
  424. top -= Math.min(height, maxHeight);
  425. }
  426. // Prefer displaying on the right.
  427. if (widthRight >= width || widthRight >= widthLeft) {
  428. // Account for 1px border width.
  429. left += 1;
  430. maxWidth = widthRight;
  431. } else {
  432. maxWidth = widthLeft;
  433. left -= Math.min(width, maxWidth);
  434. }
  435. tooltip.node.style.top = `${top}px`;
  436. tooltip.node.style.left = `${left}px`;
  437. tooltip.node.style.maxHeight = `${maxHeight}px`;
  438. tooltip.node.style.maxWidth = `${maxWidth}px`;
  439. }
  440. /**
  441. * Handle an edge requested signal.
  442. */
  443. protected onEdgeRequest(editor: CellEditorWidget, location: EdgeLocation): void {
  444. let prompt = this.prompt;
  445. if (location === 'top') {
  446. this._history.back().then(value => {
  447. if (!value) {
  448. return;
  449. }
  450. prompt.model.source = value;
  451. prompt.editor.setCursorPosition(0);
  452. });
  453. } else {
  454. this._history.forward().then(value => {
  455. // If at the bottom end of history, then clear the prompt.
  456. let text = value || '';
  457. prompt.model.source = text;
  458. prompt.editor.setCursorPosition(text.length);
  459. });
  460. }
  461. }
  462. private _completion: CompletionWidget = null;
  463. private _completionHandler: CellCompletionHandler = null;
  464. private _mimetype = 'text/x-ipython';
  465. private _rendermime: RenderMime<Widget> = null;
  466. private _tooltip: ConsoleTooltip = null;
  467. private _history: IConsoleHistory = null;
  468. private _session: ISession = null;
  469. private _pendingInspect = 0;
  470. }
  471. /**
  472. * A namespace for Console widget private data.
  473. */
  474. namespace Private {
  475. /**
  476. * Scroll an element into view if needed.
  477. *
  478. * @param area - The scroll area element.
  479. *
  480. * @param elem - The element of interest.
  481. */
  482. export
  483. function scrollIfNeeded(area: HTMLElement, elem: HTMLElement): void {
  484. let ar = area.getBoundingClientRect();
  485. let er = elem.getBoundingClientRect();
  486. if (er.top < ar.top - 10) {
  487. area.scrollTop -= ar.top - er.top + 10;
  488. } else if (er.bottom > ar.bottom + 10) {
  489. area.scrollTop += er.bottom - ar.bottom + 10;
  490. }
  491. }
  492. }