index.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. /**
  4. * @packageDocumentation
  5. * @module debugger-extension
  6. */
  7. import {
  8. ILabShell,
  9. ILayoutRestorer,
  10. JupyterFrontEnd,
  11. JupyterFrontEndPlugin
  12. } from '@jupyterlab/application';
  13. import {
  14. ICommandPalette,
  15. IThemeManager,
  16. MainAreaWidget,
  17. WidgetTracker
  18. } from '@jupyterlab/apputils';
  19. import { IEditorServices } from '@jupyterlab/codeeditor';
  20. import { ConsolePanel, IConsoleTracker } from '@jupyterlab/console';
  21. import { PageConfig, PathExt } from '@jupyterlab/coreutils';
  22. import {
  23. Debugger,
  24. IDebugger,
  25. IDebuggerConfig,
  26. IDebuggerSources,
  27. IDebuggerSidebar
  28. } from '@jupyterlab/debugger';
  29. import { DocumentWidget } from '@jupyterlab/docregistry';
  30. import { FileEditor, IEditorTracker } from '@jupyterlab/fileeditor';
  31. import { ILoggerRegistry } from '@jupyterlab/logconsole';
  32. import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook';
  33. import {
  34. RenderMimeRegistry,
  35. standardRendererFactories as initialFactories
  36. } from '@jupyterlab/rendermime';
  37. import { Session } from '@jupyterlab/services';
  38. import { ISettingRegistry } from '@jupyterlab/settingregistry';
  39. import { ITranslator } from '@jupyterlab/translation';
  40. /**
  41. * A plugin that provides visual debugging support for consoles.
  42. */
  43. const consoles: JupyterFrontEndPlugin<void> = {
  44. id: '@jupyterlab/debugger-extension:consoles',
  45. autoStart: true,
  46. requires: [IDebugger, IConsoleTracker],
  47. optional: [ILabShell],
  48. activate: (
  49. app: JupyterFrontEnd,
  50. debug: IDebugger,
  51. consoleTracker: IConsoleTracker,
  52. labShell: ILabShell | null
  53. ) => {
  54. const handler = new Debugger.Handler({
  55. type: 'console',
  56. shell: app.shell,
  57. service: debug
  58. });
  59. const updateHandlerAndCommands = async (
  60. widget: ConsolePanel
  61. ): Promise<void> => {
  62. const { sessionContext } = widget;
  63. await sessionContext.ready;
  64. await handler.updateContext(widget, sessionContext);
  65. app.commands.notifyCommandChanged();
  66. };
  67. if (labShell) {
  68. labShell.currentChanged.connect(async (_, update) => {
  69. const widget = update.newValue;
  70. if (!(widget instanceof ConsolePanel)) {
  71. return;
  72. }
  73. await updateHandlerAndCommands(widget);
  74. });
  75. return;
  76. }
  77. consoleTracker.currentChanged.connect(async (_, consolePanel) => {
  78. if (consolePanel) {
  79. void updateHandlerAndCommands(consolePanel);
  80. }
  81. });
  82. }
  83. };
  84. /**
  85. * A plugin that provides visual debugging support for file editors.
  86. */
  87. const files: JupyterFrontEndPlugin<void> = {
  88. id: '@jupyterlab/debugger-extension:files',
  89. autoStart: true,
  90. requires: [IDebugger, IEditorTracker],
  91. optional: [ILabShell],
  92. activate: (
  93. app: JupyterFrontEnd,
  94. debug: IDebugger,
  95. editorTracker: IEditorTracker,
  96. labShell: ILabShell | null
  97. ) => {
  98. const handler = new Debugger.Handler({
  99. type: 'file',
  100. shell: app.shell,
  101. service: debug
  102. });
  103. const activeSessions: {
  104. [id: string]: Session.ISessionConnection;
  105. } = {};
  106. const updateHandlerAndCommands = async (
  107. widget: DocumentWidget
  108. ): Promise<void> => {
  109. const sessions = app.serviceManager.sessions;
  110. try {
  111. const model = await sessions.findByPath(widget.context.path);
  112. if (!model) {
  113. return;
  114. }
  115. let session = activeSessions[model.id];
  116. if (!session) {
  117. // Use `connectTo` only if the session does not exist.
  118. // `connectTo` sends a kernel_info_request on the shell
  119. // channel, which blocks the debug session restore when waiting
  120. // for the kernel to be ready
  121. session = sessions.connectTo({ model });
  122. activeSessions[model.id] = session;
  123. }
  124. await handler.update(widget, session);
  125. app.commands.notifyCommandChanged();
  126. } catch {
  127. return;
  128. }
  129. };
  130. if (labShell) {
  131. labShell.currentChanged.connect(async (_, update) => {
  132. const widget = update.newValue;
  133. if (!(widget instanceof DocumentWidget)) {
  134. return;
  135. }
  136. const content = widget.content;
  137. if (!(content instanceof FileEditor)) {
  138. return;
  139. }
  140. await updateHandlerAndCommands(widget);
  141. });
  142. }
  143. editorTracker.currentChanged.connect(async (_, documentWidget) => {
  144. await updateHandlerAndCommands(
  145. (documentWidget as unknown) as DocumentWidget
  146. );
  147. });
  148. }
  149. };
  150. /**
  151. * A plugin that provides visual debugging support for notebooks.
  152. */
  153. const notebooks: JupyterFrontEndPlugin<void> = {
  154. id: '@jupyterlab/debugger-extension:notebooks',
  155. autoStart: true,
  156. requires: [IDebugger, INotebookTracker],
  157. optional: [ILabShell],
  158. activate: (
  159. app: JupyterFrontEnd,
  160. service: IDebugger,
  161. notebookTracker: INotebookTracker,
  162. labShell: ILabShell | null
  163. ) => {
  164. const handler = new Debugger.Handler({
  165. type: 'notebook',
  166. shell: app.shell,
  167. service
  168. });
  169. const updateHandlerAndCommands = async (
  170. widget: NotebookPanel
  171. ): Promise<void> => {
  172. const { sessionContext } = widget;
  173. await sessionContext.ready;
  174. await handler.updateContext(widget, sessionContext);
  175. app.commands.notifyCommandChanged();
  176. };
  177. if (labShell) {
  178. labShell.currentChanged.connect(async (_, update) => {
  179. const widget = update.newValue;
  180. if (!(widget instanceof NotebookPanel)) {
  181. return;
  182. }
  183. await updateHandlerAndCommands(widget);
  184. });
  185. return;
  186. }
  187. notebookTracker.currentChanged.connect(
  188. async (_, notebookPanel: NotebookPanel) => {
  189. await updateHandlerAndCommands(notebookPanel);
  190. }
  191. );
  192. }
  193. };
  194. /**
  195. * A plugin that provides a debugger service.
  196. */
  197. const service: JupyterFrontEndPlugin<IDebugger> = {
  198. id: '@jupyterlab/debugger-extension:service',
  199. autoStart: true,
  200. provides: IDebugger,
  201. requires: [IDebuggerConfig],
  202. optional: [IDebuggerSources],
  203. activate: (
  204. app: JupyterFrontEnd,
  205. config: IDebugger.IConfig,
  206. debuggerSources: IDebugger.ISources | null
  207. ) =>
  208. new Debugger.Service({
  209. config,
  210. debuggerSources,
  211. specsManager: app.serviceManager.kernelspecs
  212. })
  213. };
  214. /**
  215. * A plugin that provides a configuration with hash method.
  216. */
  217. const configuration: JupyterFrontEndPlugin<IDebugger.IConfig> = {
  218. id: '@jupyterlab/debugger-extension:config',
  219. provides: IDebuggerConfig,
  220. autoStart: true,
  221. activate: () => new Debugger.Config()
  222. };
  223. /**
  224. * A plugin that provides source/editor functionality for debugging.
  225. */
  226. const sources: JupyterFrontEndPlugin<IDebugger.ISources> = {
  227. id: '@jupyterlab/debugger-extension:sources',
  228. autoStart: true,
  229. provides: IDebuggerSources,
  230. requires: [IDebuggerConfig, IEditorServices],
  231. optional: [INotebookTracker, IConsoleTracker, IEditorTracker],
  232. activate: (
  233. app: JupyterFrontEnd,
  234. config: IDebugger.IConfig,
  235. editorServices: IEditorServices,
  236. notebookTracker: INotebookTracker | null,
  237. consoleTracker: IConsoleTracker | null,
  238. editorTracker: IEditorTracker | null
  239. ): IDebugger.ISources => {
  240. return new Debugger.Sources({
  241. config,
  242. shell: app.shell,
  243. editorServices,
  244. notebookTracker,
  245. consoleTracker,
  246. editorTracker
  247. });
  248. }
  249. };
  250. /*
  251. * A plugin to open detailed views for variables.
  252. */
  253. const variables: JupyterFrontEndPlugin<void> = {
  254. id: '@jupyterlab/debugger-extension:variables',
  255. autoStart: true,
  256. requires: [IDebugger, ITranslator],
  257. optional: [IThemeManager],
  258. activate: (
  259. app: JupyterFrontEnd,
  260. service: IDebugger,
  261. translator: ITranslator,
  262. themeManager: IThemeManager | null
  263. ) => {
  264. const trans = translator.load('jupyterlab');
  265. const { commands, shell } = app;
  266. const tracker = new WidgetTracker<MainAreaWidget<Debugger.VariablesGrid>>({
  267. namespace: 'debugger/inspect-variable'
  268. });
  269. const CommandIDs = Debugger.CommandIDs;
  270. commands.addCommand(CommandIDs.inspectVariable, {
  271. label: trans.__('Inspect Variable'),
  272. caption: trans.__('Inspect Variable'),
  273. execute: async args => {
  274. const { variableReference } = args;
  275. if (!variableReference || variableReference === 0) {
  276. return;
  277. }
  278. const variables = await service.inspectVariable(
  279. variableReference as number
  280. );
  281. const title = args.title as string;
  282. const id = `jp-debugger-variable-${title}`;
  283. if (
  284. !variables ||
  285. variables.length === 0 ||
  286. tracker.find(widget => widget.id === id)
  287. ) {
  288. return;
  289. }
  290. const model = service.model.variables;
  291. const widget = new MainAreaWidget<Debugger.VariablesGrid>({
  292. content: new Debugger.VariablesGrid({
  293. model,
  294. commands,
  295. scopes: [{ name: title, variables }],
  296. themeManager
  297. })
  298. });
  299. widget.addClass('jp-DebuggerVariables');
  300. widget.id = id;
  301. widget.title.icon = Debugger.Icons.variableIcon;
  302. widget.title.label = `${service.session?.connection?.name} - ${title}`;
  303. void tracker.add(widget);
  304. model.changed.connect(() => widget.dispose());
  305. shell.add(widget, 'main', {
  306. mode: tracker.currentWidget ? 'split-right' : 'split-bottom'
  307. });
  308. }
  309. });
  310. }
  311. };
  312. /**
  313. * Debugger sidebar provider plugin.
  314. */
  315. const sidebar: JupyterFrontEndPlugin<IDebugger.ISidebar> = {
  316. id: '@jupyterlab/debugger-extension:sidebar',
  317. provides: IDebuggerSidebar,
  318. requires: [IDebugger, IEditorServices, ITranslator],
  319. optional: [IThemeManager, ISettingRegistry],
  320. autoStart: true,
  321. activate: async (
  322. app: JupyterFrontEnd,
  323. service: IDebugger,
  324. editorServices: IEditorServices,
  325. translator: ITranslator,
  326. themeManager: IThemeManager | null,
  327. settingRegistry: ISettingRegistry | null
  328. ): Promise<IDebugger.ISidebar> => {
  329. const { commands } = app;
  330. const CommandIDs = Debugger.CommandIDs;
  331. const callstackCommands = {
  332. registry: commands,
  333. continue: CommandIDs.debugContinue,
  334. terminate: CommandIDs.terminate,
  335. next: CommandIDs.next,
  336. stepIn: CommandIDs.stepIn,
  337. stepOut: CommandIDs.stepOut,
  338. evaluate: CommandIDs.evaluate
  339. };
  340. const sidebar = new Debugger.Sidebar({
  341. service,
  342. callstackCommands,
  343. editorServices,
  344. themeManager,
  345. translator
  346. });
  347. if (settingRegistry) {
  348. const setting = await settingRegistry.load(main.id);
  349. const updateSettings = (): void => {
  350. const filters = setting.get('variableFilters').composite as {
  351. [key: string]: string[];
  352. };
  353. const kernel = service.session?.connection?.kernel?.name ?? '';
  354. if (kernel && filters[kernel]) {
  355. sidebar.variables.filter = new Set<string>(filters[kernel]);
  356. }
  357. };
  358. updateSettings();
  359. setting.changed.connect(updateSettings);
  360. service.sessionChanged.connect(updateSettings);
  361. }
  362. return sidebar;
  363. }
  364. };
  365. /**
  366. * The main debugger UI plugin.
  367. */
  368. const main: JupyterFrontEndPlugin<void> = {
  369. id: '@jupyterlab/debugger-extension:main',
  370. requires: [IDebugger, IDebuggerSidebar, IEditorServices, ITranslator],
  371. optional: [
  372. ICommandPalette,
  373. IDebuggerSources,
  374. ILabShell,
  375. ILayoutRestorer,
  376. ILoggerRegistry
  377. ],
  378. autoStart: true,
  379. activate: async (
  380. app: JupyterFrontEnd,
  381. service: IDebugger,
  382. sidebar: IDebugger.ISidebar,
  383. editorServices: IEditorServices,
  384. translator: ITranslator,
  385. palette: ICommandPalette | null,
  386. debuggerSources: IDebugger.ISources | null,
  387. labShell: ILabShell | null,
  388. restorer: ILayoutRestorer | null,
  389. loggerRegistry: ILoggerRegistry | null
  390. ): Promise<void> => {
  391. const trans = translator.load('jupyterlab');
  392. const { commands, shell, serviceManager } = app;
  393. const { kernelspecs } = serviceManager;
  394. const CommandIDs = Debugger.CommandIDs;
  395. // First check if there is a PageConfig override for the extension visibility
  396. const alwaysShowDebuggerExtension =
  397. PageConfig.getOption('alwaysShowDebuggerExtension').toLowerCase() ===
  398. 'true';
  399. if (!alwaysShowDebuggerExtension) {
  400. // hide the debugger sidebar if no kernel with support for debugging is available
  401. await kernelspecs.ready;
  402. const specs = kernelspecs.specs?.kernelspecs;
  403. if (!specs) {
  404. return;
  405. }
  406. const enabled = Object.keys(specs).some(
  407. name => !!(specs[name]?.metadata?.['debugger'] ?? false)
  408. );
  409. if (!enabled) {
  410. return;
  411. }
  412. }
  413. // get the mime type of the kernel language for the current debug session
  414. const getMimeType = async (): Promise<string> => {
  415. const kernel = service.session?.connection?.kernel;
  416. if (!kernel) {
  417. return '';
  418. }
  419. const info = (await kernel.info).language_info;
  420. const name = info.name;
  421. const mimeType =
  422. editorServices?.mimeTypeService.getMimeTypeByLanguage({ name }) ?? '';
  423. return mimeType;
  424. };
  425. const rendermime = new RenderMimeRegistry({ initialFactories });
  426. commands.addCommand(CommandIDs.evaluate, {
  427. label: trans.__('Evaluate Code'),
  428. caption: trans.__('Evaluate Code'),
  429. icon: Debugger.Icons.evaluateIcon,
  430. isEnabled: () => {
  431. return service.hasStoppedThreads();
  432. },
  433. execute: async () => {
  434. const mimeType = await getMimeType();
  435. const result = await Debugger.Dialogs.getCode({
  436. title: trans.__('Evaluate Code'),
  437. okLabel: trans.__('Evaluate'),
  438. cancelLabel: trans.__('Cancel'),
  439. mimeType,
  440. rendermime
  441. });
  442. const code = result.value;
  443. if (!result.button.accept || !code) {
  444. return;
  445. }
  446. const reply = await service.evaluate(code);
  447. if (reply) {
  448. const data = reply.result;
  449. const path = service?.session?.connection?.path;
  450. const logger = path ? loggerRegistry?.getLogger?.(path) : undefined;
  451. if (logger) {
  452. // print to log console of the notebook currently being debugged
  453. logger.log({ type: 'text', data, level: logger.level });
  454. } else {
  455. // fallback to printing to devtools console
  456. console.debug(data);
  457. }
  458. }
  459. }
  460. });
  461. commands.addCommand(CommandIDs.debugContinue, {
  462. label: trans.__('Continue'),
  463. caption: trans.__('Continue'),
  464. icon: Debugger.Icons.continueIcon,
  465. isEnabled: () => {
  466. return service.hasStoppedThreads();
  467. },
  468. execute: async () => {
  469. await service.continue();
  470. commands.notifyCommandChanged();
  471. }
  472. });
  473. commands.addCommand(CommandIDs.terminate, {
  474. label: trans.__('Terminate'),
  475. caption: trans.__('Terminate'),
  476. icon: Debugger.Icons.terminateIcon,
  477. isEnabled: () => {
  478. return service.hasStoppedThreads();
  479. },
  480. execute: async () => {
  481. await service.restart();
  482. commands.notifyCommandChanged();
  483. }
  484. });
  485. commands.addCommand(CommandIDs.next, {
  486. label: trans.__('Next'),
  487. caption: trans.__('Next'),
  488. icon: Debugger.Icons.stepOverIcon,
  489. isEnabled: () => {
  490. return service.hasStoppedThreads();
  491. },
  492. execute: async () => {
  493. await service.next();
  494. }
  495. });
  496. commands.addCommand(CommandIDs.stepIn, {
  497. label: trans.__('Step In'),
  498. caption: trans.__('Step In'),
  499. icon: Debugger.Icons.stepIntoIcon,
  500. isEnabled: () => {
  501. return service.hasStoppedThreads();
  502. },
  503. execute: async () => {
  504. await service.stepIn();
  505. }
  506. });
  507. commands.addCommand(CommandIDs.stepOut, {
  508. label: trans.__('Step Out'),
  509. caption: trans.__('Step Out'),
  510. icon: Debugger.Icons.stepOutIcon,
  511. isEnabled: () => {
  512. return service.hasStoppedThreads();
  513. },
  514. execute: async () => {
  515. await service.stepOut();
  516. }
  517. });
  518. service.eventMessage.connect((_, event): void => {
  519. commands.notifyCommandChanged();
  520. if (labShell && event.event === 'initialized') {
  521. labShell.activateById(sidebar.id);
  522. }
  523. });
  524. service.sessionChanged.connect(_ => {
  525. commands.notifyCommandChanged();
  526. });
  527. if (restorer) {
  528. restorer.add(sidebar, 'debugger-sidebar');
  529. }
  530. sidebar.node.setAttribute('role', 'region');
  531. sidebar.node.setAttribute('aria-label', trans.__('Debugger section'));
  532. shell.add(sidebar, 'right');
  533. if (palette) {
  534. const category = trans.__('Debugger');
  535. [
  536. CommandIDs.debugContinue,
  537. CommandIDs.terminate,
  538. CommandIDs.next,
  539. CommandIDs.stepIn,
  540. CommandIDs.stepOut,
  541. CommandIDs.evaluate
  542. ].forEach(command => {
  543. palette.addItem({ command, category });
  544. });
  545. }
  546. if (debuggerSources) {
  547. const { model } = service;
  548. const readOnlyEditorFactory = new Debugger.ReadOnlyEditorFactory({
  549. editorServices
  550. });
  551. const onCurrentFrameChanged = (
  552. _: IDebugger.Model.ICallstack,
  553. frame: IDebugger.IStackFrame
  554. ): void => {
  555. debuggerSources
  556. .find({
  557. focus: true,
  558. kernel: service.session?.connection?.kernel?.name ?? '',
  559. path: service.session?.connection?.path ?? '',
  560. source: frame?.source?.path ?? ''
  561. })
  562. .forEach(editor => {
  563. requestAnimationFrame(() => {
  564. Debugger.EditorHandler.showCurrentLine(editor, frame.line);
  565. });
  566. });
  567. };
  568. const onCurrentSourceOpened = (
  569. _: IDebugger.Model.ISources | null,
  570. source: IDebugger.Source
  571. ): void => {
  572. if (!source) {
  573. return;
  574. }
  575. const { content, mimeType, path } = source;
  576. const results = debuggerSources.find({
  577. focus: true,
  578. kernel: service.session?.connection?.kernel?.name ?? '',
  579. path: service.session?.connection?.path ?? '',
  580. source: path
  581. });
  582. if (results.length > 0) {
  583. return;
  584. }
  585. const editorWrapper = readOnlyEditorFactory.createNewEditor({
  586. content,
  587. mimeType,
  588. path
  589. });
  590. const editor = editorWrapper.editor;
  591. const editorHandler = new Debugger.EditorHandler({
  592. debuggerService: service,
  593. editor,
  594. path
  595. });
  596. editorWrapper.disposed.connect(() => editorHandler.dispose());
  597. debuggerSources.open({
  598. label: PathExt.basename(path),
  599. caption: path,
  600. editorWrapper
  601. });
  602. const frame = service.model.callstack.frame;
  603. if (frame) {
  604. Debugger.EditorHandler.showCurrentLine(editor, frame.line);
  605. }
  606. };
  607. model.callstack.currentFrameChanged.connect(onCurrentFrameChanged);
  608. model.sources.currentSourceOpened.connect(onCurrentSourceOpened);
  609. model.breakpoints.clicked.connect(async (_, breakpoint) => {
  610. const path = breakpoint.source?.path;
  611. const source = await service.getSource({
  612. sourceReference: 0,
  613. path
  614. });
  615. onCurrentSourceOpened(null, source);
  616. });
  617. }
  618. }
  619. };
  620. /**
  621. * Export the plugins as default.
  622. */
  623. const plugins: JupyterFrontEndPlugin<any>[] = [
  624. service,
  625. consoles,
  626. files,
  627. notebooks,
  628. variables,
  629. sidebar,
  630. main,
  631. sources,
  632. configuration
  633. ];
  634. export default plugins;