text-mate-language-mode-spec.js 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497
  1. const NullGrammar = require('../src/null-grammar');
  2. const TextMateLanguageMode = require('../src/text-mate-language-mode');
  3. const TextBuffer = require('text-buffer');
  4. const { Point } = TextBuffer;
  5. const _ = require('underscore-plus');
  6. const dedent = require('dedent');
  7. describe('TextMateLanguageMode', () => {
  8. let languageMode, buffer, config;
  9. beforeEach(async () => {
  10. config = atom.config;
  11. config.set('core.useTreeSitterParsers', false);
  12. // enable async tokenization
  13. TextMateLanguageMode.prototype.chunkSize = 5;
  14. jasmine.unspy(TextMateLanguageMode.prototype, 'tokenizeInBackground');
  15. await atom.packages.activatePackage('language-javascript');
  16. });
  17. afterEach(() => {
  18. buffer && buffer.destroy();
  19. languageMode && languageMode.destroy();
  20. config.unset('core.useTreeSitterParsers');
  21. });
  22. describe('when the editor is constructed with the largeFileMode option set to true', () => {
  23. it("loads the editor but doesn't tokenize", async () => {
  24. const line = 'a b c d\n';
  25. buffer = new TextBuffer(line.repeat(256 * 1024));
  26. expect(buffer.getText().length).toBe(2 * 1024 * 1024);
  27. languageMode = new TextMateLanguageMode({
  28. buffer,
  29. grammar: atom.grammars.grammarForScopeName('source.js'),
  30. tabLength: 2
  31. });
  32. buffer.setLanguageMode(languageMode);
  33. expect(languageMode.isRowCommented(0)).toBeFalsy();
  34. // It treats the entire line as one big token
  35. let iterator = languageMode.buildHighlightIterator();
  36. iterator.seek({ row: 0, column: 0 });
  37. iterator.moveToSuccessor();
  38. expect(iterator.getPosition()).toEqual({ row: 0, column: 7 });
  39. buffer.insert([0, 0], 'hey"');
  40. iterator = languageMode.buildHighlightIterator();
  41. iterator.seek({ row: 0, column: 0 });
  42. iterator.moveToSuccessor();
  43. expect(iterator.getPosition()).toEqual({ row: 0, column: 11 });
  44. });
  45. });
  46. describe('tokenizing', () => {
  47. describe('when the buffer is destroyed', () => {
  48. beforeEach(() => {
  49. buffer = atom.project.bufferForPathSync('sample.js');
  50. languageMode = new TextMateLanguageMode({
  51. buffer,
  52. config,
  53. grammar: atom.grammars.grammarForScopeName('source.js')
  54. });
  55. languageMode.startTokenizing();
  56. });
  57. it('stops tokenization', () => {
  58. languageMode.destroy();
  59. spyOn(languageMode, 'tokenizeNextChunk');
  60. advanceClock();
  61. expect(languageMode.tokenizeNextChunk).not.toHaveBeenCalled();
  62. });
  63. });
  64. describe('when the buffer contains soft-tabs', () => {
  65. beforeEach(() => {
  66. buffer = atom.project.bufferForPathSync('sample.js');
  67. languageMode = new TextMateLanguageMode({
  68. buffer,
  69. config,
  70. grammar: atom.grammars.grammarForScopeName('source.js')
  71. });
  72. buffer.setLanguageMode(languageMode);
  73. languageMode.startTokenizing();
  74. });
  75. afterEach(() => {
  76. languageMode.destroy();
  77. buffer.release();
  78. });
  79. describe('on construction', () =>
  80. it('tokenizes lines chunk at a time in the background', () => {
  81. const line0 = languageMode.tokenizedLines[0];
  82. expect(line0).toBeUndefined();
  83. const line11 = languageMode.tokenizedLines[11];
  84. expect(line11).toBeUndefined();
  85. // tokenize chunk 1
  86. advanceClock();
  87. expect(languageMode.tokenizedLines[0].ruleStack != null).toBeTruthy();
  88. expect(languageMode.tokenizedLines[4].ruleStack != null).toBeTruthy();
  89. expect(languageMode.tokenizedLines[5]).toBeUndefined();
  90. // tokenize chunk 2
  91. advanceClock();
  92. expect(languageMode.tokenizedLines[5].ruleStack != null).toBeTruthy();
  93. expect(languageMode.tokenizedLines[9].ruleStack != null).toBeTruthy();
  94. expect(languageMode.tokenizedLines[10]).toBeUndefined();
  95. // tokenize last chunk
  96. advanceClock();
  97. expect(
  98. languageMode.tokenizedLines[10].ruleStack != null
  99. ).toBeTruthy();
  100. expect(
  101. languageMode.tokenizedLines[12].ruleStack != null
  102. ).toBeTruthy();
  103. }));
  104. describe('when the buffer is partially tokenized', () => {
  105. beforeEach(() => {
  106. // tokenize chunk 1 only
  107. advanceClock();
  108. });
  109. describe('when there is a buffer change inside the tokenized region', () => {
  110. describe('when lines are added', () => {
  111. it('pushes the invalid rows down', () => {
  112. expect(languageMode.firstInvalidRow()).toBe(5);
  113. buffer.insert([1, 0], '\n\n');
  114. expect(languageMode.firstInvalidRow()).toBe(7);
  115. });
  116. });
  117. describe('when lines are removed', () => {
  118. it('pulls the invalid rows up', () => {
  119. expect(languageMode.firstInvalidRow()).toBe(5);
  120. buffer.delete([[1, 0], [3, 0]]);
  121. expect(languageMode.firstInvalidRow()).toBe(2);
  122. });
  123. });
  124. describe('when the change invalidates all the lines before the current invalid region', () => {
  125. it('retokenizes the invalidated lines and continues into the valid region', () => {
  126. expect(languageMode.firstInvalidRow()).toBe(5);
  127. buffer.insert([2, 0], '/*');
  128. expect(languageMode.firstInvalidRow()).toBe(3);
  129. advanceClock();
  130. expect(languageMode.firstInvalidRow()).toBe(8);
  131. });
  132. });
  133. });
  134. describe('when there is a buffer change surrounding an invalid row', () => {
  135. it('pushes the invalid row to the end of the change', () => {
  136. buffer.setTextInRange([[4, 0], [6, 0]], '\n\n\n');
  137. expect(languageMode.firstInvalidRow()).toBe(8);
  138. });
  139. });
  140. describe('when there is a buffer change inside an invalid region', () => {
  141. it('does not attempt to tokenize the lines in the change, and preserves the existing invalid row', () => {
  142. expect(languageMode.firstInvalidRow()).toBe(5);
  143. buffer.setTextInRange([[6, 0], [7, 0]], '\n\n\n');
  144. expect(languageMode.tokenizedLines[6]).toBeUndefined();
  145. expect(languageMode.tokenizedLines[7]).toBeUndefined();
  146. expect(languageMode.firstInvalidRow()).toBe(5);
  147. });
  148. });
  149. });
  150. describe('when the buffer is fully tokenized', () => {
  151. beforeEach(() => fullyTokenize(languageMode));
  152. describe('when there is a buffer change that is smaller than the chunk size', () => {
  153. describe('when lines are updated, but none are added or removed', () => {
  154. it('updates tokens to reflect the change', () => {
  155. buffer.setTextInRange([[0, 0], [2, 0]], 'foo()\n7\n');
  156. expect(languageMode.tokenizedLines[0].tokens[1]).toEqual({
  157. value: '(',
  158. scopes: [
  159. 'source.js',
  160. 'meta.function-call.js',
  161. 'meta.arguments.js',
  162. 'punctuation.definition.arguments.begin.bracket.round.js'
  163. ]
  164. });
  165. expect(languageMode.tokenizedLines[1].tokens[0]).toEqual({
  166. value: '7',
  167. scopes: ['source.js', 'constant.numeric.decimal.js']
  168. });
  169. // line 2 is unchanged
  170. expect(languageMode.tokenizedLines[2].tokens[1]).toEqual({
  171. value: 'if',
  172. scopes: ['source.js', 'keyword.control.js']
  173. });
  174. });
  175. describe('when the change invalidates the tokenization of subsequent lines', () => {
  176. it('schedules the invalidated lines to be tokenized in the background', () => {
  177. buffer.insert([5, 30], '/* */');
  178. buffer.insert([2, 0], '/*');
  179. expect(languageMode.tokenizedLines[3].tokens[0].scopes).toEqual(
  180. ['source.js']
  181. );
  182. advanceClock();
  183. expect(languageMode.tokenizedLines[3].tokens[0].scopes).toEqual(
  184. ['source.js', 'comment.block.js']
  185. );
  186. expect(languageMode.tokenizedLines[4].tokens[0].scopes).toEqual(
  187. ['source.js', 'comment.block.js']
  188. );
  189. expect(languageMode.tokenizedLines[5].tokens[0].scopes).toEqual(
  190. ['source.js', 'comment.block.js']
  191. );
  192. });
  193. });
  194. it('resumes highlighting with the state of the previous line', () => {
  195. buffer.insert([0, 0], '/*');
  196. buffer.insert([5, 0], '*/');
  197. buffer.insert([1, 0], 'var ');
  198. expect(languageMode.tokenizedLines[1].tokens[0].scopes).toEqual([
  199. 'source.js',
  200. 'comment.block.js'
  201. ]);
  202. });
  203. });
  204. describe('when lines are both updated and removed', () => {
  205. it('updates tokens to reflect the change', () => {
  206. buffer.setTextInRange([[1, 0], [3, 0]], 'foo()');
  207. // previous line 0 remains
  208. expect(languageMode.tokenizedLines[0].tokens[0]).toEqual({
  209. value: 'var',
  210. scopes: ['source.js', 'storage.type.var.js']
  211. });
  212. // previous line 3 should be combined with input to form line 1
  213. expect(languageMode.tokenizedLines[1].tokens[0]).toEqual({
  214. value: 'foo',
  215. scopes: [
  216. 'source.js',
  217. 'meta.function-call.js',
  218. 'entity.name.function.js'
  219. ]
  220. });
  221. expect(languageMode.tokenizedLines[1].tokens[6]).toEqual({
  222. value: '=',
  223. scopes: ['source.js', 'keyword.operator.assignment.js']
  224. });
  225. // lines below deleted regions should be shifted upward
  226. expect(languageMode.tokenizedLines[2].tokens[1]).toEqual({
  227. value: 'while',
  228. scopes: ['source.js', 'keyword.control.js']
  229. });
  230. expect(languageMode.tokenizedLines[3].tokens[1]).toEqual({
  231. value: '=',
  232. scopes: ['source.js', 'keyword.operator.assignment.js']
  233. });
  234. expect(languageMode.tokenizedLines[4].tokens[1]).toEqual({
  235. value: '<',
  236. scopes: ['source.js', 'keyword.operator.comparison.js']
  237. });
  238. });
  239. });
  240. describe('when the change invalidates the tokenization of subsequent lines', () => {
  241. it('schedules the invalidated lines to be tokenized in the background', () => {
  242. buffer.insert([5, 30], '/* */');
  243. buffer.setTextInRange([[2, 0], [3, 0]], '/*');
  244. expect(languageMode.tokenizedLines[2].tokens[0].scopes).toEqual([
  245. 'source.js',
  246. 'comment.block.js',
  247. 'punctuation.definition.comment.begin.js'
  248. ]);
  249. expect(languageMode.tokenizedLines[3].tokens[0].scopes).toEqual([
  250. 'source.js'
  251. ]);
  252. advanceClock();
  253. expect(languageMode.tokenizedLines[3].tokens[0].scopes).toEqual([
  254. 'source.js',
  255. 'comment.block.js'
  256. ]);
  257. expect(languageMode.tokenizedLines[4].tokens[0].scopes).toEqual([
  258. 'source.js',
  259. 'comment.block.js'
  260. ]);
  261. });
  262. });
  263. describe('when lines are both updated and inserted', () => {
  264. it('updates tokens to reflect the change', () => {
  265. buffer.setTextInRange(
  266. [[1, 0], [2, 0]],
  267. 'foo()\nbar()\nbaz()\nquux()'
  268. );
  269. // previous line 0 remains
  270. expect(languageMode.tokenizedLines[0].tokens[0]).toEqual({
  271. value: 'var',
  272. scopes: ['source.js', 'storage.type.var.js']
  273. });
  274. // 3 new lines inserted
  275. expect(languageMode.tokenizedLines[1].tokens[0]).toEqual({
  276. value: 'foo',
  277. scopes: [
  278. 'source.js',
  279. 'meta.function-call.js',
  280. 'entity.name.function.js'
  281. ]
  282. });
  283. expect(languageMode.tokenizedLines[2].tokens[0]).toEqual({
  284. value: 'bar',
  285. scopes: [
  286. 'source.js',
  287. 'meta.function-call.js',
  288. 'entity.name.function.js'
  289. ]
  290. });
  291. expect(languageMode.tokenizedLines[3].tokens[0]).toEqual({
  292. value: 'baz',
  293. scopes: [
  294. 'source.js',
  295. 'meta.function-call.js',
  296. 'entity.name.function.js'
  297. ]
  298. });
  299. // previous line 2 is joined with quux() on line 4
  300. expect(languageMode.tokenizedLines[4].tokens[0]).toEqual({
  301. value: 'quux',
  302. scopes: [
  303. 'source.js',
  304. 'meta.function-call.js',
  305. 'entity.name.function.js'
  306. ]
  307. });
  308. expect(languageMode.tokenizedLines[4].tokens[4]).toEqual({
  309. value: 'if',
  310. scopes: ['source.js', 'keyword.control.js']
  311. });
  312. // previous line 3 is pushed down to become line 5
  313. expect(languageMode.tokenizedLines[5].tokens[3]).toEqual({
  314. value: '=',
  315. scopes: ['source.js', 'keyword.operator.assignment.js']
  316. });
  317. });
  318. });
  319. describe('when the change invalidates the tokenization of subsequent lines', () => {
  320. it('schedules the invalidated lines to be tokenized in the background', () => {
  321. buffer.insert([5, 30], '/* */');
  322. buffer.insert([2, 0], '/*\nabcde\nabcder');
  323. expect(languageMode.tokenizedLines[2].tokens[0].scopes).toEqual([
  324. 'source.js',
  325. 'comment.block.js',
  326. 'punctuation.definition.comment.begin.js'
  327. ]);
  328. expect(languageMode.tokenizedLines[3].tokens[0].scopes).toEqual([
  329. 'source.js',
  330. 'comment.block.js'
  331. ]);
  332. expect(languageMode.tokenizedLines[4].tokens[0].scopes).toEqual([
  333. 'source.js',
  334. 'comment.block.js'
  335. ]);
  336. expect(languageMode.tokenizedLines[5].tokens[0].scopes).toEqual([
  337. 'source.js'
  338. ]);
  339. advanceClock(); // tokenize invalidated lines in background
  340. expect(languageMode.tokenizedLines[5].tokens[0].scopes).toEqual([
  341. 'source.js',
  342. 'comment.block.js'
  343. ]);
  344. expect(languageMode.tokenizedLines[6].tokens[0].scopes).toEqual([
  345. 'source.js',
  346. 'comment.block.js'
  347. ]);
  348. expect(languageMode.tokenizedLines[7].tokens[0].scopes).toEqual([
  349. 'source.js',
  350. 'comment.block.js'
  351. ]);
  352. expect(languageMode.tokenizedLines[8].tokens[0].scopes).not.toBe([
  353. 'source.js',
  354. 'comment.block.js'
  355. ]);
  356. });
  357. });
  358. });
  359. describe('when there is an insertion that is larger than the chunk size', () => {
  360. it('tokenizes the initial chunk synchronously, then tokenizes the remaining lines in the background', () => {
  361. const commentBlock = _.multiplyString(
  362. '// a comment\n',
  363. languageMode.chunkSize + 2
  364. );
  365. buffer.insert([0, 0], commentBlock);
  366. expect(
  367. languageMode.tokenizedLines[0].ruleStack != null
  368. ).toBeTruthy();
  369. expect(
  370. languageMode.tokenizedLines[4].ruleStack != null
  371. ).toBeTruthy();
  372. expect(languageMode.tokenizedLines[5]).toBeUndefined();
  373. advanceClock();
  374. expect(
  375. languageMode.tokenizedLines[5].ruleStack != null
  376. ).toBeTruthy();
  377. expect(
  378. languageMode.tokenizedLines[6].ruleStack != null
  379. ).toBeTruthy();
  380. });
  381. });
  382. });
  383. });
  384. describe('when the buffer contains hard-tabs', () => {
  385. beforeEach(async () => {
  386. atom.packages.activatePackage('language-coffee-script');
  387. buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee');
  388. languageMode = new TextMateLanguageMode({
  389. buffer,
  390. config,
  391. grammar: atom.grammars.grammarForScopeName('source.coffee')
  392. });
  393. languageMode.startTokenizing();
  394. });
  395. afterEach(() => {
  396. languageMode.destroy();
  397. buffer.release();
  398. });
  399. describe('when the buffer is fully tokenized', () => {
  400. beforeEach(() => fullyTokenize(languageMode));
  401. });
  402. });
  403. describe('when tokenization completes', () => {
  404. it('emits the `tokenized` event', async () => {
  405. const editor = await atom.workspace.open('sample.js');
  406. const tokenizedHandler = jasmine.createSpy('tokenized handler');
  407. editor.languageMode.onDidTokenize(tokenizedHandler);
  408. fullyTokenize(editor.getBuffer().getLanguageMode());
  409. expect(tokenizedHandler.callCount).toBe(1);
  410. });
  411. it("doesn't re-emit the `tokenized` event when it is re-tokenized", async () => {
  412. const editor = await atom.workspace.open('sample.js');
  413. fullyTokenize(editor.languageMode);
  414. const tokenizedHandler = jasmine.createSpy('tokenized handler');
  415. editor.languageMode.onDidTokenize(tokenizedHandler);
  416. editor.getBuffer().insert([0, 0], "'");
  417. fullyTokenize(editor.languageMode);
  418. expect(tokenizedHandler).not.toHaveBeenCalled();
  419. });
  420. });
  421. describe('when the grammar is updated because a grammar it includes is activated', async () => {
  422. it('re-emits the `tokenized` event', async () => {
  423. let tokenizationCount = 0;
  424. const editor = await atom.workspace.open('coffee.coffee');
  425. editor.onDidTokenize(() => {
  426. tokenizationCount++;
  427. });
  428. fullyTokenize(editor.getBuffer().getLanguageMode());
  429. tokenizationCount = 0;
  430. await atom.packages.activatePackage('language-coffee-script');
  431. fullyTokenize(editor.getBuffer().getLanguageMode());
  432. expect(tokenizationCount).toBe(1);
  433. });
  434. it('retokenizes the buffer', async () => {
  435. await atom.packages.activatePackage('language-ruby-on-rails');
  436. await atom.packages.activatePackage('language-ruby');
  437. buffer = atom.project.bufferForPathSync();
  438. buffer.setText("<div class='name'><%= User.find(2).full_name %></div>");
  439. languageMode = new TextMateLanguageMode({
  440. buffer,
  441. config,
  442. grammar: atom.grammars.selectGrammar('test.erb')
  443. });
  444. fullyTokenize(languageMode);
  445. expect(languageMode.tokenizedLines[0].tokens[0]).toEqual({
  446. value: "<div class='name'>",
  447. scopes: ['text.html.ruby']
  448. });
  449. await atom.packages.activatePackage('language-html');
  450. fullyTokenize(languageMode);
  451. expect(languageMode.tokenizedLines[0].tokens[0]).toEqual({
  452. value: '<',
  453. scopes: [
  454. 'text.html.ruby',
  455. 'meta.tag.block.div.html',
  456. 'punctuation.definition.tag.begin.html'
  457. ]
  458. });
  459. });
  460. });
  461. describe('when the buffer is configured with the null grammar', () => {
  462. it('does not actually tokenize using the grammar', () => {
  463. spyOn(NullGrammar, 'tokenizeLine').andCallThrough();
  464. buffer = atom.project.bufferForPathSync(
  465. 'sample.will-use-the-null-grammar'
  466. );
  467. buffer.setText('a\nb\nc');
  468. languageMode = new TextMateLanguageMode({ buffer, config });
  469. const tokenizeCallback = jasmine.createSpy('onDidTokenize');
  470. languageMode.onDidTokenize(tokenizeCallback);
  471. expect(languageMode.tokenizedLines[0]).toBeUndefined();
  472. expect(languageMode.tokenizedLines[1]).toBeUndefined();
  473. expect(languageMode.tokenizedLines[2]).toBeUndefined();
  474. expect(tokenizeCallback.callCount).toBe(0);
  475. expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled();
  476. fullyTokenize(languageMode);
  477. expect(languageMode.tokenizedLines[0]).toBeUndefined();
  478. expect(languageMode.tokenizedLines[1]).toBeUndefined();
  479. expect(languageMode.tokenizedLines[2]).toBeUndefined();
  480. expect(tokenizeCallback.callCount).toBe(0);
  481. expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled();
  482. });
  483. });
  484. });
  485. describe('.tokenForPosition(position)', () => {
  486. afterEach(() => {
  487. languageMode.destroy();
  488. buffer.release();
  489. });
  490. it('returns the correct token (regression)', () => {
  491. buffer = atom.project.bufferForPathSync('sample.js');
  492. languageMode = new TextMateLanguageMode({
  493. buffer,
  494. config,
  495. grammar: atom.grammars.grammarForScopeName('source.js')
  496. });
  497. fullyTokenize(languageMode);
  498. expect(languageMode.tokenForPosition([1, 0]).scopes).toEqual([
  499. 'source.js'
  500. ]);
  501. expect(languageMode.tokenForPosition([1, 1]).scopes).toEqual([
  502. 'source.js'
  503. ]);
  504. expect(languageMode.tokenForPosition([1, 2]).scopes).toEqual([
  505. 'source.js',
  506. 'storage.type.var.js'
  507. ]);
  508. });
  509. });
  510. describe('.bufferRangeForScopeAtPosition(selector, position)', () => {
  511. beforeEach(() => {
  512. buffer = atom.project.bufferForPathSync('sample.js');
  513. languageMode = new TextMateLanguageMode({
  514. buffer,
  515. config,
  516. grammar: atom.grammars.grammarForScopeName('source.js')
  517. });
  518. fullyTokenize(languageMode);
  519. });
  520. describe('when the selector does not match the token at the position', () =>
  521. it('returns a falsy value', () =>
  522. expect(
  523. languageMode.bufferRangeForScopeAtPosition('.bogus', [0, 1])
  524. ).toBeUndefined()));
  525. describe('when the selector matches a single token at the position', () => {
  526. it('returns the range covered by the token', () => {
  527. expect(
  528. languageMode.bufferRangeForScopeAtPosition('.storage.type.var.js', [
  529. 0,
  530. 1
  531. ])
  532. ).toEqual([[0, 0], [0, 3]]);
  533. expect(
  534. languageMode.bufferRangeForScopeAtPosition('.storage.type.var.js', [
  535. 0,
  536. 3
  537. ])
  538. ).toEqual([[0, 0], [0, 3]]);
  539. });
  540. });
  541. describe('when the selector matches a run of multiple tokens at the position', () => {
  542. it('returns the range covered by all contiguous tokens (within a single line)', () => {
  543. expect(
  544. languageMode.bufferRangeForScopeAtPosition('.function', [1, 18])
  545. ).toEqual([[1, 6], [1, 28]]);
  546. });
  547. });
  548. });
  549. describe('.tokenizedLineForRow(row)', () => {
  550. it("returns the tokenized line for a row, or a placeholder line if it hasn't been tokenized yet", () => {
  551. buffer = atom.project.bufferForPathSync('sample.js');
  552. const grammar = atom.grammars.grammarForScopeName('source.js');
  553. languageMode = new TextMateLanguageMode({ buffer, config, grammar });
  554. const line0 = buffer.lineForRow(0);
  555. const jsScopeStartId = grammar.startIdForScope(grammar.scopeName);
  556. const jsScopeEndId = grammar.endIdForScope(grammar.scopeName);
  557. languageMode.startTokenizing();
  558. expect(languageMode.tokenizedLines[0]).toBeUndefined();
  559. expect(languageMode.tokenizedLineForRow(0).text).toBe(line0);
  560. expect(languageMode.tokenizedLineForRow(0).tags).toEqual([
  561. jsScopeStartId,
  562. line0.length,
  563. jsScopeEndId
  564. ]);
  565. advanceClock(1);
  566. expect(languageMode.tokenizedLines[0]).not.toBeUndefined();
  567. expect(languageMode.tokenizedLineForRow(0).text).toBe(line0);
  568. expect(languageMode.tokenizedLineForRow(0).tags).not.toEqual([
  569. jsScopeStartId,
  570. line0.length,
  571. jsScopeEndId
  572. ]);
  573. });
  574. it('returns undefined if the requested row is outside the buffer range', () => {
  575. buffer = atom.project.bufferForPathSync('sample.js');
  576. const grammar = atom.grammars.grammarForScopeName('source.js');
  577. languageMode = new TextMateLanguageMode({ buffer, config, grammar });
  578. fullyTokenize(languageMode);
  579. expect(languageMode.tokenizedLineForRow(999)).toBeUndefined();
  580. });
  581. });
  582. describe('.buildHighlightIterator', () => {
  583. const { TextMateHighlightIterator } = TextMateLanguageMode;
  584. it('iterates over the syntactic scope boundaries', () => {
  585. buffer = new TextBuffer({ text: 'var foo = 1 /*\nhello*/var bar = 2\n' });
  586. languageMode = new TextMateLanguageMode({
  587. buffer,
  588. config,
  589. grammar: atom.grammars.grammarForScopeName('source.js')
  590. });
  591. fullyTokenize(languageMode);
  592. const iterator = languageMode.buildHighlightIterator();
  593. iterator.seek(Point(0, 0));
  594. const expectedBoundaries = [
  595. {
  596. position: Point(0, 0),
  597. closeTags: [],
  598. openTags: [
  599. 'syntax--source syntax--js',
  600. 'syntax--storage syntax--type syntax--var syntax--js'
  601. ]
  602. },
  603. {
  604. position: Point(0, 3),
  605. closeTags: ['syntax--storage syntax--type syntax--var syntax--js'],
  606. openTags: []
  607. },
  608. {
  609. position: Point(0, 8),
  610. closeTags: [],
  611. openTags: [
  612. 'syntax--keyword syntax--operator syntax--assignment syntax--js'
  613. ]
  614. },
  615. {
  616. position: Point(0, 9),
  617. closeTags: [
  618. 'syntax--keyword syntax--operator syntax--assignment syntax--js'
  619. ],
  620. openTags: []
  621. },
  622. {
  623. position: Point(0, 10),
  624. closeTags: [],
  625. openTags: [
  626. 'syntax--constant syntax--numeric syntax--decimal syntax--js'
  627. ]
  628. },
  629. {
  630. position: Point(0, 11),
  631. closeTags: [
  632. 'syntax--constant syntax--numeric syntax--decimal syntax--js'
  633. ],
  634. openTags: []
  635. },
  636. {
  637. position: Point(0, 12),
  638. closeTags: [],
  639. openTags: [
  640. 'syntax--comment syntax--block syntax--js',
  641. 'syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js'
  642. ]
  643. },
  644. {
  645. position: Point(0, 14),
  646. closeTags: [
  647. 'syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js'
  648. ],
  649. openTags: []
  650. },
  651. {
  652. position: Point(1, 5),
  653. closeTags: [],
  654. openTags: [
  655. 'syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js'
  656. ]
  657. },
  658. {
  659. position: Point(1, 7),
  660. closeTags: [
  661. 'syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js',
  662. 'syntax--comment syntax--block syntax--js'
  663. ],
  664. openTags: ['syntax--storage syntax--type syntax--var syntax--js']
  665. },
  666. {
  667. position: Point(1, 10),
  668. closeTags: ['syntax--storage syntax--type syntax--var syntax--js'],
  669. openTags: []
  670. },
  671. {
  672. position: Point(1, 15),
  673. closeTags: [],
  674. openTags: [
  675. 'syntax--keyword syntax--operator syntax--assignment syntax--js'
  676. ]
  677. },
  678. {
  679. position: Point(1, 16),
  680. closeTags: [
  681. 'syntax--keyword syntax--operator syntax--assignment syntax--js'
  682. ],
  683. openTags: []
  684. },
  685. {
  686. position: Point(1, 17),
  687. closeTags: [],
  688. openTags: [
  689. 'syntax--constant syntax--numeric syntax--decimal syntax--js'
  690. ]
  691. },
  692. {
  693. position: Point(1, 18),
  694. closeTags: [
  695. 'syntax--constant syntax--numeric syntax--decimal syntax--js'
  696. ],
  697. openTags: []
  698. }
  699. ];
  700. while (true) {
  701. const boundary = {
  702. position: iterator.getPosition(),
  703. closeTags: iterator
  704. .getCloseScopeIds()
  705. .map(scopeId => languageMode.classNameForScopeId(scopeId)),
  706. openTags: iterator
  707. .getOpenScopeIds()
  708. .map(scopeId => languageMode.classNameForScopeId(scopeId))
  709. };
  710. expect(boundary).toEqual(expectedBoundaries.shift());
  711. if (!iterator.moveToSuccessor()) {
  712. break;
  713. }
  714. }
  715. expect(
  716. iterator
  717. .seek(Point(0, 1))
  718. .map(scopeId => languageMode.classNameForScopeId(scopeId))
  719. ).toEqual([
  720. 'syntax--source syntax--js',
  721. 'syntax--storage syntax--type syntax--var syntax--js'
  722. ]);
  723. expect(iterator.getPosition()).toEqual(Point(0, 3));
  724. expect(
  725. iterator
  726. .seek(Point(0, 8))
  727. .map(scopeId => languageMode.classNameForScopeId(scopeId))
  728. ).toEqual(['syntax--source syntax--js']);
  729. expect(iterator.getPosition()).toEqual(Point(0, 8));
  730. expect(
  731. iterator
  732. .seek(Point(1, 0))
  733. .map(scopeId => languageMode.classNameForScopeId(scopeId))
  734. ).toEqual([
  735. 'syntax--source syntax--js',
  736. 'syntax--comment syntax--block syntax--js'
  737. ]);
  738. expect(iterator.getPosition()).toEqual(Point(1, 0));
  739. expect(
  740. iterator
  741. .seek(Point(1, 18))
  742. .map(scopeId => languageMode.classNameForScopeId(scopeId))
  743. ).toEqual([
  744. 'syntax--source syntax--js',
  745. 'syntax--constant syntax--numeric syntax--decimal syntax--js'
  746. ]);
  747. expect(iterator.getPosition()).toEqual(Point(1, 18));
  748. expect(
  749. iterator
  750. .seek(Point(2, 0))
  751. .map(scopeId => languageMode.classNameForScopeId(scopeId))
  752. ).toEqual(['syntax--source syntax--js']);
  753. iterator.moveToSuccessor();
  754. }); // ensure we don't infinitely loop (regression test)
  755. it('does not report columns beyond the length of the line', async () => {
  756. await atom.packages.activatePackage('language-coffee-script');
  757. buffer = new TextBuffer({ text: '# hello\n# world' });
  758. languageMode = new TextMateLanguageMode({
  759. buffer,
  760. config,
  761. grammar: atom.grammars.grammarForScopeName('source.coffee')
  762. });
  763. fullyTokenize(languageMode);
  764. const iterator = languageMode.buildHighlightIterator();
  765. iterator.seek(Point(0, 0));
  766. iterator.moveToSuccessor();
  767. iterator.moveToSuccessor();
  768. expect(iterator.getPosition().column).toBe(7);
  769. iterator.moveToSuccessor();
  770. expect(iterator.getPosition().column).toBe(0);
  771. iterator.seek(Point(0, 7));
  772. expect(iterator.getPosition().column).toBe(7);
  773. iterator.seek(Point(0, 8));
  774. expect(iterator.getPosition().column).toBe(7);
  775. });
  776. it('correctly terminates scopes at the beginning of the line (regression)', () => {
  777. const grammar = atom.grammars.createGrammar('test', {
  778. scopeName: 'text.broken',
  779. name: 'Broken grammar',
  780. patterns: [
  781. { begin: 'start', end: '(?=end)', name: 'blue.broken' },
  782. { match: '.', name: 'yellow.broken' }
  783. ]
  784. });
  785. buffer = new TextBuffer({ text: 'start x\nend x\nx' });
  786. languageMode = new TextMateLanguageMode({ buffer, config, grammar });
  787. fullyTokenize(languageMode);
  788. const iterator = languageMode.buildHighlightIterator();
  789. iterator.seek(Point(1, 0));
  790. expect(iterator.getPosition()).toEqual([1, 0]);
  791. expect(
  792. iterator
  793. .getCloseScopeIds()
  794. .map(scopeId => languageMode.classNameForScopeId(scopeId))
  795. ).toEqual(['syntax--blue syntax--broken']);
  796. expect(
  797. iterator
  798. .getOpenScopeIds()
  799. .map(scopeId => languageMode.classNameForScopeId(scopeId))
  800. ).toEqual(['syntax--yellow syntax--broken']);
  801. });
  802. describe('TextMateHighlightIterator.seek(position)', function() {
  803. it('seeks to the leftmost tag boundary greater than or equal to the given position and returns the containing tags', function() {
  804. const languageMode = {
  805. tokenizedLineForRow(row) {
  806. if (row === 0) {
  807. return {
  808. tags: [-1, -2, -3, -4, -5, 3, -3, -4, -6, -5, 4, -6, -3, -4],
  809. text: 'foo bar',
  810. openScopes: []
  811. };
  812. } else {
  813. return null;
  814. }
  815. }
  816. };
  817. const iterator = new TextMateHighlightIterator(languageMode);
  818. expect(iterator.seek(Point(0, 0))).toEqual([]);
  819. expect(iterator.getPosition()).toEqual(Point(0, 0));
  820. expect(iterator.getCloseScopeIds()).toEqual([]);
  821. expect(iterator.getOpenScopeIds()).toEqual([257]);
  822. iterator.moveToSuccessor();
  823. expect(iterator.getCloseScopeIds()).toEqual([257]);
  824. expect(iterator.getOpenScopeIds()).toEqual([259]);
  825. expect(iterator.seek(Point(0, 1))).toEqual([261]);
  826. expect(iterator.getPosition()).toEqual(Point(0, 3));
  827. expect(iterator.getCloseScopeIds()).toEqual([]);
  828. expect(iterator.getOpenScopeIds()).toEqual([259]);
  829. iterator.moveToSuccessor();
  830. expect(iterator.getPosition()).toEqual(Point(0, 3));
  831. expect(iterator.getCloseScopeIds()).toEqual([259, 261]);
  832. expect(iterator.getOpenScopeIds()).toEqual([261]);
  833. expect(iterator.seek(Point(0, 3))).toEqual([261]);
  834. expect(iterator.getPosition()).toEqual(Point(0, 3));
  835. expect(iterator.getCloseScopeIds()).toEqual([]);
  836. expect(iterator.getOpenScopeIds()).toEqual([259]);
  837. iterator.moveToSuccessor();
  838. expect(iterator.getPosition()).toEqual(Point(0, 3));
  839. expect(iterator.getCloseScopeIds()).toEqual([259, 261]);
  840. expect(iterator.getOpenScopeIds()).toEqual([261]);
  841. iterator.moveToSuccessor();
  842. expect(iterator.getPosition()).toEqual(Point(0, 7));
  843. expect(iterator.getCloseScopeIds()).toEqual([261]);
  844. expect(iterator.getOpenScopeIds()).toEqual([259]);
  845. iterator.moveToSuccessor();
  846. expect(iterator.getPosition()).toEqual(Point(0, 7));
  847. expect(iterator.getCloseScopeIds()).toEqual([259]);
  848. expect(iterator.getOpenScopeIds()).toEqual([]);
  849. iterator.moveToSuccessor();
  850. expect(iterator.getPosition()).toEqual(Point(1, 0));
  851. expect(iterator.getCloseScopeIds()).toEqual([]);
  852. expect(iterator.getOpenScopeIds()).toEqual([]);
  853. expect(iterator.seek(Point(0, 5))).toEqual([261]);
  854. expect(iterator.getPosition()).toEqual(Point(0, 7));
  855. expect(iterator.getCloseScopeIds()).toEqual([261]);
  856. expect(iterator.getOpenScopeIds()).toEqual([259]);
  857. iterator.moveToSuccessor();
  858. expect(iterator.getPosition()).toEqual(Point(0, 7));
  859. expect(iterator.getCloseScopeIds()).toEqual([259]);
  860. expect(iterator.getOpenScopeIds()).toEqual([]);
  861. });
  862. });
  863. describe('TextMateHighlightIterator.moveToSuccessor()', function() {
  864. it('reports two boundaries at the same position when tags close, open, then close again without a non-negative integer separating them (regression)', () => {
  865. const languageMode = {
  866. tokenizedLineForRow() {
  867. return {
  868. tags: [-1, -2, -1, -2],
  869. text: '',
  870. openScopes: []
  871. };
  872. }
  873. };
  874. const iterator = new TextMateHighlightIterator(languageMode);
  875. iterator.seek(Point(0, 0));
  876. expect(iterator.getPosition()).toEqual(Point(0, 0));
  877. expect(iterator.getCloseScopeIds()).toEqual([]);
  878. expect(iterator.getOpenScopeIds()).toEqual([257]);
  879. iterator.moveToSuccessor();
  880. expect(iterator.getPosition()).toEqual(Point(0, 0));
  881. expect(iterator.getCloseScopeIds()).toEqual([257]);
  882. expect(iterator.getOpenScopeIds()).toEqual([257]);
  883. iterator.moveToSuccessor();
  884. expect(iterator.getCloseScopeIds()).toEqual([257]);
  885. expect(iterator.getOpenScopeIds()).toEqual([]);
  886. });
  887. });
  888. });
  889. describe('.suggestedIndentForBufferRow', () => {
  890. let editor;
  891. describe('javascript', () => {
  892. beforeEach(async () => {
  893. editor = await atom.workspace.open('sample.js', { autoIndent: false });
  894. await atom.packages.activatePackage('language-javascript');
  895. });
  896. it('bases indentation off of the previous non-blank line', () => {
  897. expect(editor.suggestedIndentForBufferRow(0)).toBe(0);
  898. expect(editor.suggestedIndentForBufferRow(1)).toBe(1);
  899. expect(editor.suggestedIndentForBufferRow(2)).toBe(2);
  900. expect(editor.suggestedIndentForBufferRow(5)).toBe(3);
  901. expect(editor.suggestedIndentForBufferRow(7)).toBe(2);
  902. expect(editor.suggestedIndentForBufferRow(9)).toBe(1);
  903. expect(editor.suggestedIndentForBufferRow(11)).toBe(1);
  904. });
  905. it('does not take invisibles into account', () => {
  906. editor.update({ showInvisibles: true });
  907. expect(editor.suggestedIndentForBufferRow(0)).toBe(0);
  908. expect(editor.suggestedIndentForBufferRow(1)).toBe(1);
  909. expect(editor.suggestedIndentForBufferRow(2)).toBe(2);
  910. expect(editor.suggestedIndentForBufferRow(5)).toBe(3);
  911. expect(editor.suggestedIndentForBufferRow(7)).toBe(2);
  912. expect(editor.suggestedIndentForBufferRow(9)).toBe(1);
  913. expect(editor.suggestedIndentForBufferRow(11)).toBe(1);
  914. });
  915. });
  916. describe('css', () => {
  917. beforeEach(async () => {
  918. editor = await atom.workspace.open('css.css', { autoIndent: true });
  919. await atom.packages.activatePackage('language-source');
  920. await atom.packages.activatePackage('language-css');
  921. });
  922. it('does not return negative values (regression)', () => {
  923. editor.setText('.test {\npadding: 0;\n}');
  924. expect(editor.suggestedIndentForBufferRow(2)).toBe(0);
  925. });
  926. });
  927. });
  928. describe('.isFoldableAtRow(row)', () => {
  929. let editor;
  930. beforeEach(() => {
  931. buffer = atom.project.bufferForPathSync('sample.js');
  932. buffer.insert([10, 0], ' // multi-line\n // comment\n // block\n');
  933. buffer.insert([0, 0], '// multi-line\n// comment\n// block\n');
  934. languageMode = new TextMateLanguageMode({
  935. buffer,
  936. config,
  937. grammar: atom.grammars.grammarForScopeName('source.js')
  938. });
  939. buffer.setLanguageMode(languageMode);
  940. fullyTokenize(languageMode);
  941. });
  942. it('includes the first line of multi-line comments', () => {
  943. expect(languageMode.isFoldableAtRow(0)).toBe(true);
  944. expect(languageMode.isFoldableAtRow(1)).toBe(false);
  945. expect(languageMode.isFoldableAtRow(2)).toBe(false);
  946. expect(languageMode.isFoldableAtRow(3)).toBe(true); // because of indent
  947. expect(languageMode.isFoldableAtRow(13)).toBe(true);
  948. expect(languageMode.isFoldableAtRow(14)).toBe(false);
  949. expect(languageMode.isFoldableAtRow(15)).toBe(false);
  950. expect(languageMode.isFoldableAtRow(16)).toBe(false);
  951. buffer.insert([0, Infinity], '\n');
  952. expect(languageMode.isFoldableAtRow(0)).toBe(false);
  953. expect(languageMode.isFoldableAtRow(1)).toBe(false);
  954. expect(languageMode.isFoldableAtRow(2)).toBe(true);
  955. expect(languageMode.isFoldableAtRow(3)).toBe(false);
  956. buffer.undo();
  957. expect(languageMode.isFoldableAtRow(0)).toBe(true);
  958. expect(languageMode.isFoldableAtRow(1)).toBe(false);
  959. expect(languageMode.isFoldableAtRow(2)).toBe(false);
  960. expect(languageMode.isFoldableAtRow(3)).toBe(true);
  961. }); // because of indent
  962. it('includes non-comment lines that precede an increase in indentation', () => {
  963. buffer.insert([2, 0], ' '); // commented lines preceding an indent aren't foldable
  964. expect(languageMode.isFoldableAtRow(1)).toBe(false);
  965. expect(languageMode.isFoldableAtRow(2)).toBe(false);
  966. expect(languageMode.isFoldableAtRow(3)).toBe(true);
  967. expect(languageMode.isFoldableAtRow(4)).toBe(true);
  968. expect(languageMode.isFoldableAtRow(5)).toBe(false);
  969. expect(languageMode.isFoldableAtRow(6)).toBe(false);
  970. expect(languageMode.isFoldableAtRow(7)).toBe(true);
  971. expect(languageMode.isFoldableAtRow(8)).toBe(false);
  972. buffer.insert([7, 0], ' ');
  973. expect(languageMode.isFoldableAtRow(6)).toBe(true);
  974. expect(languageMode.isFoldableAtRow(7)).toBe(false);
  975. expect(languageMode.isFoldableAtRow(8)).toBe(false);
  976. buffer.undo();
  977. expect(languageMode.isFoldableAtRow(6)).toBe(false);
  978. expect(languageMode.isFoldableAtRow(7)).toBe(true);
  979. expect(languageMode.isFoldableAtRow(8)).toBe(false);
  980. buffer.insert([7, 0], ' \n x\n');
  981. expect(languageMode.isFoldableAtRow(6)).toBe(true);
  982. expect(languageMode.isFoldableAtRow(7)).toBe(false);
  983. expect(languageMode.isFoldableAtRow(8)).toBe(false);
  984. buffer.insert([9, 0], ' ');
  985. expect(languageMode.isFoldableAtRow(6)).toBe(true);
  986. expect(languageMode.isFoldableAtRow(7)).toBe(false);
  987. expect(languageMode.isFoldableAtRow(8)).toBe(false);
  988. });
  989. it('returns true if the line starts a multi-line comment', async () => {
  990. editor = await atom.workspace.open('sample-with-comments.js');
  991. fullyTokenize(editor.getBuffer().getLanguageMode());
  992. expect(editor.isFoldableAtBufferRow(1)).toBe(true);
  993. expect(editor.isFoldableAtBufferRow(6)).toBe(true);
  994. expect(editor.isFoldableAtBufferRow(8)).toBe(false);
  995. expect(editor.isFoldableAtBufferRow(11)).toBe(true);
  996. expect(editor.isFoldableAtBufferRow(15)).toBe(false);
  997. expect(editor.isFoldableAtBufferRow(17)).toBe(true);
  998. expect(editor.isFoldableAtBufferRow(21)).toBe(true);
  999. expect(editor.isFoldableAtBufferRow(24)).toBe(true);
  1000. expect(editor.isFoldableAtBufferRow(28)).toBe(false);
  1001. });
  1002. it('returns true for lines that end with a comment and are followed by an indented line', async () => {
  1003. editor = await atom.workspace.open('sample-with-comments.js');
  1004. expect(editor.isFoldableAtBufferRow(5)).toBe(true);
  1005. });
  1006. it("does not return true for a line in the middle of a comment that's followed by an indented line", async () => {
  1007. editor = await atom.workspace.open('sample-with-comments.js');
  1008. fullyTokenize(editor.getBuffer().getLanguageMode());
  1009. expect(editor.isFoldableAtBufferRow(7)).toBe(false);
  1010. editor.buffer.insert([8, 0], ' ');
  1011. expect(editor.isFoldableAtBufferRow(7)).toBe(false);
  1012. });
  1013. });
  1014. describe('.getFoldableRangesAtIndentLevel', () => {
  1015. let editor;
  1016. it('returns the ranges that can be folded at the given indent level', () => {
  1017. buffer = new TextBuffer(dedent`
  1018. if (a) {
  1019. b();
  1020. if (c) {
  1021. d()
  1022. if (e) {
  1023. f()
  1024. }
  1025. g()
  1026. }
  1027. h()
  1028. }
  1029. i()
  1030. if (j) {
  1031. k()
  1032. }
  1033. `);
  1034. languageMode = new TextMateLanguageMode({ buffer, config });
  1035. expect(simulateFold(languageMode.getFoldableRangesAtIndentLevel(0, 2)))
  1036. .toBe(dedent`
  1037. if (a) {⋯
  1038. }
  1039. i()
  1040. if (j) {⋯
  1041. }
  1042. `);
  1043. expect(simulateFold(languageMode.getFoldableRangesAtIndentLevel(1, 2)))
  1044. .toBe(dedent`
  1045. if (a) {
  1046. b();
  1047. if (c) {⋯
  1048. }
  1049. h()
  1050. }
  1051. i()
  1052. if (j) {
  1053. k()
  1054. }
  1055. `);
  1056. expect(simulateFold(languageMode.getFoldableRangesAtIndentLevel(2, 2)))
  1057. .toBe(dedent`
  1058. if (a) {
  1059. b();
  1060. if (c) {
  1061. d()
  1062. if (e) {⋯
  1063. }
  1064. g()
  1065. }
  1066. h()
  1067. }
  1068. i()
  1069. if (j) {
  1070. k()
  1071. }
  1072. `);
  1073. });
  1074. it('folds every foldable range at a given indentLevel', async () => {
  1075. editor = await atom.workspace.open('sample-with-comments.js');
  1076. fullyTokenize(editor.getBuffer().getLanguageMode());
  1077. editor.foldAllAtIndentLevel(2);
  1078. const folds = editor.unfoldAll();
  1079. expect(folds.length).toBe(5);
  1080. expect([folds[0].start.row, folds[0].end.row]).toEqual([6, 8]);
  1081. expect([folds[1].start.row, folds[1].end.row]).toEqual([11, 16]);
  1082. expect([folds[2].start.row, folds[2].end.row]).toEqual([17, 20]);
  1083. expect([folds[3].start.row, folds[3].end.row]).toEqual([21, 22]);
  1084. expect([folds[4].start.row, folds[4].end.row]).toEqual([24, 25]);
  1085. });
  1086. });
  1087. describe('.getFoldableRanges', () => {
  1088. it('returns the ranges that can be folded', () => {
  1089. buffer = new TextBuffer(dedent`
  1090. if (a) {
  1091. b();
  1092. if (c) {
  1093. d()
  1094. if (e) {
  1095. f()
  1096. }
  1097. g()
  1098. }
  1099. h()
  1100. }
  1101. i()
  1102. if (j) {
  1103. k()
  1104. }
  1105. `);
  1106. languageMode = new TextMateLanguageMode({ buffer, config });
  1107. expect(languageMode.getFoldableRanges(2).map(r => r.toString())).toEqual(
  1108. [
  1109. ...languageMode.getFoldableRangesAtIndentLevel(0, 2),
  1110. ...languageMode.getFoldableRangesAtIndentLevel(1, 2),
  1111. ...languageMode.getFoldableRangesAtIndentLevel(2, 2)
  1112. ]
  1113. .sort((a, b) => a.start.row - b.start.row || a.end.row - b.end.row)
  1114. .map(r => r.toString())
  1115. );
  1116. });
  1117. it('works with multi-line comments', async () => {
  1118. await atom.packages.activatePackage('language-javascript');
  1119. const editor = await atom.workspace.open('sample-with-comments.js', {
  1120. autoIndent: false
  1121. });
  1122. fullyTokenize(editor.getBuffer().getLanguageMode());
  1123. editor.foldAll();
  1124. const folds = editor.unfoldAll();
  1125. expect(folds.length).toBe(8);
  1126. expect([folds[0].start.row, folds[0].end.row]).toEqual([0, 30]);
  1127. expect([folds[1].start.row, folds[1].end.row]).toEqual([1, 4]);
  1128. expect([folds[2].start.row, folds[2].end.row]).toEqual([5, 27]);
  1129. expect([folds[3].start.row, folds[3].end.row]).toEqual([6, 8]);
  1130. expect([folds[4].start.row, folds[4].end.row]).toEqual([11, 16]);
  1131. expect([folds[5].start.row, folds[5].end.row]).toEqual([17, 20]);
  1132. expect([folds[6].start.row, folds[6].end.row]).toEqual([21, 22]);
  1133. expect([folds[7].start.row, folds[7].end.row]).toEqual([24, 25]);
  1134. });
  1135. });
  1136. describe('.getFoldableRangeContainingPoint', () => {
  1137. it('returns the range for the smallest fold that contains the given range', () => {
  1138. buffer = new TextBuffer(dedent`
  1139. if (a) {
  1140. b();
  1141. if (c) {
  1142. d()
  1143. if (e) {
  1144. f()
  1145. }
  1146. g()
  1147. }
  1148. h()
  1149. }
  1150. i()
  1151. if (j) {
  1152. k()
  1153. }
  1154. `);
  1155. languageMode = new TextMateLanguageMode({ buffer, config });
  1156. expect(
  1157. languageMode.getFoldableRangeContainingPoint(Point(0, 5), 2)
  1158. ).toBeNull();
  1159. let range = languageMode.getFoldableRangeContainingPoint(Point(0, 10), 2);
  1160. expect(simulateFold([range])).toBe(dedent`
  1161. if (a) {⋯
  1162. }
  1163. i()
  1164. if (j) {
  1165. k()
  1166. }
  1167. `);
  1168. range = languageMode.getFoldableRangeContainingPoint(Point(7, 0), 2);
  1169. expect(simulateFold([range])).toBe(dedent`
  1170. if (a) {
  1171. b();
  1172. if (c) {⋯
  1173. }
  1174. h()
  1175. }
  1176. i()
  1177. if (j) {
  1178. k()
  1179. }
  1180. `);
  1181. range = languageMode.getFoldableRangeContainingPoint(
  1182. Point(1, Infinity),
  1183. 2
  1184. );
  1185. expect(simulateFold([range])).toBe(dedent`
  1186. if (a) {⋯
  1187. }
  1188. i()
  1189. if (j) {
  1190. k()
  1191. }
  1192. `);
  1193. range = languageMode.getFoldableRangeContainingPoint(Point(2, 20), 2);
  1194. expect(simulateFold([range])).toBe(dedent`
  1195. if (a) {
  1196. b();
  1197. if (c) {⋯
  1198. }
  1199. h()
  1200. }
  1201. i()
  1202. if (j) {
  1203. k()
  1204. }
  1205. `);
  1206. });
  1207. it('works for coffee-script', async () => {
  1208. const editor = await atom.workspace.open('coffee.coffee');
  1209. await atom.packages.activatePackage('language-coffee-script');
  1210. buffer = editor.buffer;
  1211. languageMode = editor.languageMode;
  1212. expect(
  1213. languageMode.getFoldableRangeContainingPoint(Point(0, Infinity), 2)
  1214. ).toEqual([[0, Infinity], [20, Infinity]]);
  1215. expect(
  1216. languageMode.getFoldableRangeContainingPoint(Point(1, Infinity), 2)
  1217. ).toEqual([[1, Infinity], [17, Infinity]]);
  1218. expect(
  1219. languageMode.getFoldableRangeContainingPoint(Point(2, Infinity), 2)
  1220. ).toEqual([[1, Infinity], [17, Infinity]]);
  1221. expect(
  1222. languageMode.getFoldableRangeContainingPoint(Point(19, Infinity), 2)
  1223. ).toEqual([[19, Infinity], [20, Infinity]]);
  1224. });
  1225. it('works for javascript', async () => {
  1226. const editor = await atom.workspace.open('sample.js');
  1227. await atom.packages.activatePackage('language-javascript');
  1228. buffer = editor.buffer;
  1229. languageMode = editor.languageMode;
  1230. expect(
  1231. editor.languageMode.getFoldableRangeContainingPoint(
  1232. Point(0, Infinity),
  1233. 2
  1234. )
  1235. ).toEqual([[0, Infinity], [12, Infinity]]);
  1236. expect(
  1237. editor.languageMode.getFoldableRangeContainingPoint(
  1238. Point(1, Infinity),
  1239. 2
  1240. )
  1241. ).toEqual([[1, Infinity], [9, Infinity]]);
  1242. expect(
  1243. editor.languageMode.getFoldableRangeContainingPoint(
  1244. Point(2, Infinity),
  1245. 2
  1246. )
  1247. ).toEqual([[1, Infinity], [9, Infinity]]);
  1248. expect(
  1249. editor.languageMode.getFoldableRangeContainingPoint(
  1250. Point(4, Infinity),
  1251. 2
  1252. )
  1253. ).toEqual([[4, Infinity], [7, Infinity]]);
  1254. });
  1255. it('searches upward and downward for surrounding comment lines and folds them as a single fold', async () => {
  1256. await atom.packages.activatePackage('language-javascript');
  1257. const editor = await atom.workspace.open('sample-with-comments.js');
  1258. editor.buffer.insert(
  1259. [1, 0],
  1260. ' //this is a comment\n // and\n //more docs\n\n//second comment'
  1261. );
  1262. fullyTokenize(editor.getBuffer().getLanguageMode());
  1263. editor.foldBufferRow(1);
  1264. const [fold] = editor.unfoldAll();
  1265. expect([fold.start.row, fold.end.row]).toEqual([1, 3]);
  1266. });
  1267. });
  1268. describe('TokenIterator', () =>
  1269. it('correctly terminates scopes at the beginning of the line (regression)', () => {
  1270. const grammar = atom.grammars.createGrammar('test', {
  1271. scopeName: 'text.broken',
  1272. name: 'Broken grammar',
  1273. patterns: [
  1274. {
  1275. begin: 'start',
  1276. end: '(?=end)',
  1277. name: 'blue.broken'
  1278. },
  1279. {
  1280. match: '.',
  1281. name: 'yellow.broken'
  1282. }
  1283. ]
  1284. });
  1285. const buffer = new TextBuffer({
  1286. text: dedent`
  1287. start x
  1288. end x
  1289. x
  1290. `
  1291. });
  1292. const languageMode = new TextMateLanguageMode({
  1293. buffer,
  1294. grammar,
  1295. config: atom.config,
  1296. grammarRegistry: atom.grammars,
  1297. packageManager: atom.packages,
  1298. assert: atom.assert
  1299. });
  1300. fullyTokenize(languageMode);
  1301. const tokenIterator = languageMode
  1302. .tokenizedLineForRow(1)
  1303. .getTokenIterator();
  1304. tokenIterator.next();
  1305. expect(tokenIterator.getBufferStart()).toBe(0);
  1306. expect(tokenIterator.getScopeEnds()).toEqual([]);
  1307. expect(tokenIterator.getScopeStarts()).toEqual([
  1308. 'text.broken',
  1309. 'yellow.broken'
  1310. ]);
  1311. }));
  1312. function simulateFold(ranges) {
  1313. buffer.transact(() => {
  1314. for (const range of ranges.reverse()) {
  1315. buffer.setTextInRange(range, '⋯');
  1316. }
  1317. });
  1318. let text = buffer.getText();
  1319. buffer.undo();
  1320. return text;
  1321. }
  1322. function fullyTokenize(languageMode) {
  1323. languageMode.startTokenizing();
  1324. while (languageMode.firstInvalidRow() != null) {
  1325. advanceClock();
  1326. }
  1327. }
  1328. });