cell.ts 9.0 KB

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