cell.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { Cell, CodeCell, ICellModel } from '@jupyterlab/cells';
  4. import { CodeMirrorEditor } from '@jupyterlab/codemirror';
  5. import { ActivityMonitor } from '@jupyterlab/coreutils';
  6. import { IDisposable } from '@phosphor/disposable';
  7. import { Signal } from '@phosphor/signaling';
  8. import { Editor } from 'codemirror';
  9. import { Breakpoints, SessionTypes } from '../breakpoints';
  10. import { Callstack } from '../callstack';
  11. import { Debugger } from '../debugger';
  12. import { IDebugger } from '../tokens';
  13. const LINE_HIGHLIGHT_CLASS = 'jp-breakpoint-line-highlight';
  14. const CELL_CHANGED_TIMEOUT = 1000;
  15. export class CellManager implements IDisposable {
  16. constructor(options: CellManager.IOptions) {
  17. this._debuggerService = options.debuggerService;
  18. this.onModelChanged();
  19. this._debuggerService.modelChanged.connect(() => this.onModelChanged());
  20. this.activeCell = options.activeCell;
  21. this.onActiveCellChanged();
  22. }
  23. isDisposed: boolean;
  24. private onModelChanged() {
  25. this._debuggerModel = this._debuggerService.model;
  26. if (!this._debuggerModel) {
  27. return;
  28. }
  29. this.breakpointsModel = this._debuggerModel.breakpointsModel;
  30. this._debuggerModel.callstackModel.currentFrameChanged.connect(
  31. (_, frame) => {
  32. CellManager.cleanupHighlight(this.activeCell);
  33. if (!frame) {
  34. return;
  35. }
  36. }
  37. );
  38. this.breakpointsModel.changed.connect(async () => {
  39. if (
  40. !this.activeCell ||
  41. !this.activeCell.isVisible ||
  42. this.activeCell.isDisposed
  43. ) {
  44. return;
  45. }
  46. this.addBreakpointsToEditor(this.activeCell);
  47. });
  48. this.breakpointsModel.restored.connect(async () => {
  49. if (!this.activeCell || this.activeCell.isDisposed) {
  50. return;
  51. }
  52. this.addBreakpointsToEditor(this.activeCell);
  53. });
  54. if (this.activeCell) {
  55. this._debuggerModel.codeValue = this.activeCell.model.value;
  56. }
  57. }
  58. dispose(): void {
  59. if (this.isDisposed) {
  60. return;
  61. }
  62. if (this.previousCell) {
  63. this.removeListener(this.previousCell);
  64. }
  65. if (this._cellMonitor) {
  66. this._cellMonitor.dispose();
  67. }
  68. this.removeListener(this.activeCell);
  69. CellManager.cleanupHighlight(this.activeCell);
  70. Signal.clearData(this);
  71. }
  72. set previousCell(cell: CodeCell) {
  73. this._previousCell = cell;
  74. }
  75. get previousCell() {
  76. return this._previousCell;
  77. }
  78. set activeCell(cell: CodeCell) {
  79. if (cell) {
  80. this._activeCell = cell;
  81. this._debuggerModel.codeValue = cell.model.value;
  82. this.onActiveCellChanged();
  83. }
  84. }
  85. get activeCell(): CodeCell {
  86. return this._activeCell;
  87. }
  88. protected clearGutter(cell: CodeCell) {
  89. const editor = cell.editor as CodeMirrorEditor;
  90. editor.doc.eachLine(line => {
  91. if ((line as ILineInfo).gutterMarkers) {
  92. editor.editor.setGutterMarker(line, 'breakpoints', null);
  93. }
  94. });
  95. }
  96. onActiveCellChanged() {
  97. if (
  98. this.activeCell &&
  99. this.activeCell.isAttached &&
  100. this.activeCell.editor &&
  101. this._debuggerService &&
  102. this._debuggerService.session
  103. ) {
  104. if (this.previousCell && !this.previousCell.isDisposed) {
  105. if (this._cellMonitor) {
  106. this._cellMonitor.dispose();
  107. }
  108. this.removeListener(this.previousCell);
  109. }
  110. this._cellMonitor = new ActivityMonitor({
  111. signal: this.activeCell.model.contentChanged,
  112. timeout: CELL_CHANGED_TIMEOUT
  113. });
  114. this._cellMonitor.activityStopped.connect(() => {
  115. this.sendEditorBreakpoints();
  116. }, this);
  117. this.previousCell = this.activeCell;
  118. this.setEditor(this.activeCell);
  119. }
  120. }
  121. protected sendEditorBreakpoints() {
  122. const cell = this.activeCell;
  123. if (!cell || !cell.editor) {
  124. return;
  125. }
  126. const breakpoints = this.getBreakpointsFromEditor(cell).map(lineInfo => {
  127. return Private.createBreakpoint(
  128. this._debuggerService.session.client.name,
  129. this.getEditorId(),
  130. lineInfo.line + 1
  131. );
  132. });
  133. void this._debuggerService.updateBreakpoints(
  134. cell.editor.model.value.text,
  135. breakpoints
  136. );
  137. }
  138. protected setEditor(cell: CodeCell) {
  139. if (!cell || !cell.editor) {
  140. return;
  141. }
  142. const editor = cell.editor as CodeMirrorEditor;
  143. this.addBreakpointsToEditor(cell);
  144. editor.setOption('lineNumbers', true);
  145. editor.editor.setOption('gutters', [
  146. 'CodeMirror-linenumbers',
  147. 'breakpoints'
  148. ]);
  149. editor.editor.on('gutterClick', this.onGutterClick);
  150. }
  151. protected removeListener(cell: CodeCell) {
  152. if (cell.isDisposed) {
  153. return;
  154. }
  155. const editor = cell.editor as CodeMirrorEditor;
  156. editor.editor.off('gutterClick', this.onGutterClick);
  157. }
  158. protected getEditorId(): string {
  159. return this.activeCell.editor.uuid;
  160. }
  161. protected onGutterClick = (editor: Editor, lineNumber: number) => {
  162. const info = editor.lineInfo(lineNumber);
  163. if (!info) {
  164. return;
  165. }
  166. const isRemoveGutter = !!info.gutterMarkers;
  167. let breakpoints: Breakpoints.IBreakpoint[] = this.getBreakpoints(
  168. this._activeCell
  169. );
  170. if (isRemoveGutter) {
  171. breakpoints = breakpoints.filter(ele => ele.line !== info.line + 1);
  172. } else {
  173. breakpoints.push(
  174. Private.createBreakpoint(
  175. this._debuggerService.session.client.name,
  176. this.getEditorId(),
  177. info.line + 1
  178. )
  179. );
  180. }
  181. void this._debuggerService.updateBreakpoints(
  182. this._activeCell.model.value.text,
  183. breakpoints
  184. );
  185. };
  186. private addBreakpointsToEditor(cell: CodeCell) {
  187. this.clearGutter(cell);
  188. const editor = cell.editor as CodeMirrorEditor;
  189. const breakpoints = this.getBreakpoints(cell);
  190. breakpoints.forEach(breakpoint => {
  191. editor.editor.setGutterMarker(
  192. breakpoint.line - 1,
  193. 'breakpoints',
  194. Private.createMarkerNode()
  195. );
  196. });
  197. }
  198. private getBreakpointsFromEditor(cell: CodeCell): ILineInfo[] {
  199. const editor = cell.editor as CodeMirrorEditor;
  200. let lines = [];
  201. for (let i = 0; i < editor.doc.lineCount(); i++) {
  202. const info = editor.editor.lineInfo(i);
  203. if (info.gutterMarkers) {
  204. lines.push(info);
  205. }
  206. }
  207. return lines;
  208. }
  209. private getBreakpoints(cell: CodeCell): Breakpoints.IBreakpoint[] {
  210. return this._debuggerModel.breakpointsModel.getBreakpoints(
  211. this._debuggerService.getCellId(cell.model.value.text)
  212. );
  213. }
  214. private _previousCell: CodeCell;
  215. private _debuggerModel: Debugger.Model;
  216. private breakpointsModel: Breakpoints.Model;
  217. private _activeCell: CodeCell;
  218. private _debuggerService: IDebugger;
  219. private _cellMonitor: ActivityMonitor<ICellModel, void> = null;
  220. }
  221. export namespace CellManager {
  222. export interface IOptions {
  223. debuggerModel: Debugger.Model;
  224. debuggerService: IDebugger;
  225. breakpointsModel: Breakpoints.Model;
  226. activeCell?: CodeCell;
  227. type: SessionTypes;
  228. }
  229. /**
  230. * Highlight the current line of the frame in the given cell.
  231. * @param cell The cell to highlight.
  232. * @param frame The frame with the current line number.
  233. */
  234. export function showCurrentLine(cell: Cell, frame: Callstack.IFrame) {
  235. const editor = cell.editor as CodeMirrorEditor;
  236. cleanupHighlight(cell);
  237. editor.editor.addLineClass(frame.line - 1, 'wrap', LINE_HIGHLIGHT_CLASS);
  238. }
  239. /**
  240. * Remove all line highlighting indicators for the given cell.
  241. * @param cell The cell to cleanup.
  242. */
  243. export function cleanupHighlight(cell: Cell) {
  244. if (!cell || cell.isDisposed) {
  245. return;
  246. }
  247. const editor = cell.editor as CodeMirrorEditor;
  248. editor.doc.eachLine(line => {
  249. editor.editor.removeLineClass(line, 'wrap', LINE_HIGHLIGHT_CLASS);
  250. });
  251. }
  252. }
  253. export interface ILineInfo {
  254. line: any;
  255. handle: any;
  256. text: string;
  257. /** Object mapping gutter IDs to marker elements. */
  258. gutterMarkers: any;
  259. textClass: string;
  260. bgClass: string;
  261. wrapClass: string;
  262. /** Array of line widgets attached to this line. */
  263. widgets: any;
  264. }
  265. namespace Private {
  266. export function createMarkerNode() {
  267. let marker = document.createElement('div');
  268. marker.className = 'jp-breakpoint-marker';
  269. marker.innerHTML = '●';
  270. return marker;
  271. }
  272. export function createBreakpoint(
  273. session: string,
  274. type: string,
  275. line: number
  276. ) {
  277. return {
  278. line,
  279. active: true,
  280. verified: true,
  281. source: {
  282. name: session
  283. }
  284. };
  285. }
  286. }