cell.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  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._id = options.debuggerService.session.client.name;
  19. this.onModelChanged();
  20. this._debuggerService.modelChanged.connect(() => this.onModelChanged());
  21. this.activeCell = options.activeCell;
  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. requestAnimationFrame(() => {
  118. this.setEditor(this.activeCell);
  119. });
  120. this.previousCell = this.activeCell;
  121. }
  122. }
  123. protected sendEditorBreakpoints() {
  124. const cell = this.activeCell;
  125. if (!cell || !cell.editor) {
  126. return;
  127. }
  128. const breakpoints = this.getBreakpointsFromEditor(cell).map(lineInfo => {
  129. return Private.createBreakpoint(
  130. this._debuggerService.session.client.name,
  131. this.getEditorId(),
  132. lineInfo.line + 1
  133. );
  134. });
  135. void this._debuggerService.updateBreakpoints(
  136. cell.editor.model.value.text,
  137. breakpoints
  138. );
  139. }
  140. protected setEditor(cell: CodeCell) {
  141. if (!cell || !cell.editor) {
  142. return;
  143. }
  144. const editor = cell.editor as CodeMirrorEditor;
  145. this.addBreakpointsToEditor(cell);
  146. editor.setOption('lineNumbers', true);
  147. editor.editor.setOption('gutters', [
  148. 'CodeMirror-linenumbers',
  149. 'breakpoints'
  150. ]);
  151. editor.editor.on('gutterClick', this.onGutterClick);
  152. }
  153. protected removeListener(cell: CodeCell) {
  154. if (cell.isDisposed) {
  155. return;
  156. }
  157. const editor = cell.editor as CodeMirrorEditor;
  158. editor.editor.off('gutterClick', this.onGutterClick);
  159. }
  160. protected getEditorId(): string {
  161. return this.activeCell.editor.uuid;
  162. }
  163. protected onGutterClick = (editor: Editor, lineNumber: number) => {
  164. const info = editor.lineInfo(lineNumber);
  165. if (!info || this._id !== this._debuggerService.session.client.name) {
  166. return;
  167. }
  168. this.clearGutter(this.activeCell);
  169. const isRemoveGutter = !!info.gutterMarkers;
  170. let breakpoints: Breakpoints.IBreakpoint[] = this.getBreakpoints(
  171. this._activeCell
  172. );
  173. if (isRemoveGutter) {
  174. breakpoints = breakpoints.filter(ele => ele.line !== info.line + 1);
  175. } else {
  176. breakpoints.push(
  177. Private.createBreakpoint(
  178. this._debuggerService.session.client.name,
  179. this.getEditorId(),
  180. info.line + 1
  181. )
  182. );
  183. }
  184. void this._debuggerService.updateBreakpoints(
  185. this._activeCell.model.value.text,
  186. breakpoints
  187. );
  188. };
  189. private addBreakpointsToEditor(cell: CodeCell) {
  190. const editor = cell.editor as CodeMirrorEditor;
  191. const breakpoints = this.getBreakpoints(cell);
  192. if (
  193. breakpoints.length === 0 &&
  194. this._id === this._debuggerService.session.client.name
  195. ) {
  196. this.clearGutter(cell);
  197. } else {
  198. breakpoints.forEach(breakpoint => {
  199. editor.editor.setGutterMarker(
  200. breakpoint.line - 1,
  201. 'breakpoints',
  202. Private.createMarkerNode()
  203. );
  204. });
  205. }
  206. }
  207. private getBreakpointsFromEditor(cell: CodeCell): ILineInfo[] {
  208. const editor = cell.editor as CodeMirrorEditor;
  209. let lines = [];
  210. for (let i = 0; i < editor.doc.lineCount(); i++) {
  211. const info = editor.editor.lineInfo(i);
  212. if (info.gutterMarkers) {
  213. lines.push(info);
  214. }
  215. }
  216. return lines;
  217. }
  218. private getBreakpoints(cell: CodeCell): Breakpoints.IBreakpoint[] {
  219. return this._debuggerModel.breakpointsModel.getBreakpoints(
  220. this._debuggerService.getCellId(cell.model.value.text)
  221. );
  222. }
  223. private _previousCell: CodeCell;
  224. private _debuggerModel: Debugger.Model;
  225. private breakpointsModel: Breakpoints.Model;
  226. private _activeCell: CodeCell;
  227. private _debuggerService: IDebugger;
  228. private _cellMonitor: ActivityMonitor<ICellModel, void> = null;
  229. private _id: string;
  230. }
  231. export namespace CellManager {
  232. export interface IOptions {
  233. debuggerModel: Debugger.Model;
  234. debuggerService: IDebugger;
  235. breakpointsModel: Breakpoints.Model;
  236. activeCell?: CodeCell;
  237. type: SessionTypes;
  238. }
  239. /**
  240. * Highlight the current line of the frame in the given cell.
  241. * @param cell The cell to highlight.
  242. * @param frame The frame with the current line number.
  243. */
  244. export function showCurrentLine(cell: Cell, frame: Callstack.IFrame) {
  245. const editor = cell.editor as CodeMirrorEditor;
  246. cleanupHighlight(cell);
  247. editor.editor.addLineClass(frame.line - 1, 'wrap', LINE_HIGHLIGHT_CLASS);
  248. }
  249. /**
  250. * Remove all line highlighting indicators for the given cell.
  251. * @param cell The cell to cleanup.
  252. */
  253. export function cleanupHighlight(cell: Cell) {
  254. if (!cell || cell.isDisposed) {
  255. return;
  256. }
  257. const editor = cell.editor as CodeMirrorEditor;
  258. editor.doc.eachLine(line => {
  259. editor.editor.removeLineClass(line, 'wrap', LINE_HIGHLIGHT_CLASS);
  260. });
  261. }
  262. }
  263. export interface ILineInfo {
  264. line: any;
  265. handle: any;
  266. text: string;
  267. /** Object mapping gutter IDs to marker elements. */
  268. gutterMarkers: any;
  269. textClass: string;
  270. bgClass: string;
  271. wrapClass: string;
  272. /** Array of line widgets attached to this line. */
  273. widgets: any;
  274. }
  275. namespace Private {
  276. export function createMarkerNode() {
  277. let marker = document.createElement('div');
  278. marker.className = 'jp-breakpoint-marker';
  279. marker.innerHTML = '●';
  280. return marker;
  281. }
  282. export function createBreakpoint(
  283. session: string,
  284. type: string,
  285. line: number
  286. ) {
  287. return {
  288. line,
  289. active: true,
  290. verified: true,
  291. source: {
  292. name: session
  293. }
  294. };
  295. }
  296. }