index.ts 17 KB

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