cell.ts 9.1 KB

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