editor.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { CodeEditor } from '@jupyterlab/codeeditor';
  4. import { CodeMirrorEditor } from '@jupyterlab/codemirror';
  5. import { ActivityMonitor } from '@jupyterlab/coreutils';
  6. import { IObservableString } from '@jupyterlab/observables';
  7. import { IDisposable } from '@lumino/disposable';
  8. import { Signal } from '@lumino/signaling';
  9. import { Editor } from 'codemirror';
  10. import { IDebugger } from '../tokens';
  11. /**
  12. * The class name added to the current line.
  13. */
  14. const LINE_HIGHLIGHT_CLASS = 'jp-DebuggerEditor-highlight';
  15. /**
  16. * The timeout for listening to editor content changes.
  17. */
  18. const EDITOR_CHANGED_TIMEOUT = 1000;
  19. /**
  20. * A handler for a CodeEditor.IEditor.
  21. */
  22. export class EditorHandler implements IDisposable {
  23. /**
  24. * Instantiate a new EditorHandler.
  25. *
  26. * @param options The instantiation options for a EditorHandler.
  27. */
  28. constructor(options: EditorHandler.IOptions) {
  29. this._id = options.debuggerService.session?.connection?.id ?? '';
  30. this._path = options.path ?? '';
  31. this._debuggerService = options.debuggerService;
  32. this._editor = options.editor;
  33. this._editorMonitor = new ActivityMonitor({
  34. signal: this._editor.model.value.changed,
  35. timeout: EDITOR_CHANGED_TIMEOUT
  36. });
  37. this._editorMonitor.activityStopped.connect(() => {
  38. this._sendEditorBreakpoints();
  39. }, this);
  40. this._debuggerService.model.breakpoints.changed.connect(async () => {
  41. if (!this._editor || this._editor.isDisposed) {
  42. return;
  43. }
  44. this._addBreakpointsToEditor();
  45. });
  46. this._debuggerService.model.breakpoints.restored.connect(async () => {
  47. if (!this._editor || this._editor.isDisposed) {
  48. return;
  49. }
  50. this._addBreakpointsToEditor();
  51. });
  52. this._debuggerService.model.callstack.currentFrameChanged.connect(() => {
  53. EditorHandler.clearHighlight(this._editor);
  54. });
  55. this._setupEditor();
  56. }
  57. /**
  58. * Whether the handler is disposed.
  59. */
  60. isDisposed: boolean;
  61. /**
  62. * Dispose the handler.
  63. */
  64. dispose(): void {
  65. if (this.isDisposed) {
  66. return;
  67. }
  68. this._editorMonitor.dispose();
  69. this._clearEditor();
  70. this.isDisposed = true;
  71. Signal.clearData(this);
  72. }
  73. /**
  74. * Setup the editor.
  75. */
  76. private _setupEditor(): void {
  77. if (!this._editor || this._editor.isDisposed) {
  78. return;
  79. }
  80. this._addBreakpointsToEditor();
  81. const editor = this._editor as CodeMirrorEditor;
  82. editor.setOption('lineNumbers', true);
  83. editor.editor.setOption('gutters', [
  84. 'CodeMirror-linenumbers',
  85. 'breakpoints'
  86. ]);
  87. editor.editor.on('gutterClick', this._onGutterClick);
  88. }
  89. /**
  90. * Clear the editor by removing visual elements and handlers.
  91. */
  92. private _clearEditor(): void {
  93. if (!this._editor || this._editor.isDisposed) {
  94. return;
  95. }
  96. const editor = this._editor as CodeMirrorEditor;
  97. EditorHandler.clearHighlight(editor);
  98. EditorHandler.clearGutter(editor);
  99. editor.setOption('lineNumbers', false);
  100. editor.editor.setOption('gutters', []);
  101. editor.editor.off('gutterClick', this._onGutterClick);
  102. }
  103. /**
  104. * Send the breakpoints from the editor UI via the debug service.
  105. */
  106. private _sendEditorBreakpoints(): void {
  107. if (this._editor.isDisposed) {
  108. return;
  109. }
  110. const breakpoints = this._getBreakpointsFromEditor().map(lineInfo => {
  111. return Private.createBreakpoint(
  112. this._debuggerService.session?.connection?.name || '',
  113. lineInfo.line + 1
  114. );
  115. });
  116. void this._debuggerService.updateBreakpoints(
  117. this._editor.model.value.text,
  118. breakpoints,
  119. this._path
  120. );
  121. }
  122. /**
  123. * Handle a click on the gutter.
  124. *
  125. * @param editor The editor from where the click originated.
  126. * @param lineNumber The line corresponding to the click event.
  127. */
  128. private _onGutterClick = (editor: Editor, lineNumber: number): void => {
  129. const info = editor.lineInfo(lineNumber);
  130. if (!info || this._id !== this._debuggerService.session?.connection?.id) {
  131. return;
  132. }
  133. const remove = !!info.gutterMarkers;
  134. let breakpoints: IDebugger.IBreakpoint[] = this._getBreakpoints();
  135. if (remove) {
  136. breakpoints = breakpoints.filter(ele => ele.line !== info.line + 1);
  137. } else {
  138. breakpoints.push(
  139. Private.createBreakpoint(
  140. this._path ?? this._debuggerService.session.connection.name,
  141. info.line + 1
  142. )
  143. );
  144. }
  145. void this._debuggerService.updateBreakpoints(
  146. this._editor.model.value.text,
  147. breakpoints,
  148. this._path
  149. );
  150. };
  151. /**
  152. * Add the breakpoints to the editor.
  153. */
  154. private _addBreakpointsToEditor(): void {
  155. const editor = this._editor as CodeMirrorEditor;
  156. const breakpoints = this._getBreakpoints();
  157. if (this._id !== this._debuggerService.session?.connection?.id) {
  158. return;
  159. }
  160. EditorHandler.clearGutter(editor);
  161. breakpoints.forEach(breakpoint => {
  162. if (typeof breakpoint.line === 'number') {
  163. editor.editor.setGutterMarker(
  164. breakpoint.line - 1,
  165. 'breakpoints',
  166. Private.createMarkerNode()
  167. );
  168. }
  169. });
  170. }
  171. /**
  172. * Retrieve the breakpoints from the editor.
  173. */
  174. private _getBreakpointsFromEditor(): Private.ILineInfo[] {
  175. const editor = this._editor as CodeMirrorEditor;
  176. let lines = [];
  177. for (let i = 0; i < editor.doc.lineCount(); i++) {
  178. const info = editor.editor.lineInfo(i);
  179. if (info.gutterMarkers) {
  180. lines.push(info);
  181. }
  182. }
  183. return lines;
  184. }
  185. /**
  186. * Get the breakpoints for the editor using its content (code),
  187. * or its path (if it exists).
  188. */
  189. private _getBreakpoints(): IDebugger.IBreakpoint[] {
  190. const code = this._editor.model.value.text;
  191. return this._debuggerService.model.breakpoints.getBreakpoints(
  192. this._path || this._debuggerService.getCodeId(code)
  193. );
  194. }
  195. private _id: string;
  196. private _path: string;
  197. private _editor: CodeEditor.IEditor;
  198. private _debuggerService: IDebugger;
  199. private _editorMonitor: ActivityMonitor<
  200. IObservableString,
  201. IObservableString.IChangedArgs
  202. >;
  203. }
  204. /**
  205. * A namespace for EditorHandler `statics`.
  206. */
  207. export namespace EditorHandler {
  208. /**
  209. * Instantiation options for `EditorHandler`.
  210. */
  211. export interface IOptions {
  212. /**
  213. * The debugger service.
  214. */
  215. debuggerService: IDebugger;
  216. /**
  217. * The code editor to handle.
  218. */
  219. editor: CodeEditor.IEditor;
  220. /**
  221. * An optional path to a source file.
  222. */
  223. path?: string;
  224. }
  225. /**
  226. * Highlight the current line of the frame in the given editor.
  227. *
  228. * @param editor The editor to highlight.
  229. * @param line The line number.
  230. */
  231. export function showCurrentLine(
  232. editor: CodeEditor.IEditor,
  233. line: number
  234. ): void {
  235. clearHighlight(editor);
  236. const cmEditor = editor as CodeMirrorEditor;
  237. cmEditor.editor.addLineClass(line - 1, 'wrap', LINE_HIGHLIGHT_CLASS);
  238. }
  239. /**
  240. * Remove all line highlighting indicators for the given editor.
  241. *
  242. * @param editor The editor to cleanup.
  243. */
  244. export function clearHighlight(editor: CodeEditor.IEditor): void {
  245. if (!editor || editor.isDisposed) {
  246. return;
  247. }
  248. const cmEditor = editor as CodeMirrorEditor;
  249. cmEditor.doc.eachLine(line => {
  250. cmEditor.editor.removeLineClass(line, 'wrap', LINE_HIGHLIGHT_CLASS);
  251. });
  252. }
  253. /**
  254. * Remove line numbers and all gutters from editor.
  255. *
  256. * @param editor The editor to cleanup.
  257. */
  258. export function clearGutter(editor: CodeEditor.IEditor): void {
  259. if (!editor) {
  260. return;
  261. }
  262. const cmEditor = editor as CodeMirrorEditor;
  263. cmEditor.doc.eachLine(line => {
  264. if ((line as Private.ILineInfo).gutterMarkers) {
  265. cmEditor.editor.setGutterMarker(line, 'breakpoints', null);
  266. }
  267. });
  268. }
  269. }
  270. /**
  271. * A namespace for module private data.
  272. */
  273. namespace Private {
  274. /**
  275. * Create a marker DOM element for a breakpoint.
  276. */
  277. export function createMarkerNode(): HTMLElement {
  278. const marker = document.createElement('div');
  279. marker.className = 'jp-DebuggerEditor-marker';
  280. marker.innerHTML = '●';
  281. return marker;
  282. }
  283. /**
  284. * Create a new breakpoint.
  285. *
  286. * @param session The name of the session.
  287. * @param line The line number of the breakpoint.
  288. */
  289. export function createBreakpoint(
  290. session: string,
  291. line: number
  292. ): IDebugger.IBreakpoint {
  293. return {
  294. line,
  295. verified: true,
  296. source: {
  297. name: session
  298. }
  299. };
  300. }
  301. /**
  302. * An interface for an editor line info.
  303. */
  304. export interface ILineInfo {
  305. line: any;
  306. handle: any;
  307. text: string;
  308. /** Object mapping gutter IDs to marker elements. */
  309. gutterMarkers: any;
  310. textClass: string;
  311. bgClass: string;
  312. wrapClass: string;
  313. /** Array of line widgets attached to this line. */
  314. widgets: any;
  315. }
  316. }