index.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { Kernel, KernelMessage, Session } from '@jupyterlab/services';
  4. import { find } from '@phosphor/algorithm';
  5. import { JSONObject } from '@phosphor/coreutils';
  6. import { Widget } from '@phosphor/widgets';
  7. import { Text } from '@jupyterlab/coreutils';
  8. import {
  9. JupyterFrontEnd,
  10. JupyterFrontEndPlugin
  11. } from '@jupyterlab/application';
  12. import { CodeEditor } from '@jupyterlab/codeeditor';
  13. import { IConsoleTracker } from '@jupyterlab/console';
  14. import { IEditorTracker } from '@jupyterlab/fileeditor';
  15. import { INotebookTracker } from '@jupyterlab/notebook';
  16. import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
  17. import { ITooltipManager, Tooltip } from '@jupyterlab/tooltip';
  18. /**
  19. * The command IDs used by the tooltip plugin.
  20. */
  21. namespace CommandIDs {
  22. export const dismiss = 'tooltip:dismiss';
  23. export const launchConsole = 'tooltip:launch-console';
  24. export const launchNotebook = 'tooltip:launch-notebook';
  25. export const launchFile = 'tooltip:launch-file';
  26. }
  27. /**
  28. * The main tooltip manager plugin.
  29. */
  30. const manager: JupyterFrontEndPlugin<ITooltipManager> = {
  31. id: '@jupyterlab/tooltip-extension:manager',
  32. autoStart: true,
  33. provides: ITooltipManager,
  34. activate: (app: JupyterFrontEnd): ITooltipManager => {
  35. let tooltip: Tooltip | null = null;
  36. // Add tooltip dismiss command.
  37. app.commands.addCommand(CommandIDs.dismiss, {
  38. execute: () => {
  39. if (tooltip) {
  40. tooltip.dispose();
  41. tooltip = null;
  42. }
  43. }
  44. });
  45. return {
  46. invoke(options: ITooltipManager.IOptions): Promise<void> {
  47. const detail: 0 | 1 = 0;
  48. const { anchor, editor, kernel, rendermime } = options;
  49. if (tooltip) {
  50. tooltip.dispose();
  51. tooltip = null;
  52. }
  53. return Private.fetch({ detail, editor, kernel })
  54. .then(bundle => {
  55. tooltip = new Tooltip({ anchor, bundle, editor, rendermime });
  56. Widget.attach(tooltip, document.body);
  57. })
  58. .catch(() => {
  59. /* Fails silently. */
  60. });
  61. }
  62. };
  63. }
  64. };
  65. /**
  66. * The console tooltip plugin.
  67. */
  68. const consoles: JupyterFrontEndPlugin<void> = {
  69. id: '@jupyterlab/tooltip-extension:consoles',
  70. autoStart: true,
  71. requires: [ITooltipManager, IConsoleTracker],
  72. activate: (
  73. app: JupyterFrontEnd,
  74. manager: ITooltipManager,
  75. consoles: IConsoleTracker
  76. ): void => {
  77. // Add tooltip launch command.
  78. app.commands.addCommand(CommandIDs.launchConsole, {
  79. execute: () => {
  80. const parent = consoles.currentWidget;
  81. if (!parent) {
  82. return;
  83. }
  84. const anchor = parent.console;
  85. const editor = anchor.promptCell.editor;
  86. const kernel = anchor.session.kernel;
  87. const rendermime = anchor.rendermime;
  88. // If all components necessary for rendering exist, create a tooltip.
  89. if (!!editor && !!kernel && !!rendermime) {
  90. return manager.invoke({ anchor, editor, kernel, rendermime });
  91. }
  92. }
  93. });
  94. }
  95. };
  96. /**
  97. * The notebook tooltip plugin.
  98. */
  99. const notebooks: JupyterFrontEndPlugin<void> = {
  100. id: '@jupyterlab/tooltip-extension:notebooks',
  101. autoStart: true,
  102. requires: [ITooltipManager, INotebookTracker],
  103. activate: (
  104. app: JupyterFrontEnd,
  105. manager: ITooltipManager,
  106. notebooks: INotebookTracker
  107. ): void => {
  108. // Add tooltip launch command.
  109. app.commands.addCommand(CommandIDs.launchNotebook, {
  110. execute: () => {
  111. const parent = notebooks.currentWidget;
  112. if (!parent) {
  113. return;
  114. }
  115. const anchor = parent.content;
  116. const editor = anchor.activeCell.editor;
  117. const kernel = parent.session.kernel;
  118. const rendermime = parent.rendermime;
  119. // If all components necessary for rendering exist, create a tooltip.
  120. if (!!editor && !!kernel && !!rendermime) {
  121. return manager.invoke({ anchor, editor, kernel, rendermime });
  122. }
  123. }
  124. });
  125. }
  126. };
  127. /**
  128. * The file editor tooltip plugin.
  129. */
  130. const files: JupyterFrontEndPlugin<void> = {
  131. id: '@jupyterlab/tooltip-extension:files',
  132. autoStart: true,
  133. requires: [ITooltipManager, IEditorTracker, IRenderMimeRegistry],
  134. activate: (
  135. app: JupyterFrontEnd,
  136. manager: ITooltipManager,
  137. editorTracker: IEditorTracker,
  138. rendermime: IRenderMimeRegistry
  139. ): void => {
  140. // Keep a list of active ISessions so that we can
  141. // clean them up when they are no longer needed.
  142. const activeSessions: {
  143. [id: string]: Session.ISession;
  144. } = {};
  145. const sessions = app.serviceManager.sessions;
  146. // When the list of running sessions changes,
  147. // check to see if there are any kernels with a
  148. // matching path for the file editors.
  149. const onRunningChanged = (
  150. sender: Session.IManager,
  151. models: Session.IModel[]
  152. ) => {
  153. editorTracker.forEach(file => {
  154. const model = find(models, m => file.context.path === m.path);
  155. if (model) {
  156. const oldSession = activeSessions[file.id];
  157. // If there is a matching path, but it is the same
  158. // session as we previously had, do nothing.
  159. if (oldSession && oldSession.id === model.id) {
  160. return;
  161. }
  162. // Otherwise, dispose of the old session and reset to
  163. // a new CompletionConnector.
  164. if (oldSession) {
  165. delete activeSessions[file.id];
  166. oldSession.dispose();
  167. }
  168. const session = sessions.connectTo(model);
  169. activeSessions[file.id] = session;
  170. } else {
  171. const session = activeSessions[file.id];
  172. if (session) {
  173. session.dispose();
  174. delete activeSessions[file.id];
  175. }
  176. }
  177. });
  178. };
  179. void Session.listRunning().then(models => {
  180. onRunningChanged(sessions, models);
  181. });
  182. sessions.runningChanged.connect(onRunningChanged);
  183. // Clean up after a widget when it is disposed
  184. editorTracker.widgetAdded.connect((sender, widget) => {
  185. widget.disposed.connect(w => {
  186. const session = activeSessions[w.id];
  187. if (session) {
  188. session.dispose();
  189. delete activeSessions[w.id];
  190. }
  191. });
  192. });
  193. // Add tooltip launch command.
  194. app.commands.addCommand(CommandIDs.launchFile, {
  195. execute: async () => {
  196. const parent = editorTracker.currentWidget;
  197. const kernel =
  198. parent &&
  199. activeSessions[parent.id] &&
  200. activeSessions[parent.id].kernel;
  201. if (!kernel) {
  202. return;
  203. }
  204. const anchor = parent.content;
  205. const editor = anchor.editor;
  206. // If all components necessary for rendering exist, create a tooltip.
  207. if (!!editor && !!kernel && !!rendermime) {
  208. return manager.invoke({ anchor, editor, kernel, rendermime });
  209. }
  210. }
  211. });
  212. }
  213. };
  214. /**
  215. * Export the plugins as default.
  216. */
  217. const plugins: JupyterFrontEndPlugin<any>[] = [
  218. manager,
  219. consoles,
  220. notebooks,
  221. files
  222. ];
  223. export default plugins;
  224. /**
  225. * A namespace for private data.
  226. */
  227. namespace Private {
  228. /**
  229. * A counter for outstanding requests.
  230. */
  231. let pending = 0;
  232. export interface IFetchOptions {
  233. /**
  234. * The detail level requested from the API.
  235. *
  236. * #### Notes
  237. * The only acceptable values are 0 and 1. The default value is 0.
  238. * @see http://jupyter-client.readthedocs.io/en/latest/messaging.html#introspection
  239. */
  240. detail?: 0 | 1;
  241. /**
  242. * The referent editor for the tooltip.
  243. */
  244. editor: CodeEditor.IEditor;
  245. /**
  246. * The kernel against which the API request will be made.
  247. */
  248. kernel: Kernel.IKernelConnection;
  249. }
  250. /**
  251. * Fetch a tooltip's content from the API server.
  252. */
  253. export function fetch(options: IFetchOptions): Promise<JSONObject> {
  254. let { detail, editor, kernel } = options;
  255. let code = editor.model.value.text;
  256. let position = editor.getCursorPosition();
  257. let offset = Text.jsIndexToCharIndex(editor.getOffsetAt(position), code);
  258. // Clear hints if the new text value is empty or kernel is unavailable.
  259. if (!code || !kernel) {
  260. return Promise.reject(void 0);
  261. }
  262. let contents: KernelMessage.IInspectRequestMsg['content'] = {
  263. code,
  264. cursor_pos: offset,
  265. detail_level: detail || 0
  266. };
  267. let current = ++pending;
  268. return kernel.requestInspect(contents).then(msg => {
  269. let value = msg.content;
  270. // If a newer request is pending, bail.
  271. if (current !== pending) {
  272. return Promise.reject(void 0) as Promise<JSONObject>;
  273. }
  274. // If request fails or returns negative results, bail.
  275. if (value.status !== 'ok' || !value.found) {
  276. return Promise.reject(void 0) as Promise<JSONObject>;
  277. }
  278. return Promise.resolve(value.data);
  279. });
  280. }
  281. }