handler.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { ISessionContext, SessionContext } from '@jupyterlab/apputils';
  4. import { CodeEditor, CodeEditorWrapper } from '@jupyterlab/codeeditor';
  5. import { CodeMirrorEditor } from '@jupyterlab/codemirror';
  6. import {
  7. Completer,
  8. CompleterModel,
  9. CompletionHandler,
  10. KernelConnector
  11. } from '@jupyterlab/completer';
  12. import { createSessionContext } from '@jupyterlab/testutils';
  13. function createEditorWidget(): CodeEditorWrapper {
  14. const model = new CodeEditor.Model();
  15. const factory = (options: CodeEditor.IOptions) => {
  16. return new CodeMirrorEditor(options);
  17. };
  18. return new CodeEditorWrapper({ factory, model });
  19. }
  20. class TestCompleterModel extends CompleterModel {
  21. methods: string[] = [];
  22. createPatch(patch: string): Completer.IPatch | undefined {
  23. this.methods.push('createPatch');
  24. return super.createPatch(patch);
  25. }
  26. handleTextChange(change: Completer.ITextState): void {
  27. this.methods.push('handleTextChange');
  28. super.handleTextChange(change);
  29. }
  30. }
  31. class TestCompletionHandler extends CompletionHandler {
  32. methods: string[] = [];
  33. onTextChanged(): void {
  34. super.onTextChanged();
  35. this.methods.push('onTextChanged');
  36. }
  37. onCompletionSelected(widget: Completer, value: string): void {
  38. super.onCompletionSelected(widget, value);
  39. this.methods.push('onCompletionSelected');
  40. }
  41. }
  42. describe('@jupyterlab/completer', () => {
  43. let connector: KernelConnector;
  44. let sessionContext: ISessionContext;
  45. beforeAll(async () => {
  46. sessionContext = await createSessionContext();
  47. await (sessionContext as SessionContext).initialize();
  48. connector = new KernelConnector({ session: sessionContext.session });
  49. });
  50. afterAll(() => sessionContext.shutdown());
  51. describe('CompletionHandler', () => {
  52. describe('#constructor()', () => {
  53. it('should create a completer handler', () => {
  54. const handler = new CompletionHandler({
  55. connector,
  56. completer: new Completer({ editor: null })
  57. });
  58. expect(handler).toBeInstanceOf(CompletionHandler);
  59. });
  60. });
  61. describe('#connector', () => {
  62. it('should be a data connector', () => {
  63. const handler = new CompletionHandler({
  64. connector,
  65. completer: new Completer({ editor: null })
  66. });
  67. expect(handler.connector).toHaveProperty('fetch');
  68. expect(handler.connector).toHaveProperty('remove');
  69. expect(handler.connector).toHaveProperty('save');
  70. });
  71. });
  72. describe('#editor', () => {
  73. it('should default to null', () => {
  74. const handler = new CompletionHandler({
  75. connector,
  76. completer: new Completer({ editor: null })
  77. });
  78. expect(handler.editor).toBeNull();
  79. });
  80. it('should be settable', () => {
  81. const handler = new CompletionHandler({
  82. connector,
  83. completer: new Completer({ editor: null })
  84. });
  85. const widget = createEditorWidget();
  86. expect(handler.editor).toBeNull();
  87. handler.editor = widget.editor;
  88. expect(handler.editor).toBe(widget.editor);
  89. });
  90. it('should be resettable', () => {
  91. const handler = new CompletionHandler({
  92. connector,
  93. completer: new Completer({ editor: null })
  94. });
  95. const one = createEditorWidget();
  96. const two = createEditorWidget();
  97. expect(handler.editor).toBeNull();
  98. handler.editor = one.editor;
  99. expect(handler.editor).toBe(one.editor);
  100. handler.editor = two.editor;
  101. expect(handler.editor).toBe(two.editor);
  102. });
  103. it('should remove the completer active and enabled classes of the old editor', () => {
  104. const handler = new CompletionHandler({
  105. connector,
  106. completer: new Completer({ editor: null })
  107. });
  108. const widget = createEditorWidget();
  109. handler.editor = widget.editor;
  110. widget.toggleClass('jp-mod-completer-enabled');
  111. widget.toggleClass('jp-mod-completer-active');
  112. handler.editor = null;
  113. expect(widget.hasClass('jp-mod-completer-enabled')).toBe(false);
  114. expect(widget.hasClass('jp-mod-completer-active')).toBe(false);
  115. });
  116. });
  117. describe('#isDisposed', () => {
  118. it('should be true if handler has been disposed', () => {
  119. const handler = new CompletionHandler({
  120. connector,
  121. completer: new Completer({ editor: null })
  122. });
  123. expect(handler.isDisposed).toBe(false);
  124. handler.dispose();
  125. expect(handler.isDisposed).toBe(true);
  126. });
  127. });
  128. describe('#dispose()', () => {
  129. it('should dispose of the handler resources', () => {
  130. const handler = new CompletionHandler({
  131. connector,
  132. completer: new Completer({ editor: null })
  133. });
  134. expect(handler.isDisposed).toBe(false);
  135. handler.dispose();
  136. expect(handler.isDisposed).toBe(true);
  137. });
  138. it('should be safe to call multiple times', () => {
  139. const handler = new CompletionHandler({
  140. connector,
  141. completer: new Completer({ editor: null })
  142. });
  143. expect(handler.isDisposed).toBe(false);
  144. handler.dispose();
  145. handler.dispose();
  146. expect(handler.isDisposed).toBe(true);
  147. });
  148. });
  149. describe('#onTextChanged()', () => {
  150. it('should fire when the active editor emits a text change', () => {
  151. const handler = new TestCompletionHandler({
  152. connector,
  153. completer: new Completer({ editor: null })
  154. });
  155. handler.editor = createEditorWidget().editor;
  156. expect(handler.methods).toEqual(
  157. expect.not.arrayContaining(['onTextChanged'])
  158. );
  159. handler.editor.model.value.text = 'foo';
  160. expect(handler.methods).toEqual(
  161. expect.arrayContaining(['onTextChanged'])
  162. );
  163. });
  164. it('should call model change handler if model exists', () => {
  165. const completer = new Completer({
  166. editor: null,
  167. model: new TestCompleterModel()
  168. });
  169. const handler = new TestCompletionHandler({ completer, connector });
  170. const editor = createEditorWidget().editor;
  171. const model = completer.model as TestCompleterModel;
  172. handler.editor = editor;
  173. expect(model.methods).toEqual(
  174. expect.not.arrayContaining(['handleTextChange'])
  175. );
  176. editor.model.value.text = 'bar';
  177. editor.setCursorPosition({ line: 0, column: 2 });
  178. // This signal is emitted (again) because the cursor position that
  179. // a natural user would create need to be recreated here.
  180. (editor.model.value.changed as any).emit({ type: 'set', value: 'bar' });
  181. expect(model.methods).toEqual(
  182. expect.arrayContaining(['handleTextChange'])
  183. );
  184. });
  185. });
  186. describe('#onCompletionSelected()', () => {
  187. it('should fire when the completer widget emits a signal', () => {
  188. const completer = new Completer({ editor: null });
  189. const handler = new TestCompletionHandler({ completer, connector });
  190. expect(handler.methods).toEqual(
  191. expect.not.arrayContaining(['onCompletionSelected'])
  192. );
  193. (completer.selected as any).emit('foo');
  194. expect(handler.methods).toEqual(
  195. expect.arrayContaining(['onCompletionSelected'])
  196. );
  197. });
  198. it('should call model create patch method if model exists', () => {
  199. const completer = new Completer({
  200. editor: null,
  201. model: new TestCompleterModel()
  202. });
  203. const handler = new TestCompletionHandler({ completer, connector });
  204. const model = completer.model as TestCompleterModel;
  205. handler.editor = createEditorWidget().editor;
  206. expect(model.methods).toEqual(
  207. expect.not.arrayContaining(['createPatch'])
  208. );
  209. (completer.selected as any).emit('foo');
  210. expect(model.methods).toEqual(expect.arrayContaining(['createPatch']));
  211. });
  212. it('should update cell if patch exists', () => {
  213. const model = new CompleterModel();
  214. const patch = 'foobar';
  215. const completer = new Completer({ editor: null, model });
  216. const handler = new TestCompletionHandler({ completer, connector });
  217. const editor = createEditorWidget().editor;
  218. const text = 'eggs\nfoo # comment\nbaz';
  219. const want = 'eggs\nfoobar # comment\nbaz';
  220. const line = 1;
  221. const column = 5; // this sets the cursor after the "#" sign - not in the mid of the replaced word
  222. const request: Completer.ITextState = {
  223. column,
  224. line,
  225. lineHeight: 0,
  226. charWidth: 0,
  227. coords: null,
  228. text
  229. };
  230. handler.editor = editor;
  231. handler.editor.model.value.text = text;
  232. handler.editor.setCursorPosition({ line, column: column + 3 });
  233. model.original = request;
  234. model.cursor = { start: column, end: column + 3 };
  235. (completer.selected as any).emit(patch);
  236. expect(handler.editor.model.value.text).toBe(want);
  237. expect(handler.editor.getCursorPosition()).toEqual({
  238. line,
  239. column: column + 6
  240. });
  241. });
  242. it('should be undoable and redoable', () => {
  243. const model = new CompleterModel();
  244. const patch = 'foobar';
  245. const completer = new Completer({ editor: null, model });
  246. const handler = new TestCompletionHandler({ completer, connector });
  247. const editor = createEditorWidget().editor;
  248. const text = 'eggs\nfoo # comment\nbaz';
  249. const want = 'eggs\nfoobar # comment\nbaz';
  250. const line = 1;
  251. const column = 5;
  252. const request: Completer.ITextState = {
  253. column,
  254. line,
  255. lineHeight: 0,
  256. charWidth: 0,
  257. coords: null,
  258. text
  259. };
  260. handler.editor = editor;
  261. handler.editor.model.value.text = text;
  262. handler.editor.model.sharedModel.clearUndoHistory();
  263. handler.editor.setCursorPosition({ line, column: column + 3 });
  264. model.original = request;
  265. model.cursor = { start: column, end: column + 3 };
  266. // Make the completion, check its value and cursor position.
  267. (completer.selected as any).emit(patch);
  268. expect(editor.model.value.text).toBe(want);
  269. expect(editor.getCursorPosition()).toEqual({
  270. line,
  271. column: column + 6
  272. });
  273. console.warn(editor.getCursorPosition());
  274. // Undo the completion, check its value and cursor position.
  275. editor.undo();
  276. expect(editor.model.value.text).toBe(text);
  277. expect(editor.getCursorPosition()).toEqual({
  278. line,
  279. column: column + 3
  280. });
  281. console.warn(editor.getCursorPosition());
  282. // Redo the completion, check its value and cursor position.
  283. editor.redo();
  284. expect(editor.model.value.text).toBe(want);
  285. expect(editor.getCursorPosition()).toEqual({
  286. line,
  287. column: column + 6
  288. });
  289. console.warn(editor.getCursorPosition());
  290. });
  291. });
  292. it('should update cursor position after autocomplete on empty word', () => {
  293. const model = new CompleterModel();
  294. const patch = 'foobar';
  295. const completer = new Completer({ editor: null, model });
  296. const handler = new TestCompletionHandler({ completer, connector });
  297. const editor = createEditorWidget().editor;
  298. const text = 'eggs\n # comment\nbaz';
  299. const want = 'eggs\n foobar # comment\nbaz';
  300. const line = 1;
  301. const column = 1;
  302. const request: Completer.ITextState = {
  303. column: column,
  304. line,
  305. lineHeight: 0,
  306. charWidth: 0,
  307. coords: null,
  308. text
  309. };
  310. handler.editor = editor;
  311. handler.editor.model.value.text = text;
  312. handler.editor.model.sharedModel.clearUndoHistory();
  313. handler.editor.setCursorPosition({ line, column });
  314. model.original = request;
  315. const offset = handler.editor.getOffsetAt({ line, column });
  316. model.cursor = { start: offset, end: offset };
  317. // Make the completion, check its value and cursor position.
  318. (completer.selected as any).emit(patch);
  319. expect(editor.model.value.text).toBe(want);
  320. expect(editor.getCursorPosition()).toEqual({
  321. line,
  322. column: column + 6
  323. });
  324. });
  325. });
  326. });