diff --git a/__tests__/checkboxUtils.base.test.ts b/__tests__/checkboxUtils.base.test.ts new file mode 100644 index 0000000..c277cec --- /dev/null +++ b/__tests__/checkboxUtils.base.test.ts @@ -0,0 +1,932 @@ +import { CheckboxLineInfo, CheckboxUtils } from '../src/checkboxUtils'; // Путь к вашему файлу +import { CheckboxState, CheckboxSyncPluginSettings, DEFAULT_SETTINGS } from '../src/types'; // Путь к вашему файлу типов + +// Вспомогательная функция для создания настроек с переопределениями +const createSettings = (overrides: Partial = {}): Readonly => { + return { ...DEFAULT_SETTINGS, ...overrides } as Readonly; +}; + +describe('CheckboxUtils', () => { + let checkboxUtils: CheckboxUtils; + let settings: Readonly; + + beforeEach(() => { + // Используем настройки по умолчанию для большинства тестов + settings = createSettings(); + checkboxUtils = new CheckboxUtils(settings); + }); + + // --- Тесты для matchCheckboxLine --- + describe('matchCheckboxLine', () => { + it('should match standard unchecked checkbox', () => { + const line = '- [ ] Task'; + const result = checkboxUtils.matchCheckboxLine(line); + expect(result).not.toBeNull(); + expect(result).toEqual({ + indent: 0, + marker: '-', + checkChar: ' ', + checkboxCharPosition: 3, + checkboxState: CheckboxState.Unchecked, + isChecked: false, + listItemText: "Task", + }); + }); + + it('should match standard checked checkbox', () => { + const line = '* [x] Done'; + const result = checkboxUtils.matchCheckboxLine(line); + expect(result).not.toBeNull(); + expect(result).toEqual({ + indent: 0, + marker: '*', + checkChar: 'x', + checkboxCharPosition: 3, + checkboxState: CheckboxState.Checked, + isChecked: true, + listItemText: "Done", + }); + }); + + it('should match checkbox with indentation', () => { + const line = ' + [x] Indented Task'; + const result = checkboxUtils.matchCheckboxLine(line); + expect(result).not.toBeNull(); + expect(result).toEqual({ + indent: 2, + marker: '+', + checkChar: 'x', + checkboxCharPosition: 5, // 2 spaces + 1 marker + 1 space + 1 [ = 5 + checkboxState: CheckboxState.Checked, + isChecked: true, + listItemText: "Indented Task", + }); + }); + + it('should match numbered list checkbox', () => { + const line = '1. [ ] Numbered'; + const result = checkboxUtils.matchCheckboxLine(line); + expect(result).not.toBeNull(); + expect(result).toEqual({ + indent: 0, + marker: '1.', + checkChar: ' ', + checkboxCharPosition: 4, // 2 marker + 1 space + 1 [ = 4 + checkboxState: CheckboxState.Unchecked, + isChecked: false, + listItemText: "Numbered", + }); + }); + + it('should match checkbox with multi-digit numbered list', () => { + const line = '10. [x] Double Digit'; + const result = checkboxUtils.matchCheckboxLine(line); + expect(result).not.toBeNull(); + expect(result).toEqual({ + indent: 0, + marker: '10.', + checkChar: 'x', + checkboxCharPosition: 5, // 3 marker + 1 space + 1 [ = 5 + checkboxState: CheckboxState.Checked, + isChecked: true, + listItemText: "Double Digit", + }); + }); + + it('should return null for lines without checkbox format', () => { + expect(checkboxUtils.matchCheckboxLine('Just text')).toBeNull(); + expect(checkboxUtils.matchCheckboxLine('- [ ]')).toBeNull(); // No space after ] + expect(checkboxUtils.matchCheckboxLine('- [] ')).toBeNull(); // No char inside + expect(checkboxUtils.matchCheckboxLine('[ ] Task')).toBeNull(); // No marker + expect(checkboxUtils.matchCheckboxLine('-[ ] Task')).toBeNull(); // No space after marker + }); + + it('should match custom symbols based on settings', () => { + const customSettings = createSettings({ + checkedSymbols: ['X', 'V'], + uncheckedSymbols: ['O', '-'], + ignoreSymbols: ['~'], + }); + const customUtils = new CheckboxUtils(customSettings); + + const checkedLine = '- [V] Custom Checked'; + const checkedResult = customUtils.matchCheckboxLine(checkedLine); + expect(checkedResult?.checkboxState).toBe(CheckboxState.Checked); + expect(checkedResult?.isChecked).toBe(true); + expect(checkedResult?.checkChar).toBe('V'); + + const uncheckedLine = '* [O] Custom Unchecked'; + const uncheckedResult = customUtils.matchCheckboxLine(uncheckedLine); + expect(uncheckedResult?.checkboxState).toBe(CheckboxState.Unchecked); + expect(uncheckedResult?.isChecked).toBe(false); + expect(uncheckedResult?.checkChar).toBe('O'); + + const ignoredLine = '+ [~] Custom Ignored'; + const ignoredResult = customUtils.matchCheckboxLine(ignoredLine); + expect(ignoredResult?.checkboxState).toBe(CheckboxState.Ignore); + expect(ignoredResult?.isChecked).toBeUndefined(); + expect(ignoredResult?.checkChar).toBe('~'); + }); + }); + + // --- Тесты для getCheckboxState --- + describe('getCheckboxState', () => { + it('should return Checked for symbols in checkedSymbols', () => { + const customSettings = createSettings({ checkedSymbols: ['x', 'X', '✓'] }); + const customUtils = new CheckboxUtils(customSettings); + expect(customUtils.getCheckboxState('x')).toBe(CheckboxState.Checked); + expect(customUtils.getCheckboxState('X')).toBe(CheckboxState.Checked); + expect(customUtils.getCheckboxState('✓')).toBe(CheckboxState.Checked); + }); + + it('should return Unchecked for symbols in uncheckedSymbols', () => { + const customSettings = createSettings({ uncheckedSymbols: [' ', '_', '?'] }); + const customUtils = new CheckboxUtils(customSettings); + expect(customUtils.getCheckboxState(' ')).toBe(CheckboxState.Unchecked); + expect(customUtils.getCheckboxState('_')).toBe(CheckboxState.Unchecked); + expect(customUtils.getCheckboxState('?')).toBe(CheckboxState.Unchecked); + }); + + it('should return Ignore for symbols in ignoreSymbols', () => { + const customSettings = createSettings({ ignoreSymbols: ['-', '~', '>'] }); + const customUtils = new CheckboxUtils(customSettings); + expect(customUtils.getCheckboxState('-')).toBe(CheckboxState.Ignore); + expect(customUtils.getCheckboxState('~')).toBe(CheckboxState.Ignore); + expect(customUtils.getCheckboxState('>')).toBe(CheckboxState.Ignore); + }); + + it('should use unknownSymbolPolicy for symbols not in any list', () => { + const settingsChecked = createSettings({ unknownSymbolPolicy: CheckboxState.Checked }); + const utilsChecked = new CheckboxUtils(settingsChecked); + expect(utilsChecked.getCheckboxState('?')).toBe(CheckboxState.Checked); + + const settingsUnchecked = createSettings({ unknownSymbolPolicy: CheckboxState.Unchecked }); + const utilsUnchecked = new CheckboxUtils(settingsUnchecked); + expect(utilsUnchecked.getCheckboxState('?')).toBe(CheckboxState.Unchecked); + + const settingsIgnore = createSettings({ unknownSymbolPolicy: CheckboxState.Ignore }); + const utilsIgnore = new CheckboxUtils(settingsIgnore); + expect(utilsIgnore.getCheckboxState('?')).toBe(CheckboxState.Ignore); + }); + }); + + // --- Тесты для updateLineCheckboxStateWithInfo --- + describe('updateLineCheckboxStateWithInfo', () => { + it('should update checkbox state from unchecked to checked', () => { + const line = '- [ ] Task'; + const lineInfo = checkboxUtils.matchCheckboxLine(line)!; // Assume valid line + const updatedLine = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, lineInfo); + // Uses the first symbol from settings.checkedSymbols ('x' by default) + expect(updatedLine).toBe('- [x] Task'); + }); + + it('should update checkbox state from checked to unchecked', () => { + const line = '* [x] Done'; + const lineInfo = checkboxUtils.matchCheckboxLine(line)!; + const updatedLine = checkboxUtils.updateLineCheckboxStateWithInfo(line, false, lineInfo); + // Uses the first symbol from settings.uncheckedSymbols (' ' by default) + expect(updatedLine).toBe('* [ ] Done'); + }); + + it('should use first symbol from settings for update', () => { + const customSettings = createSettings({ checkedSymbols: ['V', 'X'], uncheckedSymbols: ['O', ' '] }); + const customUtils = new CheckboxUtils(customSettings); + + const lineToCheck = '- [O] Task'; + const infoToCheck = customUtils.matchCheckboxLine(lineToCheck)!; + const updatedToCheck = customUtils.updateLineCheckboxStateWithInfo(lineToCheck, true, infoToCheck); + expect(updatedToCheck).toBe('- [V] Task'); // Should use 'V' + + const lineToUncheck = '* [V] Done'; + const infoToUncheck = customUtils.matchCheckboxLine(lineToUncheck)!; + const updatedToUncheck = customUtils.updateLineCheckboxStateWithInfo(lineToUncheck, false, infoToUncheck); + expect(updatedToUncheck).toBe('* [O] Done'); // Should use 'O' + }); + + it('should handle indented lines correctly', () => { + const line = ' - [ ] Indented'; + const lineInfo = checkboxUtils.matchCheckboxLine(line)!; + const updatedLine = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, lineInfo); + expect(updatedLine).toBe(' - [x] Indented'); + }); + + it('should return original line if position is invalid (simulated)', () => { + const line = '- [ ] Task'; + const lineInfo = checkboxUtils.matchCheckboxLine(line)!; + // Simulate invalid position + const invalidInfo = { ...lineInfo, checkboxCharPosition: 100 }; + const updatedLine = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, invalidInfo); + expect(updatedLine).toBe(line); // Should not change + }); + + it('should use default checked symbol "x" if checkedSymbols setting is empty', () => { + // Создаем настройки с пустым списком checkedSymbols + const customSettings = createSettings({ checkedSymbols: [] }); + const customUtils = new CheckboxUtils(customSettings); + const line = '- [ ] Task'; + const lineInfo = customUtils.matchCheckboxLine(line)!; + + // Пытаемся отметить чекбокс + const updatedLine = customUtils.updateLineCheckboxStateWithInfo(line, true, lineInfo); + expect(updatedLine).toBe('- [x] Task'); // Ожидаем дефолтный 'x' + }); + + it('should use default unchecked symbol " " if uncheckedSymbols setting is empty', () => { + // Создаем настройки с пустым списком uncheckedSymbols + const customSettings = createSettings({ uncheckedSymbols: [] }); + const customUtils = new CheckboxUtils(customSettings); + const line = '- [x] Task'; // Используем 'x' из дефолтных checkedSymbols + const lineInfo = customUtils.matchCheckboxLine(line)!; + + // Пытаемся снять отметку + const updatedLine = customUtils.updateLineCheckboxStateWithInfo(line, false, lineInfo); + expect(updatedLine).toBe('- [ ] Task'); // Ожидаем дефолтный ' ' (пробел) + }); + + // Усиленный тест для невалидной позиции (с проверкой console.warn) + it('should return original line and warn if position is invalid', () => { + const line = '- [ ] Task'; + const lineInfo = checkboxUtils.matchCheckboxLine(line)!; + // Мокаем console.warn + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { }); + + // Simulate invalid position + const invalidInfo = { ...lineInfo, checkboxCharPosition: -1 }; // Невалидная позиция + const updatedLineNegative = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, invalidInfo); + expect(updatedLineNegative).toBe(line); // Должен вернуть оригинальную строку + expect(warnSpy).toHaveBeenCalledTimes(1); // Проверяем вызов warn + + const invalidInfo2 = { ...lineInfo, checkboxCharPosition: 100 }; // Другая невалидная позиция + const updatedLineOob = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, invalidInfo2); + expect(updatedLineOob).toBe(line); // Должен вернуть оригинальную строку + expect(warnSpy).toHaveBeenCalledTimes(2); // Проверяем вызов warn еще раз + + // Восстанавливаем оригинальную функцию console.warn + warnSpy.mockRestore(); + }); + + it('should return original line and warn if checkboxCharPosition is invalid for a valid checkbox type', () => { + const line = '- [ ] Task'; + const lineInfo = checkboxUtils.matchCheckboxLine(line)!; // Это валидный чекбокс + const invalidPosInfo = { ...lineInfo, checkboxCharPosition: -1 }; // Невалидная позиция + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { }); + const updatedLine = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, invalidPosInfo); + expect(updatedLine).toBe(line); + expect(warnSpy).toHaveBeenCalledWith("updateLineCheckboxStateWithInfo: Invalid checkbox position in lineInfo for line:", line); + warnSpy.mockRestore(); + }); + + it('UPDATE_LINE: should return original line and warn if lineInfo is for NoCheckbox', () => { + const line = "- Plain item"; + // Создаем lineInfo, как будто он пришел от plain list item + const plainLineInfo: CheckboxLineInfo = { + indent: 0, + marker: '-', + checkboxState: CheckboxState.NoCheckbox, // Ключевой момент + listItemText: 'Plain item', + // checkboxCharPosition не важен, т.к. до него не дойдет + }; + + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { }); + + const updatedLine = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, plainLineInfo); + + expect(updatedLine).toBe(line); // Строка не должна измениться + expect(warnSpy).toHaveBeenCalledWith("updateLineCheckboxStateWithInfo: Invalid lineInfo(no checkbox) for line:", line); + + warnSpy.mockRestore(); + }); + }); + + // --- Тесты для propagateStateToChildren --- + describe('propagateStateToChildren', () => { + const setupUtils = (customSettings?: Partial) => { + return new CheckboxUtils(createSettings(customSettings)); + } + + it('should check children when parent is checked', () => { + const text = [ + '- [x] Parent', // line 0 + ' - [ ] Child 1', + ' - [ ] Grandchild', + ' - [ ] Child 2', + '- [ ] Sibling', + ].join('\n'); + const expected = [ + '- [x] Parent', + ' - [x] Child 1', + ' - [x] Grandchild', + ' - [x] Child 2', + '- [ ] Sibling', // Sibling should not change + ].join('\n'); + expect(checkboxUtils.propagateStateToChildren(text, 0)).toBe(expected); + }); + + it('should uncheck children when parent is unchecked', () => { + const text = [ + '- [ ] Parent', // line 0 + ' - [x] Child 1', + ' - [x] Grandchild', + ' - [ ] Child 2', // Already unchecked + '- [x] Sibling', + ].join('\n'); + const expected = [ + '- [ ] Parent', + ' - [ ] Child 1', + ' - [ ] Grandchild', + ' - [ ] Child 2', + '- [x] Sibling', // Sibling should not change + ].join('\n'); + expect(checkboxUtils.propagateStateToChildren(text, 0)).toBe(expected); + }); + + it('should stop propagation at siblings or less indented lines', () => { + const text = [ + ' - [x] Parent', // line 0 + ' - [ ] Child', + ' - [ ] Sibling', // Same indent level + '- [ ] Less Indented', + ].join('\n'); + const expected = [ + ' - [x] Parent', + ' - [x] Child', // Changed + ' - [ ] Sibling', // Not changed + '- [ ] Less Indented', // Not changed + ].join('\n'); + expect(checkboxUtils.propagateStateToChildren(text, 0)).toBe(expected); + }); + + it('should skip ignored children and their subtrees', () => { + const utils = setupUtils({ ignoreSymbols: ['~'] }); + const text = [ + '- [x] Parent', // 0 + ' - [ ] Child 1', // 1 + ' - [~] Ignored Child',// 2 (ignore) + ' - [ ] Skipped GC', // 3 (skipped because parent ignored) + ' - [ ] Child 2', // 4 + ].join('\n'); + const expected = [ + '- [x] Parent', + ' - [x] Child 1', // Changed + ' - [~] Ignored Child',// Unchanged + ' - [ ] Skipped GC', // Unchanged + ' - [x] Child 2', // Changed + ].join('\n'); + expect(utils.propagateStateToChildren(text, 0)).toBe(expected); + }); + + it('should do nothing if the parent line is not a checkbox', () => { + const text = [ + 'Parent Line', + ' - [ ] Child', + ].join('\n'); + expect(checkboxUtils.propagateStateToChildren(text, 0)).toBe(text); + }); + + it('should do nothing if the parent checkbox is ignored', () => { + const utils = setupUtils({ ignoreSymbols: ['~'] }); + const text = [ + '- [~] Ignored Parent', // 0 + ' - [ ] Child', + ].join('\n'); + expect(utils.propagateStateToChildren(text, 0)).toBe(text); + }); + + it('should return original text and warn if parent line index does not contain a list item', () => { + const text = "Not a list item\n - [ ] Child"; + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { }); + const result = checkboxUtils.propagateStateToChildren(text, 0); + expect(result).toBe(text); + expect(warnSpy).toHaveBeenCalledWith("checkbox not found in line 0"); + warnSpy.mockRestore(); + }); + + it('should stop propagation if a non-list-item line is encountered among children', () => { + const text = [ + '- [x] Parent', + ' Not a child list item', // This will make childLineInfo null + ' - [ ] Grandchild (should not be processed)', + ].join('\n'); + const expected = [ + '- [x] Parent', + ' Not a child list item', + ' - [ ] Grandchild (should not be processed)', + ].join('\n'); + expect(checkboxUtils.propagateStateToChildren(text, 0)).toBe(expected); + }); + + it('should correctly stop processing sub-children of an ignored node if a non-list-item is found', () => { + const utils = new CheckboxUtils(createSettings({ ignoreSymbols: ['~'] })); + const text = [ + '- [x] Parent', + ' - [~] Ignored Child', + ' - [ ] Valid Sub-Child (will be skipped by inner loop)', // j инкрементируется + ' Not a sub-child list item', // subChildLineInfo = null, inner loop breaks + ' - [ ] Another Sub-Child (SHOULD NOT BE SKIPPED by inner loop, as it breaks, and also not processed by outer loop)', + ' - [ ] Next Sibling of Ignored', // This should also not be processed due to outer loop break + ].join('\n'); + + // Ожидаем, что Parent и Ignored Child останутся, а остальное не изменится, + // потому что внешний цикл прервется после обработки "Not a sub-child list item" + const expected = [ + '- [x] Parent', + ' - [~] Ignored Child', + ' - [ ] Valid Sub-Child (will be skipped by inner loop)', + ' Not a sub-child list item', + ' - [ ] Another Sub-Child (SHOULD NOT BE SKIPPED by inner loop, as it breaks, and also not processed by outer loop)', + ' - [ ] Next Sibling of Ignored', + ].join('\n'); + + const result = utils.propagateStateToChildren(text, 0); + expect(result).toBe(expected); + // В данном случае, поскольку Parent [x], и мы ничего не меняем, результат будет равен text. + // Важно то, что Next Sibling of Ignored не стал [x]. + }); + }); + + // --- Тесты для propagateStateFromChildren --- + describe('propagateStateFromChildren', () => { + const setupUtils = (customSettings?: Partial) => { + return new CheckboxUtils(createSettings(customSettings)); + } + + it('should check parent if all children are checked', () => { + const text = [ + '- [ ] Parent', // line 0 + ' - [x] Child 1', + ' - [x] Child 2', + ' - [x] Grandchild', // Needs parent (Child 2) to be checked first + ].join('\n'); + // Expected: Grandchild affects Child 2 -> Child 2 and Child 1 affect Parent + const expectedPass1 = [ // After processing Grandchild and Child 2 + '- [ ] Parent', + ' - [x] Child 1', + ' - [x] Child 2', // Stays checked + ' - [x] Grandchild', + ].join('\n'); + const expectedPass2 = [ // After processing Child 1 and Parent + '- [x] Parent', // Changes to checked + ' - [x] Child 1', + ' - [x] Child 2', + ' - [x] Grandchild', + ].join('\n'); + // Since it processes bottom-up, Child 2 should already be evaluated correctly + expect(checkboxUtils.propagateStateFromChildren(text)).toBe(expectedPass2); + }); + + it('should uncheck parent if any child is unchecked', () => { + const text = [ + '- [x] Parent', // line 0 + ' - [x] Child 1', + ' - [ ] Child 2', // This one makes the parent unchecked + ' - [ ] Grandchild', + ].join('\n'); + const expected = [ + '- [ ] Parent', // Changes to unchecked + ' - [x] Child 1', + ' - [ ] Child 2', + ' - [ ] Grandchild', + ].join('\n'); + expect(checkboxUtils.propagateStateFromChildren(text)).toBe(expected); + }); + + it('should handle nested updates correctly (bottom-up)', () => { + const text = [ + '- [ ] Grandparent', // 0 + ' - [ ] Parent 1', // 1 + ' - [x] Child 1.1',// 2 + ' - [x] Child 1.2',// 3 + ' - [x] Parent 2', // 4 + ' - [ ] Child 2.1',// 5 (Makes Parent 2 unchecked) + ].join('\n'); + const expected = [ + '- [ ] Grandparent', // Becomes unchecked (because Parent 2 becomes unchecked) + ' - [x] Parent 1', // Becomes checked (Child 1.1, 1.2 are checked) + ' - [x] Child 1.1', + ' - [x] Child 1.2', + ' - [ ] Parent 2', // Becomes unchecked (Child 2.1 is unchecked) + ' - [ ] Child 2.1', + ].join('\n'); + expect(checkboxUtils.propagateStateFromChildren(text)).toBe(expected); + }); + + it('should not change parent state if it has no relevant children', () => { + const text = [ + '- [ ] Parent', + 'Sibling', // Not a child checkbox + ' Not indented correctly', + ].join('\n'); + expect(checkboxUtils.propagateStateFromChildren(text)).toBe(text); + }); + + it('should ignore non-checkbox lines when determining parent state', () => { + const text = [ + '- [ ] Parent', + ' - [x] Child 1', + ' Just text', + ' - [x] Child 2', + ].join('\n'); + const expected = [ + '- [x] Parent', // Should become checked + ' - [x] Child 1', + ' Just text', + ' - [x] Child 2', + ].join('\n'); + expect(checkboxUtils.propagateStateFromChildren(text)).toBe(expected); + }); + + it('should skip ignored children when determining parent state', () => { + const utils = setupUtils({ ignoreSymbols: ['~'] }); + const text = [ + '- [ ] Parent', // 0 + ' - [x] Child 1', // 1 + ' - [~] Ignored Child',// 2 (skipped) + ' - [ ] Skipped GC', // 3 (doesn't affect parent) + ' - [x] Child 2', // 4 + ].join('\n'); + const expected = [ + '- [x] Parent', // Becomes checked (based on Child 1 and Child 2 only) + ' - [x] Child 1', + ' - [~] Ignored Child', + ' - [ ] Skipped GC', + ' - [x] Child 2', + ].join('\n'); + expect(utils.propagateStateFromChildren(text)).toBe(expected); + }); + + it('should not change parent state if all direct children are ignored', () => { + const utils = setupUtils({ ignoreSymbols: ['~'] }); + const text = [ + '- [ ] Parent', // 0 + ' - [~] Ignored 1', // 1 + ' - [~] Ignored 2', // 2 + ].join('\n'); + expect(utils.propagateStateFromChildren(text)).toBe(text); // Parent stays unchecked + const text2 = [ + '- [x] Parent', // 0 + ' - [~] Ignored 1', // 1 + ' - [~] Ignored 2', // 2 + ].join('\n'); + expect(utils.propagateStateFromChildren(text2)).toBe(text2); // Parent stays checked + }); + + it('should not update ignored parents', () => { + const utils = setupUtils({ ignoreSymbols: ['~'] }); + const text = [ + '- [~] Ignored Parent', // 0 + ' - [x] Child 1', + ' - [x] Child 2', + ].join('\n'); + expect(utils.propagateStateFromChildren(text)).toBe(text); // Parent remains ignored + }); + + it('should stop searching children when a line with equal or lesser indent is found', () => { + const text = [ + '- [ ] Parent 1', // indent 0 + 'Some regular text', // indent 0. Stops search for Parent 1 children. + ' - [x] Child 1.1', // indent 2. Should be ignored for Parent 1. + '- [ ] Parent 2', // indent 0. Processed independently. + ' - [x] Child 2.1', // indent 2. Child of Parent 2. + ].join('\n'); + + const result = checkboxUtils.propagateStateFromChildren(text); + + const expected = [ + '- [ ] Parent 1', // Unchanged (no children found before stop) + 'Some regular text', + ' - [x] Child 1.1', + '- [x] Parent 2', // Updated by Child 2.1 + ' - [x] Child 2.1', + ].join('\n'); + expect(result).toBe(expected); + }); + + + it('should stop searching children when a sibling checkbox (equal/lesser indent) is found', () => { + const text = [ + '- [ ] Parent 1', // indent 0 + '- [ ] Sibling Parent', // indent 0. Stops search for Parent 1 children. + ' - [x] Child SP.1', // indent 2. Belongs to Sibling Parent. + ].join('\n'); + + const result = checkboxUtils.propagateStateFromChildren(text); + + const expected = [ + '- [ ] Parent 1', // Unchanged (no children found before stop) + '- [x] Sibling Parent', // Updated by Child SP.1 + ' - [x] Child SP.1', + ].join('\n'); + expect(result).toBe(expected); + + const text2 = [ + ' - [ ] Parent 1', // indent 2 + ' - [x] Child 1.1', // indent 4 + ' - [ ] Sibling Parent', // indent 2. Stops search for Parent 1 children. + ' - [ ] Child SP.1', // indent 4. Belongs to Sibling Parent. + ].join('\n'); + const result2 = checkboxUtils.propagateStateFromChildren(text2); + const expected2 = [ + ' - [x] Parent 1', // Updated by Child 1.1 + ' - [x] Child 1.1', + ' - [ ] Sibling Parent', // Unchanged (Child SP.1 is unchecked) + ' - [ ] Child SP.1', + ].join('\n'); + expect(result2).toBe(expected2); + }); + + it('should ignore non-list-item lines when iterating upwards', () => { + const text = [ + '- [ ] Parent', + ' - [x] Child', + 'Just some random text in between', // parentLineInfo will be null for this + '- [ ] Another Parent', + ' - [x] Another Child', + ].join('\n'); + const expected = [ + '- [x] Parent', + ' - [x] Child', + 'Just some random text in between', + '- [x] Another Parent', + ' - [x] Another Child', + ].join('\n'); + expect(checkboxUtils.propagateStateFromChildren(text)).toBe(expected); + }); + }); + + // --- Тесты для syncText --- + describe('syncText', () => { + const textBefore = [ + '- [ ] Parent', + ' - [ ] Child', + ].join('\n'); + const textAfterParentChecked = [ // Simulate user checking parent + '- [x] Parent', + ' - [ ] Child', + ].join('\n'); + const textAfterChildChecked = [ // Simulate user checking child + '- [ ] Parent', + ' - [x] Child', + ].join('\n'); + + it('should propagate down only if child sync enabled and diff matches', () => { + const utils = new CheckboxUtils(createSettings({ + enableAutomaticChildState: true, + enableAutomaticParentState: false, // Parent sync off + })); + const expected = [ + '- [x] Parent', + ' - [x] Child', // Propagated down + ].join('\n'); + expect(utils.syncText(textAfterParentChecked, textBefore)).toBe(expected); + }); + + it('should propagate up only if parent sync enabled', () => { + const utils = new CheckboxUtils(createSettings({ + enableAutomaticChildState: false, // Child sync off + enableAutomaticParentState: true, + })); + const expected = [ + '- [x] Parent', // Propagated up + ' - [x] Child', + ].join('\n'); + // syncText calls propagateFromChildren unconditionally if enabled + expect(utils.syncText(textAfterChildChecked, textBefore)).toBe(expected); + }); + + it('should propagate down then up if both enabled', () => { + const utils = new CheckboxUtils(createSettings({ + enableAutomaticChildState: true, + enableAutomaticParentState: true, + })); + // 1. User checks parent: '- [x] Parent', ' - [ ] Child' + // 2. Propagate down: '- [x] Parent', ' - [x] Child' + // 3. Propagate up: (no change needed as parent is already checked) + const expectedDown = [ + '- [x] Parent', + ' - [x] Child', // Propagated down + ].join('\n'); + expect(utils.syncText(textAfterParentChecked, textBefore)).toBe(expectedDown); + + // 1. User checks child: '- [ ] Parent', ' - [x] Child' + // 2. Propagate down: (no change, as only one line changed, and it wasn't the parent) + // 3. Propagate up: '- [x] Parent', ' - [x] Child' + const expectedUp = [ + '- [x] Parent', // Propagated up + ' - [x] Child', + ].join('\n'); + expect(utils.syncText(textAfterChildChecked, textBefore)).toBe(expectedUp); + }); + + it('should do nothing if both syncs disabled', () => { + const utils = new CheckboxUtils(createSettings({ + enableAutomaticChildState: false, + enableAutomaticParentState: false, + })); + expect(utils.syncText(textAfterParentChecked, textBefore)).toBe(textAfterParentChecked); + expect(utils.syncText(textAfterChildChecked, textBefore)).toBe(textAfterChildChecked); + }); + + it('should not propagate down if diff is not a single checkbox state change', () => { + const utils = new CheckboxUtils(createSettings({ + enableAutomaticChildState: true, // Down propagation enabled + enableAutomaticParentState: false, + })); + const textMultipleChanges = [ + '- [x] Parent Changed', // Text changed too + ' - [x] Child also changed', + ].join('\n'); + const textIndentChange = [ + ' - [x] Parent', // Indent changed + ' - [ ] Child', + ].join('\n'); + + // Multiple lines changed + expect(utils.syncText(textMultipleChanges, textBefore)).toBe(textMultipleChanges); + // Indent changed (diff > 1 line index) + expect(utils.syncText(textIndentChange, textBefore)).toBe(textIndentChange); + }); + }); + + // --- Тесты для propagateStateToChildrenFromSingleDiff --- + describe('propagateStateToChildrenFromSingleDiff', () => { + const textBefore = [ + '- [ ] Parent', + ' - [ ] Child', + ].join('\n'); + + it('should propagate if only checkbox state changed', () => { + const textAfter = [ + '- [x] Parent', // Only state changed + ' - [ ] Child', + ].join('\n'); + const expected = [ + '- [x] Parent', + ' - [x] Child', // Propagated + ].join('\n'); + expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(expected); + }); + + it('should propagate if text content also changed', () => { + const textAfter = [ + '- [x] Parent Changed Text', // State and text changed + ' - [ ] Child', + ].join('\n'); + const expected = [ + '- [x] Parent Changed Text', + ' - [x] Child', // Propagated + ].join('\n'); + expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(expected); + }); + + it('should NOT propagate if marker changed', () => { + const textAfter = [ + '* [x] Parent', // Marker changed from - to * + ' - [ ] Child', + ].join('\n'); + expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(textAfter); + }); + + it('should NOT propagate if indentation changed', () => { + const textAfter = [ + ' - [x] Parent', // Indentation changed + ' - [ ] Child', + ].join('\n'); + // This will be detected as > 1 line difference by findDifferentLineIndexes + expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(textAfter); + }); + + + it('should NOT propagate if multiple lines changed', () => { + const textAfter = [ + '- [x] Parent', + ' - [x] Child', // Second line also changed + ].join('\n'); + expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(textAfter); + }); + + it('should NOT propagate if changed line is not a checkbox', () => { + const textBeforeNonCheck = "Line 1\nLine 2"; + const textAfterNonCheck = "Line 1 changed\nLine 2"; + expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfterNonCheck, textBeforeNonCheck)).toBe(textAfterNonCheck); + }); + + it('should propagate if changed line is an ignored checkbox', () => { + const utils = new CheckboxUtils(createSettings({ ignoreSymbols: ['~'] })); + const textBeforeIgnored = [ + '- [~] Parent', + ' - [ ] Child', + ].join('\n'); + const textAfterIgnored = [ + '- [x] Parent', // Changed FROM ignored TO checked + ' - [ ] Child', + ].join('\n'); + const expected = [ + '- [x] Parent', + ' - [x] Child', // Propagated + ].join('\n'); + + expect(utils.propagateStateToChildrenFromSingleDiff(textAfterIgnored, textBeforeIgnored)).toBe(expected); + }); + + it('should return original text if textBefore is undefined', () => { + const text = '- [ ] Line'; + expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(text, undefined)).toBe(text); + }); + + it('should return original text if line counts differ', () => { + const text1 = '- [ ] Line1\n- [ ] Line2'; + const text2 = '- [ ] Line1'; + expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(text1, text2)).toBe(text1); + }); + + it('should return original text if textBefore and text have different line counts', () => { + const textBefore = "- [ ] Line 1"; + const text = "- [ ] Line 1\n- [ ] Line 2"; + expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(text, textBefore)).toBe(text); + }); + + it('should return original text if no lines changed (diffIndexes.length === 0)', () => { + const text = "- [ ] Task"; + expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(text, text)).toBe(text); + }); + + it('should return original text if multiple lines changed (diffIndexes.length > 1)', () => { + const textBefore = "- [ ] Task1\n- [ ] Task2"; + const textAfter = "- [x] Task1\n- [x] Task2"; // 2 diffs + expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(textAfter); + }); + + it('should not propagate if changed line was not a list item before', () => { + const textBefore = "Not a list item"; + const textAfter = "- [x] Became a list item"; + expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(textAfter); + }); + + it('should not propagate if changed line is not a list item after', () => { + const textBefore = "- [ ] Was a list item"; + const textAfter = "Not a list item anymore"; + expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(textAfter); + }); + + it('should not propagate if indent changed', () => { + const textBeforeIndent = [ + "- [ ] Parent", + " - [ ] Child", + ].join('\n'); + const textAfterIndent = [ + " - [ ] Parent", + " - [ ] Child", + ].join('\n'); // Parent indent changed + expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfterIndent, textBeforeIndent)).toBe(textAfterIndent); + }); + + it('should not propagate if marker changed', () => { + const textBefore = "- [ ] Item\n - [ ] Child"; + const textAfter = "* [ ] Item\n - [ ] Child"; // Marker changed + expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(textAfter); + }); + + it('should not propagate if checkbox state did not change (e.g., only text content)', () => { + const textBefore = "- [x] Task Alpha\n - [ ] Child"; + const textAfter = "- [x] Task Bravo\n - [ ] Child"; // Only text changed + expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(textAfter); + }); + }); + + // --- Тесты для findDifferentLineIndexes --- + describe('findDifferentLineIndexes', () => { + it('should return empty array for identical lines', () => { + const lines1 = ['a', 'b', 'c']; + const lines2 = ['a', 'b', 'c']; + expect(checkboxUtils.findDifferentLineIndexes(lines1, lines2)).toEqual([]); + }); + + it('should return index of the single different line', () => { + const lines1 = ['a', 'b', 'c']; + const lines2 = ['a', 'B', 'c']; + expect(checkboxUtils.findDifferentLineIndexes(lines1, lines2)).toEqual([1]); + }); + + it('should return indexes of multiple different lines', () => { + const lines1 = ['a', 'b', 'c', 'd']; + const lines2 = ['A', 'b', 'C', 'd']; + expect(checkboxUtils.findDifferentLineIndexes(lines1, lines2)).toEqual([0, 2]); + }); + + it('should detect differences at start and end', () => { + const lines1 = ['a', 'b', 'c']; + const lines2 = ['X', 'b', 'Y']; + expect(checkboxUtils.findDifferentLineIndexes(lines1, lines2)).toEqual([0, 2]); + }); + + it('should throw error if line arrays have different lengths', () => { + const lines1 = ['a', 'b']; + const lines2 = ['a', 'b', 'c']; + expect(() => checkboxUtils.findDifferentLineIndexes(lines1, lines2)) + .toThrow("the length of the lines must be equal"); + expect(() => checkboxUtils.findDifferentLineIndexes(lines2, lines1)) + .toThrow("the length of the lines must be equal"); + }); + }); + +}); diff --git a/__tests__/checkboxUtils.plainLists.test.ts b/__tests__/checkboxUtils.plainLists.test.ts new file mode 100644 index 0000000..45bd743 --- /dev/null +++ b/__tests__/checkboxUtils.plainLists.test.ts @@ -0,0 +1,383 @@ +// checkboxUtils.plainLists.test.ts + +import { CheckboxUtils } from '../src/checkboxUtils'; +import { CheckboxState, CheckboxSyncPluginSettings, DEFAULT_SETTINGS } from '../src/types'; + +// Вспомогательная функция для создания настроек с переопределениями +const createSettings = (overrides: Partial = {}): Readonly => { + return { ...DEFAULT_SETTINGS, ...overrides } as Readonly; +}; + +describe('CheckboxUtils - Plain List Item Support', () => { + let checkboxUtils: CheckboxUtils; + let settings: Readonly; + + beforeEach(() => { + settings = createSettings(); + checkboxUtils = new CheckboxUtils(settings); + }); + + // --- Тесты для matchCheckboxLine с поддержкой plain list items --- + describe('matchCheckboxLine with Plain List Items', () => { + it('should match a plain bullet list item', () => { + const line = '- Plain item text'; + const result = checkboxUtils.matchCheckboxLine(line); + expect(result).not.toBeNull(); + expect(result).toEqual({ + indent: 0, + marker: '-', + checkboxState: CheckboxState.NoCheckbox, + listItemText: 'Plain item text', + // Опциональные поля для чекбокса должны быть undefined + checkChar: undefined, + checkboxCharPosition: undefined, + isChecked: undefined, + }); + }); + + it('should match a plain asterisk list item with indentation', () => { + const line = ' * Indented plain item'; + const result = checkboxUtils.matchCheckboxLine(line); + expect(result).not.toBeNull(); + expect(result).toEqual({ + indent: 2, + marker: '*', + checkboxState: CheckboxState.NoCheckbox, + listItemText: 'Indented plain item', + }); + }); + + it('should match a plain plus list item', () => { + const line = '+ Another plain item'; + const result = checkboxUtils.matchCheckboxLine(line); + expect(result).not.toBeNull(); + expect(result).toEqual({ + indent: 0, + marker: '+', + checkboxState: CheckboxState.NoCheckbox, + listItemText: 'Another plain item', + }); + }); + + it('should match a plain numbered list item', () => { + const line = '1. Numbered plain item'; + const result = checkboxUtils.matchCheckboxLine(line); + expect(result).not.toBeNull(); + expect(result).toEqual({ + indent: 0, + marker: '1.', + checkboxState: CheckboxState.NoCheckbox, + listItemText: 'Numbered plain item', + }); + }); + + it('should match a plain multi-digit numbered list item', () => { + const line = '10. Long numbered plain item'; + const result = checkboxUtils.matchCheckboxLine(line); + expect(result).not.toBeNull(); + expect(result).toEqual({ + indent: 0, + marker: '10.', + checkboxState: CheckboxState.NoCheckbox, + listItemText: 'Long numbered plain item', + }); + }); + + it('should still match a standard checkbox item', () => { + const line = '- [x] A checkbox item'; + const result = checkboxUtils.matchCheckboxLine(line); + expect(result).not.toBeNull(); + expect(result?.checkboxState).toBe(CheckboxState.Checked); + expect(result?.isChecked).toBe(true); + expect(result?.checkChar).toBe('x'); + expect(result?.listItemText).toBe('A checkbox item'); + }); + + it('should match a plain list item with no text after marker', () => { + const line = '- '; // Marker and space + const result = checkboxUtils.matchCheckboxLine(line); + expect(result).not.toBeNull(); + expect(result).toEqual({ + indent: 0, + marker: '-', + checkboxState: CheckboxState.NoCheckbox, + listItemText: '', // Empty string for text + }); + }); + + it('should match a plain list item with only spaces after marker', () => { + const line = '* '; // Marker and spaces + const result = checkboxUtils.matchCheckboxLine(line); + expect(result).not.toBeNull(); + expect(result).toEqual({ + indent: 0, + marker: '*', + checkboxState: CheckboxState.NoCheckbox, + listItemText: '', // Text is trimmed + }); + }); + + it('should correctly parse listItemText for checkboxes', () => { + const line = '- [ ] Task with text'; + const result = checkboxUtils.matchCheckboxLine(line); + expect(result?.listItemText).toBe('Task with text'); + + const lineNoText = '- [x] '; + const resultNoText = checkboxUtils.matchCheckboxLine(lineNoText); + expect(resultNoText?.listItemText).toBe(''); + }); + + it('should return null for lines that are not list items or malformed', () => { + expect(checkboxUtils.matchCheckboxLine('Just some text')).toBeNull(); + expect(checkboxUtils.matchCheckboxLine('-NoSpaceAfterMarker')).toBeNull(); + expect(checkboxUtils.matchCheckboxLine('1.NoSpaceAfterMarker')).toBeNull(); + expect(checkboxUtils.matchCheckboxLine('> Not a list marker')).toBeNull(); + // Это все еще НЕ чекбокс и НЕ plain list item по текущим правилам (нет пробела после маркера) + expect(checkboxUtils.matchCheckboxLine('-[ ] No space after marker for checkbox')).toBeNull(); + // Это plain list item, а не чекбокс, т.к. нет пробела перед [ ] + const trickyLine = "-[ ] Not a checkbox, but could be a plain item if regex allowed marker immediately followed by non-space"; + // По текущему regex ^(\s*)([*+-]|\d+\.)\s+(?!\[.?\])(.*)$ для plain list, это будет null. + // Если бы regex для plain list был ^(\s*)([*+-]|\d+\.)\s*(.*)$, и мы бы отсекали чекбоксы отдельно, + // то "-[ ] text" мог бы считаться plain. Но текущий regex для plain требует \s+ после маркера. + // А regex для чекбокса требует " \[" . Так что "-[ ] text" не матчится ни тем, ни другим. + expect(checkboxUtils.matchCheckboxLine(trickyLine)).toBeNull(); + + // A line that looks like a checkbox but is missing the char inside brackets + expect(checkboxUtils.matchCheckboxLine('- [] Text')).toBeNull(); // Checkbox regex expects a char + // A line that looks like a checkbox but missing space after brackets (and thus missing listItemText part) + expect(checkboxUtils.matchCheckboxLine('- [x]')).toBeNull(); // Checkbox regex expects \s after [.] + }); + }); + + // --- Тесты для propagateStateToChildren с Plain List Items --- + describe('propagateStateToChildren with Plain List Items', () => { + it('should not change plain list item children', () => { + const text = [ + '- [x] Parent Checkbox', + ' - Plain Child Item', + ' * [ ] Another Checkbox Child', + ' - Deep Plain Child', + ].join('\n'); + const expected = [ + '- [x] Parent Checkbox', + ' - Plain Child Item', // Unchanged + ' * [x] Another Checkbox Child', // Changed + ' - Deep Plain Child', // Unchanged + ].join('\n'); + expect(checkboxUtils.propagateStateToChildren(text, 0)).toBe(expected); + }); + + it('should not propagate from a plain list item parent', () => { + const text = [ + '- Parent Plain Item', // line 0 + ' - [ ] Child Checkbox', + ' * Plain Grandchild', + ].join('\n'); + // No changes expected as parent is NoCheckbox + expect(checkboxUtils.propagateStateToChildren(text, 0)).toBe(text); + }); + + it('should skip ignored children and not skip childs plain list items correctly', () => { + const customSettings = createSettings({ ignoreSymbols: ['~'] }); + const utils = new CheckboxUtils(customSettings); + const text = [ + '- [x] Parent', // 0 + ' - Plain Child', // 1 (NoCheckbox) + ' - [ ] Grandchild CB',// 2 (Checkbox under NoCheckbox, should be affected by Parent) + ' - [~] Ignored Child', // 3 (Ignore) + ' - [ ] Skipped GC', // 4 (Checkbox under Ignore, should not be affected) + ' - [ ] Checkbox Child', // 5 (Checkbox) + ].join('\n'); + const expected = [ + '- [x] Parent', + ' - Plain Child', // Unchanged + ' - [x] Grandchild CB',// Changed + ' - [~] Ignored Child', // Unchanged + ' - [ ] Skipped GC', // Unchanged + ' - [x] Checkbox Child', // Changed + ].join('\n'); + expect(utils.propagateStateToChildren(text, 0)).toBe(expected); + }); + + it('should stop propagation if a non-list-item line is encountered (indent > parent)', () => { + const text = [ + '- [x] Parent', + ' This is just text, not a list item.', // childLineInfo будет null, indent > parent.indent. Цикл прервется. + ' - [ ] Grandchild (should not be processed as loop breaks)', + ].join('\n'); + const expected = [ + '- [x] Parent', + ' This is just text, not a list item.', + ' - [ ] Grandchild (should not be processed as loop breaks)', + ].join('\n'); + // Проверяем, что Grandchild не изменился + const result = checkboxUtils.propagateStateToChildren(text, 0); + expect(result).toBe(expected); + // Дополнительно можно проверить, что matchCheckboxLine для "Grandchild" не вызывался бы + // (если это важно, но для покрытия достаточно проверки результата) + }); + + }); + + // --- Тесты для propagateStateFromChildren с Plain List Items --- + describe('propagateStateFromChildren with Plain List Items', () => { + it('should not update a plain list item parent based on children', () => { + const text = [ + '- Parent Plain Item', + ' - [x] Child Checkbox 1', + ' - [x] Child Checkbox 2', + ].join('\n'); + // Parent is NoCheckbox, so it should not change + expect(checkboxUtils.propagateStateFromChildren(text)).toBe(text); + }); + + it('should not ignore plain list item children when determining parent checkbox state', () => { + const text = [ + '- [ ] Parent Checkbox', + ' - Plain Child 1', + ' - [x] Actual Checkbox Child', + ' * Plain Child 2', + ' - [ ] Grandchild (under plain, ignored for Parent Checkbox)', + ].join('\n'); + const expected = [ + '- [ ] Parent Checkbox', // Becomes checked because "Actual Checkbox Child" is checked + ' - Plain Child 1', + ' - [x] Actual Checkbox Child', + ' * Plain Child 2', + ' - [ ] Grandchild (under plain, ignored for Parent Checkbox)', + ].join('\n'); + expect(checkboxUtils.propagateStateFromChildren(text)).toBe(expected); + }); + + it('parent checkbox remains unchecked if all relevant children are plain or ignored', () => { + const customSettings = createSettings({ ignoreSymbols: ['~'] }); + const utils = new CheckboxUtils(customSettings); + const text = [ + '- [ ] Parent Checkbox', + ' - Plain Child', + ' - [~] Ignored Child', + ].join('\n'); + // No actual checkbox children to influence the parent + expect(utils.propagateStateFromChildren(text)).toBe(text); + + const text2 = [ + '- [x] Parent Checkbox Initially Checked', + ' - Plain Child', + ' - [~] Ignored Child', + ].join('\n'); + // Parent should remain checked as no relevant children to change it + expect(utils.propagateStateFromChildren(text2)).toBe(text2); + }); + + it('parent checkbox becomes unchecked if it has one unchecked checkbox child and other plain/ignored children', () => { + const text = [ + '- [x] Parent Checkbox', + ' - Plain Child', + ' - [ ] Unchecked Checkbox Child', // This will make parent unchecked + ' - [x] Checked Checkbox Child (but one is enough)', + ].join('\n'); + // Note: propagateStateFromChildren processes from bottom up. + // The order of children doesn't matter as much as their collective state. + // Let's simplify to ensure clarity: + const simplerText = [ + '- [x] Parent Checkbox', + ' - Plain Child', + ' - [ ] Unchecked Checkbox Child', + ].join('\n'); + const expected = [ + '- [ ] Parent Checkbox', // Becomes unchecked + ' - Plain Child', + ' - [ ] Unchecked Checkbox Child', + ].join('\n'); + expect(checkboxUtils.propagateStateFromChildren(simplerText)).toBe(expected); + }); + + it('handles mixed children correctly, bottom-up', () => { + const text = [ + '- [ ] GP', // 0 + ' - Plain Child of GP', // 1 + ' - [ ] P1 (Checkbox)', // 2 + ' - [x] C1.1 (Checkbox)', // 3 + ' - Plain Grandchild', // 4 + ' - [x] P2 (Checkbox)', // 5 (initially checked) + ' - Plain Grandchild 2', // 6 + ' - [ ] C2.1 (Unchecked CB)',// 7 + ].join('\n'); + + const expected = [ + '- [ ] GP', // P1 becomes [x], P2 becomes [ ]. So GP is [ ] + ' - Plain Child of GP', + ' - [x] P1 (Checkbox)', // Becomes [x] due to C1.1 + ' - [x] C1.1 (Checkbox)', + ' - Plain Grandchild', + ' - [ ] P2 (Checkbox)', // Becomes [ ] due to C2.1 + ' - Plain Grandchild 2', + ' - [ ] C2.1 (Unchecked CB)', + ].join('\n'); + expect(checkboxUtils.propagateStateFromChildren(text)).toBe(expected); + }); + }); + + // --- Тесты для syncText с Plain List Items (влияние на propagateStateToChildrenFromSingleDiff) --- + describe('syncText (and propagateStateToChildrenFromSingleDiff) with Plain List Items', () => { + const utils = new CheckboxUtils(createSettings({ + enableAutomaticChildState: true, + enableAutomaticParentState: true, // Keep true to see full effect if needed, though child is main focus + })); + + it('should propagate down if a line changes from Plain to Checked Checkbox', () => { + const textBefore = [ + '- Parent Plain Item', // Was plain + ' - [ ] Child Checkbox', + ' - Plain Grandchild', + ].join('\n'); + const textAfter = [ + '- [x] Parent Plain Item', // Now a checked checkbox + ' - [ ] Child Checkbox', + ' - Plain Grandchild', + ].join('\n'); + const expected = [ + '- [x] Parent Plain Item', + ' - [x] Child Checkbox', // Propagated + ' - Plain Grandchild', // Unchanged (it's plain) + ].join('\n'); + expect(utils.syncText(textAfter, textBefore)).toBe(expected); + }); + + it('should NOT propagate down if a line changes from Checked Checkbox to Plain', () => { + const textBefore = [ + '- [x] Parent Checkbox', // Was checkbox + ' - [ ] Child Checkbox', // This would have been checked by propagation + ' - Plain Grandchild', + ].join('\n'); + const textAfter = [ + '- Parent Checkbox', // Now plain + ' - [ ] Child Checkbox', + ' - Plain Grandchild', + ].join('\n'); + // propagateStateToChildrenFromSingleDiff will call propagateStateToChildren + // propagateStateToChildren will see the parent is NoCheckbox and do nothing. + // Then propagateStateFromChildren runs, but parent is NoCheckbox. + expect(utils.syncText(textAfter, textBefore)).toBe(textAfter); + }); + + it('should correctly propagate up if a child changes, parent is checkbox, and siblings are plain', () => { + const textBefore = [ + '- [ ] Parent Checkbox', + ' - Plain Sibling', + ' - [ ] Child Checkbox that changes', + ].join('\n'); + const textAfter = [ + '- [ ] Parent Checkbox', + ' - Plain Sibling', + ' - [x] Child Checkbox that changes', // User checks this + ].join('\n'); + const expected = [ + '- [x] Parent Checkbox', // Propagates up + ' - Plain Sibling', + ' - [x] Child Checkbox that changes', + ].join('\n'); + expect(utils.syncText(textAfter, textBefore)).toBe(expected); + }); + }); +}); diff --git a/__tests__/checkboxUtils.test.ts b/__tests__/checkboxUtils.test.ts deleted file mode 100644 index 027035d..0000000 --- a/__tests__/checkboxUtils.test.ts +++ /dev/null @@ -1,774 +0,0 @@ -import { CheckboxUtils } from '../src/checkboxUtils'; // Путь к вашему файлу -import { CheckboxState, CheckboxSyncPluginSettings, DEFAULT_SETTINGS } from '../src/types'; // Путь к вашему файлу типов - -// Вспомогательная функция для создания настроек с переопределениями -const createSettings = (overrides: Partial = {}): Readonly => { - return { ...DEFAULT_SETTINGS, ...overrides } as Readonly; -}; - -describe('CheckboxUtils', () => { - let checkboxUtils: CheckboxUtils; - let settings: Readonly; - - beforeEach(() => { - // Используем настройки по умолчанию для большинства тестов - settings = createSettings(); - checkboxUtils = new CheckboxUtils(settings); - }); - - // --- Тесты для matchCheckboxLine --- - describe('matchCheckboxLine', () => { - it('should match standard unchecked checkbox', () => { - const line = '- [ ] Task'; - const result = checkboxUtils.matchCheckboxLine(line); - expect(result).not.toBeNull(); - expect(result).toEqual({ - indent: 0, - marker: '-', - checkChar: ' ', - checkboxCharPosition: 3, - checkboxState: CheckboxState.Unchecked, - isChecked: false, - }); - }); - - it('should match standard checked checkbox', () => { - const line = '* [x] Done'; - const result = checkboxUtils.matchCheckboxLine(line); - expect(result).not.toBeNull(); - expect(result).toEqual({ - indent: 0, - marker: '*', - checkChar: 'x', - checkboxCharPosition: 3, - checkboxState: CheckboxState.Checked, - isChecked: true, - }); - }); - - it('should match checkbox with indentation', () => { - const line = ' + [x] Indented Task'; - const result = checkboxUtils.matchCheckboxLine(line); - expect(result).not.toBeNull(); - expect(result).toEqual({ - indent: 2, - marker: '+', - checkChar: 'x', - checkboxCharPosition: 5, // 2 spaces + 1 marker + 1 space + 1 [ = 5 - checkboxState: CheckboxState.Checked, - isChecked: true, - }); - }); - - it('should match numbered list checkbox', () => { - const line = '1. [ ] Numbered'; - const result = checkboxUtils.matchCheckboxLine(line); - expect(result).not.toBeNull(); - expect(result).toEqual({ - indent: 0, - marker: '1.', - checkChar: ' ', - checkboxCharPosition: 4, // 2 marker + 1 space + 1 [ = 4 - checkboxState: CheckboxState.Unchecked, - isChecked: false, - }); - }); - - it('should match checkbox with multi-digit numbered list', () => { - const line = '10. [x] Double Digit'; - const result = checkboxUtils.matchCheckboxLine(line); - expect(result).not.toBeNull(); - expect(result).toEqual({ - indent: 0, - marker: '10.', - checkChar: 'x', - checkboxCharPosition: 5, // 3 marker + 1 space + 1 [ = 5 - checkboxState: CheckboxState.Checked, - isChecked: true, - }); - }); - - it('should return null for lines without checkbox format', () => { - expect(checkboxUtils.matchCheckboxLine('Just text')).toBeNull(); - expect(checkboxUtils.matchCheckboxLine('- Not a checkbox')).toBeNull(); - expect(checkboxUtils.matchCheckboxLine('- [ ]')).toBeNull(); // No space after ] - expect(checkboxUtils.matchCheckboxLine('- [] ')).toBeNull(); // No char inside - expect(checkboxUtils.matchCheckboxLine('[ ] Task')).toBeNull(); // No marker - expect(checkboxUtils.matchCheckboxLine('-[ ] Task')).toBeNull(); // No space after marker - }); - - it('should match custom symbols based on settings', () => { - const customSettings = createSettings({ - checkedSymbols: ['X', 'V'], - uncheckedSymbols: ['O', '-'], - ignoreSymbols: ['~'], - }); - const customUtils = new CheckboxUtils(customSettings); - - const checkedLine = '- [V] Custom Checked'; - const checkedResult = customUtils.matchCheckboxLine(checkedLine); - expect(checkedResult?.checkboxState).toBe(CheckboxState.Checked); - expect(checkedResult?.isChecked).toBe(true); - expect(checkedResult?.checkChar).toBe('V'); - - const uncheckedLine = '* [O] Custom Unchecked'; - const uncheckedResult = customUtils.matchCheckboxLine(uncheckedLine); - expect(uncheckedResult?.checkboxState).toBe(CheckboxState.Unchecked); - expect(uncheckedResult?.isChecked).toBe(false); - expect(uncheckedResult?.checkChar).toBe('O'); - - const ignoredLine = '+ [~] Custom Ignored'; - const ignoredResult = customUtils.matchCheckboxLine(ignoredLine); - expect(ignoredResult?.checkboxState).toBe(CheckboxState.Ignore); - expect(ignoredResult?.isChecked).toBeUndefined(); - expect(ignoredResult?.checkChar).toBe('~'); - }); - }); - - // --- Тесты для getCheckboxState --- - describe('getCheckboxState', () => { - it('should return Checked for symbols in checkedSymbols', () => { - const customSettings = createSettings({ checkedSymbols: ['x', 'X', '✓'] }); - const customUtils = new CheckboxUtils(customSettings); - expect(customUtils.getCheckboxState('x')).toBe(CheckboxState.Checked); - expect(customUtils.getCheckboxState('X')).toBe(CheckboxState.Checked); - expect(customUtils.getCheckboxState('✓')).toBe(CheckboxState.Checked); - }); - - it('should return Unchecked for symbols in uncheckedSymbols', () => { - const customSettings = createSettings({ uncheckedSymbols: [' ', '_', '?'] }); - const customUtils = new CheckboxUtils(customSettings); - expect(customUtils.getCheckboxState(' ')).toBe(CheckboxState.Unchecked); - expect(customUtils.getCheckboxState('_')).toBe(CheckboxState.Unchecked); - expect(customUtils.getCheckboxState('?')).toBe(CheckboxState.Unchecked); - }); - - it('should return Ignore for symbols in ignoreSymbols', () => { - const customSettings = createSettings({ ignoreSymbols: ['-', '~', '>'] }); - const customUtils = new CheckboxUtils(customSettings); - expect(customUtils.getCheckboxState('-')).toBe(CheckboxState.Ignore); - expect(customUtils.getCheckboxState('~')).toBe(CheckboxState.Ignore); - expect(customUtils.getCheckboxState('>')).toBe(CheckboxState.Ignore); - }); - - it('should use unknownSymbolPolicy for symbols not in any list', () => { - const settingsChecked = createSettings({ unknownSymbolPolicy: CheckboxState.Checked }); - const utilsChecked = new CheckboxUtils(settingsChecked); - expect(utilsChecked.getCheckboxState('?')).toBe(CheckboxState.Checked); - - const settingsUnchecked = createSettings({ unknownSymbolPolicy: CheckboxState.Unchecked }); - const utilsUnchecked = new CheckboxUtils(settingsUnchecked); - expect(utilsUnchecked.getCheckboxState('?')).toBe(CheckboxState.Unchecked); - - const settingsIgnore = createSettings({ unknownSymbolPolicy: CheckboxState.Ignore }); - const utilsIgnore = new CheckboxUtils(settingsIgnore); - expect(utilsIgnore.getCheckboxState('?')).toBe(CheckboxState.Ignore); - }); - }); - - // --- Тесты для updateLineCheckboxStateWithInfo --- - describe('updateLineCheckboxStateWithInfo', () => { - it('should update checkbox state from unchecked to checked', () => { - const line = '- [ ] Task'; - const lineInfo = checkboxUtils.matchCheckboxLine(line)!; // Assume valid line - const updatedLine = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, lineInfo); - // Uses the first symbol from settings.checkedSymbols ('x' by default) - expect(updatedLine).toBe('- [x] Task'); - }); - - it('should update checkbox state from checked to unchecked', () => { - const line = '* [x] Done'; - const lineInfo = checkboxUtils.matchCheckboxLine(line)!; - const updatedLine = checkboxUtils.updateLineCheckboxStateWithInfo(line, false, lineInfo); - // Uses the first symbol from settings.uncheckedSymbols (' ' by default) - expect(updatedLine).toBe('* [ ] Done'); - }); - - it('should use first symbol from settings for update', () => { - const customSettings = createSettings({ checkedSymbols: ['V', 'X'], uncheckedSymbols: ['O', ' '] }); - const customUtils = new CheckboxUtils(customSettings); - - const lineToCheck = '- [O] Task'; - const infoToCheck = customUtils.matchCheckboxLine(lineToCheck)!; - const updatedToCheck = customUtils.updateLineCheckboxStateWithInfo(lineToCheck, true, infoToCheck); - expect(updatedToCheck).toBe('- [V] Task'); // Should use 'V' - - const lineToUncheck = '* [V] Done'; - const infoToUncheck = customUtils.matchCheckboxLine(lineToUncheck)!; - const updatedToUncheck = customUtils.updateLineCheckboxStateWithInfo(lineToUncheck, false, infoToUncheck); - expect(updatedToUncheck).toBe('* [O] Done'); // Should use 'O' - }); - - it('should handle indented lines correctly', () => { - const line = ' - [ ] Indented'; - const lineInfo = checkboxUtils.matchCheckboxLine(line)!; - const updatedLine = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, lineInfo); - expect(updatedLine).toBe(' - [x] Indented'); - }); - - it('should return original line if position is invalid (simulated)', () => { - const line = '- [ ] Task'; - const lineInfo = checkboxUtils.matchCheckboxLine(line)!; - // Simulate invalid position - const invalidInfo = { ...lineInfo, checkboxCharPosition: 100 }; - const updatedLine = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, invalidInfo); - expect(updatedLine).toBe(line); // Should not change - }); - - it('should use default checked symbol "x" if checkedSymbols setting is empty', () => { - // Создаем настройки с пустым списком checkedSymbols - const customSettings = createSettings({ checkedSymbols: [] }); - const customUtils = new CheckboxUtils(customSettings); - const line = '- [ ] Task'; - const lineInfo = customUtils.matchCheckboxLine(line)!; - - // Пытаемся отметить чекбокс - const updatedLine = customUtils.updateLineCheckboxStateWithInfo(line, true, lineInfo); - expect(updatedLine).toBe('- [x] Task'); // Ожидаем дефолтный 'x' - }); - - it('should use default unchecked symbol " " if uncheckedSymbols setting is empty', () => { - // Создаем настройки с пустым списком uncheckedSymbols - const customSettings = createSettings({ uncheckedSymbols: [] }); - const customUtils = new CheckboxUtils(customSettings); - const line = '- [x] Task'; // Используем 'x' из дефолтных checkedSymbols - const lineInfo = customUtils.matchCheckboxLine(line)!; - - // Пытаемся снять отметку - const updatedLine = customUtils.updateLineCheckboxStateWithInfo(line, false, lineInfo); - expect(updatedLine).toBe('- [ ] Task'); // Ожидаем дефолтный ' ' (пробел) - }); - - // Усиленный тест для невалидной позиции (с проверкой console.warn) - it('should return original line and warn if position is invalid', () => { - const line = '- [ ] Task'; - const lineInfo = checkboxUtils.matchCheckboxLine(line)!; - // Мокаем console.warn - const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { }); - - // Simulate invalid position - const invalidInfo = { ...lineInfo, checkboxCharPosition: -1 }; // Невалидная позиция - const updatedLineNegative = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, invalidInfo); - expect(updatedLineNegative).toBe(line); // Должен вернуть оригинальную строку - expect(warnSpy).toHaveBeenCalledTimes(1); // Проверяем вызов warn - - const invalidInfo2 = { ...lineInfo, checkboxCharPosition: 100 }; // Другая невалидная позиция - const updatedLineOob = checkboxUtils.updateLineCheckboxStateWithInfo(line, true, invalidInfo2); - expect(updatedLineOob).toBe(line); // Должен вернуть оригинальную строку - expect(warnSpy).toHaveBeenCalledTimes(2); // Проверяем вызов warn еще раз - - // Восстанавливаем оригинальную функцию console.warn - warnSpy.mockRestore(); - }); - }); - - // --- Тесты для propagateStateToChildren --- - describe('propagateStateToChildren', () => { - const setupUtils = (customSettings?: Partial) => { - return new CheckboxUtils(createSettings(customSettings)); - } - - it('should check children when parent is checked', () => { - const text = [ - '- [x] Parent', // line 0 - ' - [ ] Child 1', - ' - [ ] Grandchild', - ' - [ ] Child 2', - '- [ ] Sibling', - ].join('\n'); - const expected = [ - '- [x] Parent', - ' - [x] Child 1', - ' - [x] Grandchild', - ' - [x] Child 2', - '- [ ] Sibling', // Sibling should not change - ].join('\n'); - expect(checkboxUtils.propagateStateToChildren(text, 0)).toBe(expected); - }); - - it('should uncheck children when parent is unchecked', () => { - const text = [ - '- [ ] Parent', // line 0 - ' - [x] Child 1', - ' - [x] Grandchild', - ' - [ ] Child 2', // Already unchecked - '- [x] Sibling', - ].join('\n'); - const expected = [ - '- [ ] Parent', - ' - [ ] Child 1', - ' - [ ] Grandchild', - ' - [ ] Child 2', - '- [x] Sibling', // Sibling should not change - ].join('\n'); - expect(checkboxUtils.propagateStateToChildren(text, 0)).toBe(expected); - }); - - it('should stop propagation at siblings or less indented lines', () => { - const text = [ - ' - [x] Parent', // line 0 - ' - [ ] Child', - ' - [ ] Sibling', // Same indent level - '- [ ] Less Indented', - ].join('\n'); - const expected = [ - ' - [x] Parent', - ' - [x] Child', // Changed - ' - [ ] Sibling', // Not changed - '- [ ] Less Indented', // Not changed - ].join('\n'); - expect(checkboxUtils.propagateStateToChildren(text, 0)).toBe(expected); - }); - - it('should skip ignored children and their subtrees', () => { - const utils = setupUtils({ ignoreSymbols: ['~'] }); - const text = [ - '- [x] Parent', // 0 - ' - [ ] Child 1', // 1 - ' - [~] Ignored Child',// 2 (ignore) - ' - [ ] Skipped GC', // 3 (skipped because parent ignored) - ' - [ ] Child 2', // 4 - ].join('\n'); - const expected = [ - '- [x] Parent', - ' - [x] Child 1', // Changed - ' - [~] Ignored Child',// Unchanged - ' - [ ] Skipped GC', // Unchanged - ' - [x] Child 2', // Changed - ].join('\n'); - expect(utils.propagateStateToChildren(text, 0)).toBe(expected); - }); - - it('should do nothing if the parent line is not a checkbox', () => { - const text = [ - 'Parent Line', - ' - [ ] Child', - ].join('\n'); - expect(checkboxUtils.propagateStateToChildren(text, 0)).toBe(text); - }); - - it('should do nothing if the parent checkbox is ignored', () => { - const utils = setupUtils({ ignoreSymbols: ['~'] }); - const text = [ - '- [~] Ignored Parent', // 0 - ' - [ ] Child', - ].join('\n'); - expect(utils.propagateStateToChildren(text, 0)).toBe(text); - }); - }); - - // --- Тесты для propagateStateFromChildren --- - describe('propagateStateFromChildren', () => { - const setupUtils = (customSettings?: Partial) => { - return new CheckboxUtils(createSettings(customSettings)); - } - - it('should check parent if all children are checked', () => { - const text = [ - '- [ ] Parent', // line 0 - ' - [x] Child 1', - ' - [x] Child 2', - ' - [x] Grandchild', // Needs parent (Child 2) to be checked first - ].join('\n'); - // Expected: Grandchild affects Child 2 -> Child 2 and Child 1 affect Parent - const expectedPass1 = [ // After processing Grandchild and Child 2 - '- [ ] Parent', - ' - [x] Child 1', - ' - [x] Child 2', // Stays checked - ' - [x] Grandchild', - ].join('\n'); - const expectedPass2 = [ // After processing Child 1 and Parent - '- [x] Parent', // Changes to checked - ' - [x] Child 1', - ' - [x] Child 2', - ' - [x] Grandchild', - ].join('\n'); - // Since it processes bottom-up, Child 2 should already be evaluated correctly - expect(checkboxUtils.propagateStateFromChildren(text)).toBe(expectedPass2); - }); - - it('should uncheck parent if any child is unchecked', () => { - const text = [ - '- [x] Parent', // line 0 - ' - [x] Child 1', - ' - [ ] Child 2', // This one makes the parent unchecked - ' - [ ] Grandchild', - ].join('\n'); - const expected = [ - '- [ ] Parent', // Changes to unchecked - ' - [x] Child 1', - ' - [ ] Child 2', - ' - [ ] Grandchild', - ].join('\n'); - expect(checkboxUtils.propagateStateFromChildren(text)).toBe(expected); - }); - - it('should handle nested updates correctly (bottom-up)', () => { - const text = [ - '- [ ] Grandparent', // 0 - ' - [ ] Parent 1', // 1 - ' - [x] Child 1.1',// 2 - ' - [x] Child 1.2',// 3 - ' - [x] Parent 2', // 4 - ' - [ ] Child 2.1',// 5 (Makes Parent 2 unchecked) - ].join('\n'); - const expected = [ - '- [ ] Grandparent', // Becomes unchecked (because Parent 2 becomes unchecked) - ' - [x] Parent 1', // Becomes checked (Child 1.1, 1.2 are checked) - ' - [x] Child 1.1', - ' - [x] Child 1.2', - ' - [ ] Parent 2', // Becomes unchecked (Child 2.1 is unchecked) - ' - [ ] Child 2.1', - ].join('\n'); - expect(checkboxUtils.propagateStateFromChildren(text)).toBe(expected); - }); - - it('should not change parent state if it has no relevant children', () => { - const text = [ - '- [ ] Parent', - 'Sibling', // Not a child checkbox - ' Not indented correctly', - ].join('\n'); - expect(checkboxUtils.propagateStateFromChildren(text)).toBe(text); - }); - - it('should ignore non-checkbox lines when determining parent state', () => { - const text = [ - '- [ ] Parent', - ' - [x] Child 1', - ' Just text', - ' - [x] Child 2', - ].join('\n'); - const expected = [ - '- [x] Parent', // Should become checked - ' - [x] Child 1', - ' Just text', - ' - [x] Child 2', - ].join('\n'); - expect(checkboxUtils.propagateStateFromChildren(text)).toBe(expected); - }); - - it('should skip ignored children when determining parent state', () => { - const utils = setupUtils({ ignoreSymbols: ['~'] }); - const text = [ - '- [ ] Parent', // 0 - ' - [x] Child 1', // 1 - ' - [~] Ignored Child',// 2 (skipped) - ' - [ ] Skipped GC', // 3 (doesn't affect parent) - ' - [x] Child 2', // 4 - ].join('\n'); - const expected = [ - '- [x] Parent', // Becomes checked (based on Child 1 and Child 2 only) - ' - [x] Child 1', - ' - [~] Ignored Child', - ' - [ ] Skipped GC', - ' - [x] Child 2', - ].join('\n'); - expect(utils.propagateStateFromChildren(text)).toBe(expected); - }); - - it('should not change parent state if all direct children are ignored', () => { - const utils = setupUtils({ ignoreSymbols: ['~'] }); - const text = [ - '- [ ] Parent', // 0 - ' - [~] Ignored 1', // 1 - ' - [~] Ignored 2', // 2 - ].join('\n'); - expect(utils.propagateStateFromChildren(text)).toBe(text); // Parent stays unchecked - const text2 = [ - '- [x] Parent', // 0 - ' - [~] Ignored 1', // 1 - ' - [~] Ignored 2', // 2 - ].join('\n'); - expect(utils.propagateStateFromChildren(text2)).toBe(text2); // Parent stays checked - }); - - it('should not update ignored parents', () => { - const utils = setupUtils({ ignoreSymbols: ['~'] }); - const text = [ - '- [~] Ignored Parent', // 0 - ' - [x] Child 1', - ' - [x] Child 2', - ].join('\n'); - expect(utils.propagateStateFromChildren(text)).toBe(text); // Parent remains ignored - }); - - it('should stop searching children when a line with equal or lesser indent is found', () => { - const text = [ - '- [ ] Parent 1', // indent 0 - 'Some regular text', // indent 0. Stops search for Parent 1 children. - ' - [x] Child 1.1', // indent 2. Should be ignored for Parent 1. - '- [ ] Parent 2', // indent 0. Processed independently. - ' - [x] Child 2.1', // indent 2. Child of Parent 2. - ].join('\n'); - - const result = checkboxUtils.propagateStateFromChildren(text); - - const expected = [ - '- [ ] Parent 1', // Unchanged (no children found before stop) - 'Some regular text', - ' - [x] Child 1.1', - '- [x] Parent 2', // Updated by Child 2.1 - ' - [x] Child 2.1', - ].join('\n'); - expect(result).toBe(expected); - }); - - - it('should stop searching children when a sibling checkbox (equal/lesser indent) is found', () => { - const text = [ - '- [ ] Parent 1', // indent 0 - '- [ ] Sibling Parent', // indent 0. Stops search for Parent 1 children. - ' - [x] Child SP.1', // indent 2. Belongs to Sibling Parent. - ].join('\n'); - - const result = checkboxUtils.propagateStateFromChildren(text); - - const expected = [ - '- [ ] Parent 1', // Unchanged (no children found before stop) - '- [x] Sibling Parent', // Updated by Child SP.1 - ' - [x] Child SP.1', - ].join('\n'); - expect(result).toBe(expected); - - const text2 = [ - ' - [ ] Parent 1', // indent 2 - ' - [x] Child 1.1', // indent 4 - ' - [ ] Sibling Parent', // indent 2. Stops search for Parent 1 children. - ' - [ ] Child SP.1', // indent 4. Belongs to Sibling Parent. - ].join('\n'); - const result2 = checkboxUtils.propagateStateFromChildren(text2); - const expected2 = [ - ' - [x] Parent 1', // Updated by Child 1.1 - ' - [x] Child 1.1', - ' - [ ] Sibling Parent', // Unchanged (Child SP.1 is unchecked) - ' - [ ] Child SP.1', - ].join('\n'); - expect(result2).toBe(expected2); - }); - }); - - // --- Тесты для syncText --- - describe('syncText', () => { - const textBefore = [ - '- [ ] Parent', - ' - [ ] Child', - ].join('\n'); - const textAfterParentChecked = [ // Simulate user checking parent - '- [x] Parent', - ' - [ ] Child', - ].join('\n'); - const textAfterChildChecked = [ // Simulate user checking child - '- [ ] Parent', - ' - [x] Child', - ].join('\n'); - - it('should propagate down only if child sync enabled and diff matches', () => { - const utils = new CheckboxUtils(createSettings({ - enableAutomaticChildState: true, - enableAutomaticParentState: false, // Parent sync off - })); - const expected = [ - '- [x] Parent', - ' - [x] Child', // Propagated down - ].join('\n'); - expect(utils.syncText(textAfterParentChecked, textBefore)).toBe(expected); - }); - - it('should propagate up only if parent sync enabled', () => { - const utils = new CheckboxUtils(createSettings({ - enableAutomaticChildState: false, // Child sync off - enableAutomaticParentState: true, - })); - const expected = [ - '- [x] Parent', // Propagated up - ' - [x] Child', - ].join('\n'); - // syncText calls propagateFromChildren unconditionally if enabled - expect(utils.syncText(textAfterChildChecked, textBefore)).toBe(expected); - }); - - it('should propagate down then up if both enabled', () => { - const utils = new CheckboxUtils(createSettings({ - enableAutomaticChildState: true, - enableAutomaticParentState: true, - })); - // 1. User checks parent: '- [x] Parent', ' - [ ] Child' - // 2. Propagate down: '- [x] Parent', ' - [x] Child' - // 3. Propagate up: (no change needed as parent is already checked) - const expectedDown = [ - '- [x] Parent', - ' - [x] Child', // Propagated down - ].join('\n'); - expect(utils.syncText(textAfterParentChecked, textBefore)).toBe(expectedDown); - - // 1. User checks child: '- [ ] Parent', ' - [x] Child' - // 2. Propagate down: (no change, as only one line changed, and it wasn't the parent) - // 3. Propagate up: '- [x] Parent', ' - [x] Child' - const expectedUp = [ - '- [x] Parent', // Propagated up - ' - [x] Child', - ].join('\n'); - expect(utils.syncText(textAfterChildChecked, textBefore)).toBe(expectedUp); - }); - - it('should do nothing if both syncs disabled', () => { - const utils = new CheckboxUtils(createSettings({ - enableAutomaticChildState: false, - enableAutomaticParentState: false, - })); - expect(utils.syncText(textAfterParentChecked, textBefore)).toBe(textAfterParentChecked); - expect(utils.syncText(textAfterChildChecked, textBefore)).toBe(textAfterChildChecked); - }); - - it('should not propagate down if diff is not a single checkbox state change', () => { - const utils = new CheckboxUtils(createSettings({ - enableAutomaticChildState: true, // Down propagation enabled - enableAutomaticParentState: false, - })); - const textMultipleChanges = [ - '- [x] Parent Changed', // Text changed too - ' - [x] Child also changed', - ].join('\n'); - const textIndentChange = [ - ' - [x] Parent', // Indent changed - ' - [ ] Child', - ].join('\n'); - - // Multiple lines changed - expect(utils.syncText(textMultipleChanges, textBefore)).toBe(textMultipleChanges); - // Indent changed (diff > 1 line index) - expect(utils.syncText(textIndentChange, textBefore)).toBe(textIndentChange); - }); - }); - - // --- Тесты для propagateStateToChildrenFromSingleDiff --- - describe('propagateStateToChildrenFromSingleDiff', () => { - const textBefore = [ - '- [ ] Parent', - ' - [ ] Child', - ].join('\n'); - - it('should propagate if only checkbox state changed', () => { - const textAfter = [ - '- [x] Parent', // Only state changed - ' - [ ] Child', - ].join('\n'); - const expected = [ - '- [x] Parent', - ' - [x] Child', // Propagated - ].join('\n'); - expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(expected); - }); - - it('should propagate if text content also changed', () => { - const textAfter = [ - '- [x] Parent Changed Text', // State and text changed - ' - [ ] Child', - ].join('\n'); - const expected = [ - '- [x] Parent Changed Text', - ' - [x] Child', // Propagated - ].join('\n'); - expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(expected); - }); - - it('should NOT propagate if marker changed', () => { - const textAfter = [ - '* [x] Parent', // Marker changed from - to * - ' - [ ] Child', - ].join('\n'); - expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(textAfter); - }); - - it('should NOT propagate if indentation changed', () => { - const textAfter = [ - ' - [x] Parent', // Indentation changed - ' - [ ] Child', - ].join('\n'); - // This will be detected as > 1 line difference by findDifferentLineIndexes - expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(textAfter); - }); - - - it('should NOT propagate if multiple lines changed', () => { - const textAfter = [ - '- [x] Parent', - ' - [x] Child', // Second line also changed - ].join('\n'); - expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfter, textBefore)).toBe(textAfter); - }); - - it('should NOT propagate if changed line is not a checkbox', () => { - const textBeforeNonCheck = "Line 1\nLine 2"; - const textAfterNonCheck = "Line 1 changed\nLine 2"; - expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(textAfterNonCheck, textBeforeNonCheck)).toBe(textAfterNonCheck); - }); - - it('should propagate if changed line is an ignored checkbox', () => { - const utils = new CheckboxUtils(createSettings({ ignoreSymbols: ['~'] })); - const textBeforeIgnored = [ - '- [~] Parent', - ' - [ ] Child', - ].join('\n'); - const textAfterIgnored = [ - '- [x] Parent', // Changed FROM ignored TO checked - ' - [ ] Child', - ].join('\n'); - const expected = [ - '- [x] Parent', - ' - [x] Child', // Propagated - ].join('\n'); - - expect(utils.propagateStateToChildrenFromSingleDiff(textAfterIgnored, textBeforeIgnored)).toBe(expected); - }); - - it('should return original text if textBefore is undefined', () => { - const text = '- [ ] Line'; - expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(text, undefined)).toBe(text); - }); - - it('should return original text if line counts differ', () => { - const text1 = '- [ ] Line1\n- [ ] Line2'; - const text2 = '- [ ] Line1'; - expect(checkboxUtils.propagateStateToChildrenFromSingleDiff(text1, text2)).toBe(text1); - }); - }); - - // --- Тесты для findDifferentLineIndexes --- - describe('findDifferentLineIndexes', () => { - it('should return empty array for identical lines', () => { - const lines1 = ['a', 'b', 'c']; - const lines2 = ['a', 'b', 'c']; - expect(checkboxUtils.findDifferentLineIndexes(lines1, lines2)).toEqual([]); - }); - - it('should return index of the single different line', () => { - const lines1 = ['a', 'b', 'c']; - const lines2 = ['a', 'B', 'c']; - expect(checkboxUtils.findDifferentLineIndexes(lines1, lines2)).toEqual([1]); - }); - - it('should return indexes of multiple different lines', () => { - const lines1 = ['a', 'b', 'c', 'd']; - const lines2 = ['A', 'b', 'C', 'd']; - expect(checkboxUtils.findDifferentLineIndexes(lines1, lines2)).toEqual([0, 2]); - }); - - it('should detect differences at start and end', () => { - const lines1 = ['a', 'b', 'c']; - const lines2 = ['X', 'b', 'Y']; - expect(checkboxUtils.findDifferentLineIndexes(lines1, lines2)).toEqual([0, 2]); - }); - - it('should throw error if line arrays have different lengths', () => { - const lines1 = ['a', 'b']; - const lines2 = ['a', 'b', 'c']; - expect(() => checkboxUtils.findDifferentLineIndexes(lines1, lines2)) - .toThrow("the length of the lines must be equal"); - expect(() => checkboxUtils.findDifferentLineIndexes(lines2, lines1)) - .toThrow("the length of the lines must be equal"); - }); - }); - -}); \ No newline at end of file diff --git a/jest.config.ts b/jest.config.ts index 38d873b..e061c34 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -17,7 +17,8 @@ const config: Config = { coverageDirectory: "coverage", // Indicates which provider should be used to instrument code for coverage - coverageProvider: "v8", + // coverageProvider: "v8", + coverageProvider: "babel", // An array of file extensions your modules use moduleFileExtensions: [ diff --git a/src/checkboxUtils.ts b/src/checkboxUtils.ts index 070c167..dfc6e55 100644 --- a/src/checkboxUtils.ts +++ b/src/checkboxUtils.ts @@ -1,212 +1,246 @@ import { CheckboxState, CheckboxSyncPluginSettings } from "./types"; export interface CheckboxLineInfo { - indent: number; - marker: string; - checkChar: string; - checkboxCharPosition: number; - checkboxState: CheckboxState; - isChecked: boolean | undefined; + indent: number; + marker: string; + checkChar?: string; + checkboxCharPosition?: number; + checkboxState: CheckboxState; + isChecked?: boolean | undefined; + listItemText?: string; } export class CheckboxUtils { - private settings: Readonly; - constructor(settings: Readonly) { - this.settings = settings; - } + private settings: Readonly; + constructor(settings: Readonly) { + this.settings = settings; + } - matchCheckboxLine(line: string): CheckboxLineInfo | null { - const match = line.match(/^(\s*)([*+-]|\d+\.) \[(.)\]\s/); - if (!match) { - return null; - } - const indent = match[1].length; - const marker = match[2]; - const checkChar = match[3]; - const checkboxCharPosition = indent + marker.length + 2; - const checkboxState = this.getCheckboxState(checkChar); - let isChecked = checkboxState === CheckboxState.Ignore ? undefined : checkboxState === CheckboxState.Checked; + matchCheckboxLine(line: string): CheckboxLineInfo | null { + // const checkboxMatch = line.match(/^(\s*)([*+-]|\d+\.) \[(.)\](\s.*)?$/); + // const checkboxMatch = line.match(/^(\s*)([*+-]|\d+\.) \[(.)\](\s|$)(.*)$/); + const checkboxMatch = line.match(/^(\s*)([*+-]|\d+\.) \[(.)\]\s(.*)$/); + if (checkboxMatch) { + const indent = checkboxMatch[1].length; + const marker = checkboxMatch[2]; + const checkChar = checkboxMatch[3]; + const checkboxCharPosition = indent + marker.length + 2; + const checkboxState = this.getCheckboxState(checkChar); + const isChecked = checkboxState === CheckboxState.Ignore ? undefined : checkboxState === CheckboxState.Checked; + const listItemText = (checkboxMatch[4] || "").trimStart(); - return { - indent, - marker, - checkChar, - checkboxCharPosition, - checkboxState, - isChecked - }; - } + return { + indent, + marker, + checkChar, + checkboxCharPosition, + checkboxState, + isChecked, + listItemText + }; + } + const listItemMatch = line.match(/^(\s*)([*+-]|\d+\.)\s+(?!\[.?\])(.*)$/); + if (listItemMatch) { + const indent = listItemMatch[1].length; + const marker = listItemMatch[2]; + const listItemText = listItemMatch[3].trimStart(); - getCheckboxState(text: string): CheckboxState { - if (this.settings.checkedSymbols.includes(text)) { - return CheckboxState.Checked; - } - if (this.settings.uncheckedSymbols.includes(text)) { - return CheckboxState.Unchecked; - } - if (this.settings.ignoreSymbols.includes(text)) { - return CheckboxState.Ignore; - } - return this.settings.unknownSymbolPolicy; - } + return { + indent, + marker, + // checkChar, checkboxCharPosition, isChecked - не применимы + checkboxState: CheckboxState.NoCheckbox, + listItemText + }; + } + return null; + } - updateLineCheckboxStateWithInfo(line: string, shouldBeChecked: boolean, lineInfo: CheckboxLineInfo): string { - const checkedChar = this.settings.checkedSymbols.length > 0 ? this.settings.checkedSymbols[0] : 'x'; // 'x' как дефолт - const uncheckedChar = this.settings.uncheckedSymbols.length > 0 ? this.settings.uncheckedSymbols[0] : ' '; // ' ' как дефолт + getCheckboxState(text: string): CheckboxState { + if (this.settings.checkedSymbols.includes(text)) { + return CheckboxState.Checked; + } + if (this.settings.uncheckedSymbols.includes(text)) { + return CheckboxState.Unchecked; + } + if (this.settings.ignoreSymbols.includes(text)) { + return CheckboxState.Ignore; + } + return this.settings.unknownSymbolPolicy; + } - const newCheckChar = shouldBeChecked ? checkedChar : uncheckedChar; - const pos = lineInfo.checkboxCharPosition; - if (pos >= 0 && pos < line.length) { - return line.substring(0, pos) + newCheckChar + line.substring(pos + 1); - } - console.warn("updateLineCheckboxStateWithInfo: Invalid checkbox position in lineInfo for line:", line); - return line; - } + updateLineCheckboxStateWithInfo(line: string, shouldBeChecked: boolean, lineInfo: CheckboxLineInfo): string { + if (lineInfo.checkboxState !== CheckboxState.NoCheckbox) { + const checkedChar = this.settings.checkedSymbols.length > 0 ? this.settings.checkedSymbols[0] : 'x'; // 'x' как дефолт + const uncheckedChar = this.settings.uncheckedSymbols.length > 0 ? this.settings.uncheckedSymbols[0] : ' '; // ' ' как дефолт - propagateStateToChildren(text: string, parentLine: number): string { - // console.log("propagateStateToChildren"); + const newCheckChar = shouldBeChecked ? checkedChar : uncheckedChar; + const pos = lineInfo.checkboxCharPosition!; + if (pos >= 0 && pos < line.length) { + return line.substring(0, pos) + newCheckChar + line.substring(pos + 1); + } else { + console.warn("updateLineCheckboxStateWithInfo: Invalid checkbox position in lineInfo for line:", line); + return line; + } + } + console.warn("updateLineCheckboxStateWithInfo: Invalid lineInfo(no checkbox) for line:", line); + return line; + } - const lines = text.split("\n"); + propagateStateToChildren(text: string, parentLine: number): string { + const lines = text.split("\n"); + const parentLineInfo = this.matchCheckboxLine(lines[parentLine]); - const parentLineInfo = this.matchCheckboxLine(lines[parentLine]); - if (!parentLineInfo) { - console.warn(`checkbox not found in line ${parentLine}`) - return text; - } + if (!parentLineInfo) { + console.warn(`checkbox not found in line ${parentLine}`) + return text; + } - if (parentLineInfo.checkboxState === CheckboxState.Ignore) { - return text; - } - let parentIsChecked = parentLineInfo.isChecked!; + if (parentLineInfo.checkboxState === CheckboxState.Ignore || + parentLineInfo.checkboxState === CheckboxState.NoCheckbox || + parentLineInfo.isChecked === undefined) { + return text; + } + const parentIsChecked = parentLineInfo.isChecked; - let j = parentLine + 1; + let j = parentLine + 1; - while (j < lines.length) { - const childText = lines[j]; - const childLineInfo = this.matchCheckboxLine(childText); - if (!childLineInfo || childLineInfo.indent <= parentLineInfo.indent) break; + while (j < lines.length) { + const childText = lines[j]; + const childLineInfo = this.matchCheckboxLine(childText); + if (!childLineInfo || childLineInfo.indent <= parentLineInfo.indent) { + break; + } - if (childLineInfo.checkboxState !== CheckboxState.Ignore) { - lines[j] = this.updateLineCheckboxStateWithInfo(lines[j], parentIsChecked, childLineInfo); - j++; - } else { - //skip children ignore node - j++; - while (j < lines.length) { - const subChildLineInfo = this.matchCheckboxLine(lines[j]); - if (!subChildLineInfo || subChildLineInfo.indent <= childLineInfo.indent) { - break; - }; - j++; - } - } - } - return lines.join("\n"); - } + if (childLineInfo.checkboxState === CheckboxState.Ignore) { + //skip children ignore node + j++; + while (j < lines.length) { + const subChildLineInfo = this.matchCheckboxLine(lines[j]); + if (!subChildLineInfo || subChildLineInfo.indent <= childLineInfo.indent) { + break; + }; + j++; + } + } else if (childLineInfo.checkboxState === CheckboxState.NoCheckbox) { + j++; + } else { + lines[j] = this.updateLineCheckboxStateWithInfo(lines[j], parentIsChecked, childLineInfo); + j++; + } + } + return lines.join("\n"); + } - propagateStateFromChildren(text: string): string { - const lines = text.split("\n"); + propagateStateFromChildren(text: string): string { + const lines = text.split("\n"); - for (let i = lines.length - 1; i >= 0; i--) { - const parentLineInfo = this.matchCheckboxLine(lines[i]); - if (!parentLineInfo) continue; + for (let i = lines.length - 1; i >= 0; i--) { + const parentLineInfo = this.matchCheckboxLine(lines[i]); + if (!parentLineInfo) continue; - if (parentLineInfo.checkboxState === CheckboxState.Ignore) { - continue; - } - const parentIsChecked = parentLineInfo.isChecked!; - let allChildrenChecked = true; - let hasChildren = false; + if ( + parentLineInfo.checkboxState === CheckboxState.Ignore || + parentLineInfo.checkboxState === CheckboxState.NoCheckbox + ) { + continue; + } + const parentIsChecked = parentLineInfo.isChecked!; + let allRelevantChildrenChecked = true; + let hasRelevantChildren = false; - for (let j = i + 1; j < lines.length; j++) { - const childLineInfo = this.matchCheckboxLine(lines[j]); - - if (!childLineInfo || childLineInfo.indent <= parentLineInfo.indent) break; + for (let j = i + 1; j < lines.length; j++) { + const childLineInfo = this.matchCheckboxLine(lines[j]); - if (childLineInfo.checkboxState === CheckboxState.Ignore) { - while (j + 1 < lines.length) { - const subChildLineInfo = this.matchCheckboxLine(lines[j + 1]); - if (!subChildLineInfo || subChildLineInfo.indent <= childLineInfo.indent) { - break; - }; - j++; - } - continue; - } - hasChildren = true; - const childrenIsChecked = childLineInfo.isChecked!; - if (!childrenIsChecked) { - allChildrenChecked = false; - break; - } - } + if (!childLineInfo || childLineInfo.indent <= parentLineInfo.indent) break; - if (hasChildren && parentIsChecked !== allChildrenChecked) { - lines[i] = this.updateLineCheckboxStateWithInfo(lines[i], allChildrenChecked, parentLineInfo); - } - } - return lines.join("\n"); - } + if (childLineInfo.checkboxState === CheckboxState.Ignore) { + while (j + 1 < lines.length) { + const subChildLineInfo = this.matchCheckboxLine(lines[j + 1]); + if (!subChildLineInfo || subChildLineInfo.indent <= childLineInfo.indent) { + break; + }; + j++; + } + continue; + } + if (childLineInfo.checkboxState === CheckboxState.NoCheckbox) { + continue; + } + hasRelevantChildren = true; + const childrenIsChecked = childLineInfo.isChecked!; + if (!childrenIsChecked) { + allRelevantChildrenChecked = false; + break; + } + } - syncText(text: string, textBefore: string | undefined): string { - let newText = text; - if (this.settings.enableAutomaticChildState) { - newText = this.propagateStateToChildrenFromSingleDiff(text, textBefore); - } - if (this.settings.enableAutomaticParentState) { - newText = this.propagateStateFromChildren(newText); - } - return newText; - } + if (hasRelevantChildren && parentIsChecked !== allRelevantChildrenChecked) { + lines[i] = this.updateLineCheckboxStateWithInfo(lines[i], allRelevantChildrenChecked, parentLineInfo); + } + } + return lines.join("\n"); + } - propagateStateToChildrenFromSingleDiff(text: string, textBefore: string | undefined): string { - if (!textBefore) return text; - const textBeforeLines = textBefore.split('\n'); - const textLines = text.split('\n'); + syncText(text: string, textBefore: string | undefined): string { + let newText = text; + if (this.settings.enableAutomaticChildState) { + newText = this.propagateStateToChildrenFromSingleDiff(text, textBefore); + } + if (this.settings.enableAutomaticParentState) { + newText = this.propagateStateFromChildren(newText); + } + return newText; + } - if (textBeforeLines.length !== textLines.length) return text; + propagateStateToChildrenFromSingleDiff(text: string, textBefore: string | undefined): string { + if (!textBefore) return text; + const textBeforeLines = textBefore.split('\n'); + const textLines = text.split('\n'); - const diffIndexes = this.findDifferentLineIndexes(textBeforeLines, textLines); - if (diffIndexes.length !== 1) { - return text; - } + if (textBeforeLines.length !== textLines.length) return text; - const index = diffIndexes[0]; + const diffIndexes = this.findDifferentLineIndexes(textBeforeLines, textLines); + if (diffIndexes.length !== 1) { + return text; + } - const lineBefore = textBeforeLines[index]; - const lineAfter = textLines[index]; - // Получаем информацию о чекбоксе ДО и ПОСЛЕ изменения - const lineInfoBefore = this.matchCheckboxLine(lineBefore); - const lineInfoAfter = this.matchCheckboxLine(lineAfter); - // Проверяем, что: - // 1. Обе строки (до и после) являются валидными строками с чекбоксом. - // 2. Отступ и маркер списка не изменились (т.е. структура строки та же). - // 3. Изменился именно символ внутри скобок [ ]. - if ( - lineInfoBefore && lineInfoAfter && - lineInfoBefore.indent === lineInfoAfter.indent && - lineInfoBefore.marker === lineInfoAfter.marker && - lineInfoBefore.checkboxState !== lineInfoAfter.checkboxState - ) { - return this.propagateStateToChildren(text, diffIndexes[0]); - } - return text; + const index = diffIndexes[0]; - } + const lineBefore = textBeforeLines[index]; + const lineAfter = textLines[index]; + // Получаем информацию о чекбоксе ДО и ПОСЛЕ изменения + const lineInfoBefore = this.matchCheckboxLine(lineBefore); + const lineInfoAfter = this.matchCheckboxLine(lineAfter); + // Проверяем, что: + // 1. Обе строки (до и после) являются валидными строками с чекбоксом. + // 2. Отступ и маркер списка не изменились (т.е. структура строки та же). + // 3. Изменился именно символ внутри скобок [ ]. + if ( + lineInfoBefore && lineInfoAfter && + lineInfoBefore.indent === lineInfoAfter.indent && + lineInfoBefore.marker === lineInfoAfter.marker && + lineInfoBefore.checkboxState !== lineInfoAfter.checkboxState + ) { + return this.propagateStateToChildren(text, diffIndexes[0]); + } + return text; - findDifferentLineIndexes(lines1: string[], lines2: string[]): number[] { - if (lines1.length !== lines2.length) { - throw new Error("the length of the lines must be equal"); - } + } - const length = lines1.length; - const result: number[] = []; - for (let i = 0; i < length; i++) { - if (lines1[i] !== lines2[i]) { - result.push(i); - } - } - return result; - } + findDifferentLineIndexes(lines1: string[], lines2: string[]): number[] { + if (lines1.length !== lines2.length) { + throw new Error("the length of the lines must be equal"); + } -} \ No newline at end of file + const length = lines1.length; + const result: number[] = []; + for (let i = 0; i < length; i++) { + if (lines1[i] !== lines2[i]) { + result.push(i); + } + } + return result; + } + +} diff --git a/src/types.ts b/src/types.ts index 0072b49..6c16730 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,7 +13,8 @@ export interface CheckboxSyncPluginSettings { export enum CheckboxState { Checked = 'checked', Unchecked = 'unchecked', - Ignore = 'ignore' + Ignore = 'ignore', + NoCheckbox = "noCheckbox" } export const DEFAULT_SETTINGS: CheckboxSyncPluginSettings = {