cell.ts 7.6 KB

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