8dd8b72300551a4d892e936ea051f094f75ba44adbc621961435e10bcae31f20f9496b8dfe2661c933e074169254fc0122872893b11106d38eb487dfdb5188 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. import { Emitter } from '../../../../base/common/event.js';
  6. import { FoldingRegions } from './foldingRanges.js';
  7. import { hash } from '../../../../base/common/hash.js';
  8. export class FoldingModel {
  9. constructor(textModel, decorationProvider) {
  10. this._updateEventEmitter = new Emitter();
  11. this.onDidChange = this._updateEventEmitter.event;
  12. this._textModel = textModel;
  13. this._decorationProvider = decorationProvider;
  14. this._regions = new FoldingRegions(new Uint32Array(0), new Uint32Array(0));
  15. this._editorDecorationIds = [];
  16. }
  17. get regions() { return this._regions; }
  18. get textModel() { return this._textModel; }
  19. toggleCollapseState(toggledRegions) {
  20. if (!toggledRegions.length) {
  21. return;
  22. }
  23. toggledRegions = toggledRegions.sort((r1, r2) => r1.regionIndex - r2.regionIndex);
  24. const processed = {};
  25. this._decorationProvider.changeDecorations(accessor => {
  26. let k = 0; // index from [0 ... this.regions.length]
  27. let dirtyRegionEndLine = -1; // end of the range where decorations need to be updated
  28. let lastHiddenLine = -1; // the end of the last hidden lines
  29. const updateDecorationsUntil = (index) => {
  30. while (k < index) {
  31. const endLineNumber = this._regions.getEndLineNumber(k);
  32. const isCollapsed = this._regions.isCollapsed(k);
  33. if (endLineNumber <= dirtyRegionEndLine) {
  34. const isManual = this.regions.getSource(k) !== 0 /* FoldSource.provider */;
  35. accessor.changeDecorationOptions(this._editorDecorationIds[k], this._decorationProvider.getDecorationOption(isCollapsed, endLineNumber <= lastHiddenLine, isManual));
  36. }
  37. if (isCollapsed && endLineNumber > lastHiddenLine) {
  38. lastHiddenLine = endLineNumber;
  39. }
  40. k++;
  41. }
  42. };
  43. for (const region of toggledRegions) {
  44. const index = region.regionIndex;
  45. const editorDecorationId = this._editorDecorationIds[index];
  46. if (editorDecorationId && !processed[editorDecorationId]) {
  47. processed[editorDecorationId] = true;
  48. updateDecorationsUntil(index); // update all decorations up to current index using the old dirtyRegionEndLine
  49. const newCollapseState = !this._regions.isCollapsed(index);
  50. this._regions.setCollapsed(index, newCollapseState);
  51. dirtyRegionEndLine = Math.max(dirtyRegionEndLine, this._regions.getEndLineNumber(index));
  52. }
  53. }
  54. updateDecorationsUntil(this._regions.length);
  55. });
  56. this._updateEventEmitter.fire({ model: this, collapseStateChanged: toggledRegions });
  57. }
  58. removeManualRanges(ranges) {
  59. const newFoldingRanges = new Array();
  60. const intersects = (foldRange) => {
  61. for (const range of ranges) {
  62. if (!(range.startLineNumber > foldRange.endLineNumber || foldRange.startLineNumber > range.endLineNumber)) {
  63. return true;
  64. }
  65. }
  66. return false;
  67. };
  68. for (let i = 0; i < this._regions.length; i++) {
  69. const foldRange = this._regions.toFoldRange(i);
  70. if (foldRange.source === 0 /* FoldSource.provider */ || !intersects(foldRange)) {
  71. newFoldingRanges.push(foldRange);
  72. }
  73. }
  74. this.updatePost(FoldingRegions.fromFoldRanges(newFoldingRanges));
  75. }
  76. update(newRegions, blockedLineNumers = []) {
  77. const foldedOrManualRanges = this._currentFoldedOrManualRanges(blockedLineNumers);
  78. const newRanges = FoldingRegions.sanitizeAndMerge(newRegions, foldedOrManualRanges, this._textModel.getLineCount());
  79. this.updatePost(FoldingRegions.fromFoldRanges(newRanges));
  80. }
  81. updatePost(newRegions) {
  82. const newEditorDecorations = [];
  83. let lastHiddenLine = -1;
  84. for (let index = 0, limit = newRegions.length; index < limit; index++) {
  85. const startLineNumber = newRegions.getStartLineNumber(index);
  86. const endLineNumber = newRegions.getEndLineNumber(index);
  87. const isCollapsed = newRegions.isCollapsed(index);
  88. const isManual = newRegions.getSource(index) !== 0 /* FoldSource.provider */;
  89. const decorationRange = {
  90. startLineNumber: startLineNumber,
  91. startColumn: this._textModel.getLineMaxColumn(startLineNumber),
  92. endLineNumber: endLineNumber,
  93. endColumn: this._textModel.getLineMaxColumn(endLineNumber) + 1
  94. };
  95. newEditorDecorations.push({ range: decorationRange, options: this._decorationProvider.getDecorationOption(isCollapsed, endLineNumber <= lastHiddenLine, isManual) });
  96. if (isCollapsed && endLineNumber > lastHiddenLine) {
  97. lastHiddenLine = endLineNumber;
  98. }
  99. }
  100. this._decorationProvider.changeDecorations(accessor => this._editorDecorationIds = accessor.deltaDecorations(this._editorDecorationIds, newEditorDecorations));
  101. this._regions = newRegions;
  102. this._updateEventEmitter.fire({ model: this });
  103. }
  104. _currentFoldedOrManualRanges(blockedLineNumers = []) {
  105. const isBlocked = (startLineNumber, endLineNumber) => {
  106. for (const blockedLineNumber of blockedLineNumers) {
  107. if (startLineNumber < blockedLineNumber && blockedLineNumber <= endLineNumber) { // first line is visible
  108. return true;
  109. }
  110. }
  111. return false;
  112. };
  113. const foldedRanges = [];
  114. for (let i = 0, limit = this._regions.length; i < limit; i++) {
  115. let isCollapsed = this.regions.isCollapsed(i);
  116. const source = this.regions.getSource(i);
  117. if (isCollapsed || source !== 0 /* FoldSource.provider */) {
  118. const foldRange = this._regions.toFoldRange(i);
  119. const decRange = this._textModel.getDecorationRange(this._editorDecorationIds[i]);
  120. if (decRange) {
  121. if (isCollapsed && (isBlocked(decRange.startLineNumber, decRange.endLineNumber) || decRange.endLineNumber - decRange.startLineNumber !== foldRange.endLineNumber - foldRange.startLineNumber)) {
  122. isCollapsed = false; // uncollapse is the range is blocked or there has been lines removed or added
  123. }
  124. foldedRanges.push({
  125. startLineNumber: decRange.startLineNumber,
  126. endLineNumber: decRange.endLineNumber,
  127. type: foldRange.type,
  128. isCollapsed,
  129. source
  130. });
  131. }
  132. }
  133. }
  134. return foldedRanges;
  135. }
  136. /**
  137. * Collapse state memento, for persistence only
  138. */
  139. getMemento() {
  140. const foldedOrManualRanges = this._currentFoldedOrManualRanges();
  141. const result = [];
  142. for (let i = 0, limit = foldedOrManualRanges.length; i < limit; i++) {
  143. const range = foldedOrManualRanges[i];
  144. const checksum = this._getLinesChecksum(range.startLineNumber + 1, range.endLineNumber);
  145. result.push({
  146. startLineNumber: range.startLineNumber,
  147. endLineNumber: range.endLineNumber,
  148. isCollapsed: range.isCollapsed,
  149. source: range.source,
  150. checksum: checksum
  151. });
  152. }
  153. return (result.length > 0) ? result : undefined;
  154. }
  155. /**
  156. * Apply persisted state, for persistence only
  157. */
  158. applyMemento(state) {
  159. var _a, _b;
  160. if (!Array.isArray(state)) {
  161. return;
  162. }
  163. const rangesToRestore = [];
  164. const maxLineNumber = this._textModel.getLineCount();
  165. for (const range of state) {
  166. if (range.startLineNumber >= range.endLineNumber || range.startLineNumber < 1 || range.endLineNumber > maxLineNumber) {
  167. continue;
  168. }
  169. const checksum = this._getLinesChecksum(range.startLineNumber + 1, range.endLineNumber);
  170. if (!range.checksum || checksum === range.checksum) {
  171. rangesToRestore.push({
  172. startLineNumber: range.startLineNumber,
  173. endLineNumber: range.endLineNumber,
  174. type: undefined,
  175. isCollapsed: (_a = range.isCollapsed) !== null && _a !== void 0 ? _a : true,
  176. source: (_b = range.source) !== null && _b !== void 0 ? _b : 0 /* FoldSource.provider */
  177. });
  178. }
  179. }
  180. const newRanges = FoldingRegions.sanitizeAndMerge(this._regions, rangesToRestore, maxLineNumber);
  181. this.updatePost(FoldingRegions.fromFoldRanges(newRanges));
  182. }
  183. _getLinesChecksum(lineNumber1, lineNumber2) {
  184. const h = hash(this._textModel.getLineContent(lineNumber1)
  185. + this._textModel.getLineContent(lineNumber2));
  186. return h % 1000000; // 6 digits is plenty
  187. }
  188. dispose() {
  189. this._decorationProvider.removeDecorations(this._editorDecorationIds);
  190. }
  191. getAllRegionsAtLine(lineNumber, filter) {
  192. const result = [];
  193. if (this._regions) {
  194. let index = this._regions.findRange(lineNumber);
  195. let level = 1;
  196. while (index >= 0) {
  197. const current = this._regions.toRegion(index);
  198. if (!filter || filter(current, level)) {
  199. result.push(current);
  200. }
  201. level++;
  202. index = current.parentIndex;
  203. }
  204. }
  205. return result;
  206. }
  207. getRegionAtLine(lineNumber) {
  208. if (this._regions) {
  209. const index = this._regions.findRange(lineNumber);
  210. if (index >= 0) {
  211. return this._regions.toRegion(index);
  212. }
  213. }
  214. return null;
  215. }
  216. getRegionsInside(region, filter) {
  217. const result = [];
  218. const index = region ? region.regionIndex + 1 : 0;
  219. const endLineNumber = region ? region.endLineNumber : Number.MAX_VALUE;
  220. if (filter && filter.length === 2) {
  221. const levelStack = [];
  222. for (let i = index, len = this._regions.length; i < len; i++) {
  223. const current = this._regions.toRegion(i);
  224. if (this._regions.getStartLineNumber(i) < endLineNumber) {
  225. while (levelStack.length > 0 && !current.containedBy(levelStack[levelStack.length - 1])) {
  226. levelStack.pop();
  227. }
  228. levelStack.push(current);
  229. if (filter(current, levelStack.length)) {
  230. result.push(current);
  231. }
  232. }
  233. else {
  234. break;
  235. }
  236. }
  237. }
  238. else {
  239. for (let i = index, len = this._regions.length; i < len; i++) {
  240. const current = this._regions.toRegion(i);
  241. if (this._regions.getStartLineNumber(i) < endLineNumber) {
  242. if (!filter || filter(current)) {
  243. result.push(current);
  244. }
  245. }
  246. else {
  247. break;
  248. }
  249. }
  250. }
  251. return result;
  252. }
  253. }
  254. /**
  255. * Collapse or expand the regions at the given locations
  256. * @param levels The number of levels. Use 1 to only impact the regions at the location, use Number.MAX_VALUE for all levels.
  257. * @param lineNumbers the location of the regions to collapse or expand, or if not set, all regions in the model.
  258. */
  259. export function toggleCollapseState(foldingModel, levels, lineNumbers) {
  260. const toToggle = [];
  261. for (const lineNumber of lineNumbers) {
  262. const region = foldingModel.getRegionAtLine(lineNumber);
  263. if (region) {
  264. const doCollapse = !region.isCollapsed;
  265. toToggle.push(region);
  266. if (levels > 1) {
  267. const regionsInside = foldingModel.getRegionsInside(region, (r, level) => r.isCollapsed !== doCollapse && level < levels);
  268. toToggle.push(...regionsInside);
  269. }
  270. }
  271. }
  272. foldingModel.toggleCollapseState(toToggle);
  273. }
  274. /**
  275. * Collapse or expand the regions at the given locations including all children.
  276. * @param doCollapse Whether to collapse or expand
  277. * @param levels The number of levels. Use 1 to only impact the regions at the location, use Number.MAX_VALUE for all levels.
  278. * @param lineNumbers the location of the regions to collapse or expand, or if not set, all regions in the model.
  279. */
  280. export function setCollapseStateLevelsDown(foldingModel, doCollapse, levels = Number.MAX_VALUE, lineNumbers) {
  281. const toToggle = [];
  282. if (lineNumbers && lineNumbers.length > 0) {
  283. for (const lineNumber of lineNumbers) {
  284. const region = foldingModel.getRegionAtLine(lineNumber);
  285. if (region) {
  286. if (region.isCollapsed !== doCollapse) {
  287. toToggle.push(region);
  288. }
  289. if (levels > 1) {
  290. const regionsInside = foldingModel.getRegionsInside(region, (r, level) => r.isCollapsed !== doCollapse && level < levels);
  291. toToggle.push(...regionsInside);
  292. }
  293. }
  294. }
  295. }
  296. else {
  297. const regionsInside = foldingModel.getRegionsInside(null, (r, level) => r.isCollapsed !== doCollapse && level < levels);
  298. toToggle.push(...regionsInside);
  299. }
  300. foldingModel.toggleCollapseState(toToggle);
  301. }
  302. /**
  303. * Collapse or expand the regions at the given locations including all parents.
  304. * @param doCollapse Whether to collapse or expand
  305. * @param levels The number of levels. Use 1 to only impact the regions at the location, use Number.MAX_VALUE for all levels.
  306. * @param lineNumbers the location of the regions to collapse or expand.
  307. */
  308. export function setCollapseStateLevelsUp(foldingModel, doCollapse, levels, lineNumbers) {
  309. const toToggle = [];
  310. for (const lineNumber of lineNumbers) {
  311. const regions = foldingModel.getAllRegionsAtLine(lineNumber, (region, level) => region.isCollapsed !== doCollapse && level <= levels);
  312. toToggle.push(...regions);
  313. }
  314. foldingModel.toggleCollapseState(toToggle);
  315. }
  316. /**
  317. * Collapse or expand a region at the given locations. If the inner most region is already collapsed/expanded, uses the first parent instead.
  318. * @param doCollapse Whether to collapse or expand
  319. * @param lineNumbers the location of the regions to collapse or expand.
  320. */
  321. export function setCollapseStateUp(foldingModel, doCollapse, lineNumbers) {
  322. const toToggle = [];
  323. for (const lineNumber of lineNumbers) {
  324. const regions = foldingModel.getAllRegionsAtLine(lineNumber, (region) => region.isCollapsed !== doCollapse);
  325. if (regions.length > 0) {
  326. toToggle.push(regions[0]);
  327. }
  328. }
  329. foldingModel.toggleCollapseState(toToggle);
  330. }
  331. /**
  332. * Folds or unfolds all regions that have a given level, except if they contain one of the blocked lines.
  333. * @param foldLevel level. Level == 1 is the top level
  334. * @param doCollapse Whether to collapse or expand
  335. */
  336. export function setCollapseStateAtLevel(foldingModel, foldLevel, doCollapse, blockedLineNumbers) {
  337. const filter = (region, level) => level === foldLevel && region.isCollapsed !== doCollapse && !blockedLineNumbers.some(line => region.containsLine(line));
  338. const toToggle = foldingModel.getRegionsInside(null, filter);
  339. foldingModel.toggleCollapseState(toToggle);
  340. }
  341. /**
  342. * Folds or unfolds all regions, except if they contain or are contained by a region of one of the blocked lines.
  343. * @param doCollapse Whether to collapse or expand
  344. * @param blockedLineNumbers the location of regions to not collapse or expand
  345. */
  346. export function setCollapseStateForRest(foldingModel, doCollapse, blockedLineNumbers) {
  347. const filteredRegions = [];
  348. for (const lineNumber of blockedLineNumbers) {
  349. const regions = foldingModel.getAllRegionsAtLine(lineNumber, undefined);
  350. if (regions.length > 0) {
  351. filteredRegions.push(regions[0]);
  352. }
  353. }
  354. const filter = (region) => filteredRegions.every((filteredRegion) => !filteredRegion.containedBy(region) && !region.containedBy(filteredRegion)) && region.isCollapsed !== doCollapse;
  355. const toToggle = foldingModel.getRegionsInside(null, filter);
  356. foldingModel.toggleCollapseState(toToggle);
  357. }
  358. /**
  359. * Folds all regions for which the lines start with a given regex
  360. * @param foldingModel the folding model
  361. */
  362. export function setCollapseStateForMatchingLines(foldingModel, regExp, doCollapse) {
  363. const editorModel = foldingModel.textModel;
  364. const regions = foldingModel.regions;
  365. const toToggle = [];
  366. for (let i = regions.length - 1; i >= 0; i--) {
  367. if (doCollapse !== regions.isCollapsed(i)) {
  368. const startLineNumber = regions.getStartLineNumber(i);
  369. if (regExp.test(editorModel.getLineContent(startLineNumber))) {
  370. toToggle.push(regions.toRegion(i));
  371. }
  372. }
  373. }
  374. foldingModel.toggleCollapseState(toToggle);
  375. }
  376. /**
  377. * Folds all regions of the given type
  378. * @param foldingModel the folding model
  379. */
  380. export function setCollapseStateForType(foldingModel, type, doCollapse) {
  381. const regions = foldingModel.regions;
  382. const toToggle = [];
  383. for (let i = regions.length - 1; i >= 0; i--) {
  384. if (doCollapse !== regions.isCollapsed(i) && type === regions.getType(i)) {
  385. toToggle.push(regions.toRegion(i));
  386. }
  387. }
  388. foldingModel.toggleCollapseState(toToggle);
  389. }
  390. /**
  391. * Get line to go to for parent fold of current line
  392. * @param lineNumber the current line number
  393. * @param foldingModel the folding model
  394. *
  395. * @return Parent fold start line
  396. */
  397. export function getParentFoldLine(lineNumber, foldingModel) {
  398. let startLineNumber = null;
  399. const foldingRegion = foldingModel.getRegionAtLine(lineNumber);
  400. if (foldingRegion !== null) {
  401. startLineNumber = foldingRegion.startLineNumber;
  402. // If current line is not the start of the current fold, go to top line of current fold. If not, go to parent fold
  403. if (lineNumber === startLineNumber) {
  404. const parentFoldingIdx = foldingRegion.parentIndex;
  405. if (parentFoldingIdx !== -1) {
  406. startLineNumber = foldingModel.regions.getStartLineNumber(parentFoldingIdx);
  407. }
  408. else {
  409. startLineNumber = null;
  410. }
  411. }
  412. }
  413. return startLineNumber;
  414. }
  415. /**
  416. * Get line to go to for previous fold at the same level of current line
  417. * @param lineNumber the current line number
  418. * @param foldingModel the folding model
  419. *
  420. * @return Previous fold start line
  421. */
  422. export function getPreviousFoldLine(lineNumber, foldingModel) {
  423. let foldingRegion = foldingModel.getRegionAtLine(lineNumber);
  424. // If on the folding range start line, go to previous sibling.
  425. if (foldingRegion !== null && foldingRegion.startLineNumber === lineNumber) {
  426. // If current line is not the start of the current fold, go to top line of current fold. If not, go to previous fold.
  427. if (lineNumber !== foldingRegion.startLineNumber) {
  428. return foldingRegion.startLineNumber;
  429. }
  430. else {
  431. // Find min line number to stay within parent.
  432. const expectedParentIndex = foldingRegion.parentIndex;
  433. let minLineNumber = 0;
  434. if (expectedParentIndex !== -1) {
  435. minLineNumber = foldingModel.regions.getStartLineNumber(foldingRegion.parentIndex);
  436. }
  437. // Find fold at same level.
  438. while (foldingRegion !== null) {
  439. if (foldingRegion.regionIndex > 0) {
  440. foldingRegion = foldingModel.regions.toRegion(foldingRegion.regionIndex - 1);
  441. // Keep at same level.
  442. if (foldingRegion.startLineNumber <= minLineNumber) {
  443. return null;
  444. }
  445. else if (foldingRegion.parentIndex === expectedParentIndex) {
  446. return foldingRegion.startLineNumber;
  447. }
  448. }
  449. else {
  450. return null;
  451. }
  452. }
  453. }
  454. }
  455. else {
  456. // Go to last fold that's before the current line.
  457. if (foldingModel.regions.length > 0) {
  458. foldingRegion = foldingModel.regions.toRegion(foldingModel.regions.length - 1);
  459. while (foldingRegion !== null) {
  460. // Found fold before current line.
  461. if (foldingRegion.startLineNumber < lineNumber) {
  462. return foldingRegion.startLineNumber;
  463. }
  464. if (foldingRegion.regionIndex > 0) {
  465. foldingRegion = foldingModel.regions.toRegion(foldingRegion.regionIndex - 1);
  466. }
  467. else {
  468. foldingRegion = null;
  469. }
  470. }
  471. }
  472. }
  473. return null;
  474. }
  475. /**
  476. * Get line to go to next fold at the same level of current line
  477. * @param lineNumber the current line number
  478. * @param foldingModel the folding model
  479. *
  480. * @return Next fold start line
  481. */
  482. export function getNextFoldLine(lineNumber, foldingModel) {
  483. let foldingRegion = foldingModel.getRegionAtLine(lineNumber);
  484. // If on the folding range start line, go to next sibling.
  485. if (foldingRegion !== null && foldingRegion.startLineNumber === lineNumber) {
  486. // Find max line number to stay within parent.
  487. const expectedParentIndex = foldingRegion.parentIndex;
  488. let maxLineNumber = 0;
  489. if (expectedParentIndex !== -1) {
  490. maxLineNumber = foldingModel.regions.getEndLineNumber(foldingRegion.parentIndex);
  491. }
  492. else if (foldingModel.regions.length === 0) {
  493. return null;
  494. }
  495. else {
  496. maxLineNumber = foldingModel.regions.getEndLineNumber(foldingModel.regions.length - 1);
  497. }
  498. // Find fold at same level.
  499. while (foldingRegion !== null) {
  500. if (foldingRegion.regionIndex < foldingModel.regions.length) {
  501. foldingRegion = foldingModel.regions.toRegion(foldingRegion.regionIndex + 1);
  502. // Keep at same level.
  503. if (foldingRegion.startLineNumber >= maxLineNumber) {
  504. return null;
  505. }
  506. else if (foldingRegion.parentIndex === expectedParentIndex) {
  507. return foldingRegion.startLineNumber;
  508. }
  509. }
  510. else {
  511. return null;
  512. }
  513. }
  514. }
  515. else {
  516. // Go to first fold that's after the current line.
  517. if (foldingModel.regions.length > 0) {
  518. foldingRegion = foldingModel.regions.toRegion(0);
  519. while (foldingRegion !== null) {
  520. // Found fold after current line.
  521. if (foldingRegion.startLineNumber > lineNumber) {
  522. return foldingRegion.startLineNumber;
  523. }
  524. if (foldingRegion.regionIndex < foldingModel.regions.length) {
  525. foldingRegion = foldingModel.regions.toRegion(foldingRegion.regionIndex + 1);
  526. }
  527. else {
  528. foldingRegion = null;
  529. }
  530. }
  531. }
  532. }
  533. return null;
  534. }