cell.ts 8.4 KB

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