This commit is contained in:
Grol Grol 2025-04-11 18:07:53 +03:00
parent 32a5fe4229
commit 198adf253f
11 changed files with 892 additions and 241 deletions

View file

@ -0,0 +1,570 @@
// checkboxUtils.test.ts
import { CheckboxUtils, CheckboxLineInfo } from '../src/checkboxUtils';
import { CheckboxSyncPluginSettings } from '../src/types'; // Предполагаем, что типы в types.ts
// --- Mock Settings ---
const defaultSettings: Readonly<CheckboxSyncPluginSettings> = {
xOnlyMode: false, // По умолчанию: любой не-пробел считается 'checked'
};
const xOnlySettings: Readonly<CheckboxSyncPluginSettings> = {
xOnlyMode: true, // Только 'x' считается 'checked'
};
// --- Test Suite ---
describe('CheckboxUtils', () => {
let utilsDefault: CheckboxUtils;
let utilsXOnly: CheckboxUtils;
beforeEach(() => {
// Создаем новые экземпляры перед каждым тестом для изоляции
utilsDefault = new CheckboxUtils(defaultSettings);
utilsXOnly = new CheckboxUtils(xOnlySettings);
});
// --- matchCheckboxLine ---
describe('matchCheckboxLine', () => {
it.each([
// Простые случаи
['- [ ] Text', { indent: 0, marker: '-', checkChar: ' ', checkboxCharPosition: 3 }],
['* [x] Task', { indent: 0, marker: '*', checkChar: 'x', checkboxCharPosition: 3 }],
['+ [?] Query', { indent: 0, marker: '+', checkChar: '?', checkboxCharPosition: 3 }],
['1. [!] Num', { indent: 0, marker: '1.', checkChar: '!', checkboxCharPosition: 4 }],
// С отступами
[' - [ ] Indented', { indent: 2, marker: '-', checkChar: ' ', checkboxCharPosition: 5 }],
['\t* [x] Tab Indent', { indent: 1, marker: '*', checkChar: 'x', checkboxCharPosition: 4 }], // Зависит от размера таба, Jest может считать его 1
[' 10. [A] Deep', { indent: 4, marker: '10.', checkChar: 'A', checkboxCharPosition: 9 }],
])('should match valid checkbox line: %s', (line, expected) => {
const result = utilsDefault.matchCheckboxLine(line);
expect(result).toEqual(expected);
});
it.each([
'Not a list item',
'- Just text',
'-[] No space',
'- [No Char]',
'- [ ]', // Нет пробела после скобки, как требует регекс \s
' Non-checkbox line',
'-[ ] No space before marker',
'1.[ ] No space after numbered marker',
'- ] Missing opening bracket',
'* [x Missing closing bracket',
])('should return null for invalid lines: %s', (line) => {
expect(utilsDefault.matchCheckboxLine(line)).toBeNull();
});
it('should require a space after the closing bracket', () => {
expect(utilsDefault.matchCheckboxLine('- [ ]')).toBeNull(); // Missing trailing space
expect(utilsDefault.matchCheckboxLine('- [ ] ')).not.toBeNull(); // Has trailing space
});
it.each([' ', 'x', '-', '?', '!'])('should correctly parse the checkChar', (state) => {
const lineInfo = utilsDefault.matchCheckboxLine(`- [${state}] Task`);
expect(lineInfo).not.toBeNull();
expect(lineInfo?.checkChar).toBe(state);
});
});
// --- isCheckedSymbol ---
describe('isCheckedSymbol', () => {
// Default mode (xOnlyMode: false)
it('should check symbols correctly in default mode', () => {
expect(utilsDefault.isCheckedSymbol('x')).toBe(true);
expect(utilsDefault.isCheckedSymbol('X')).toBe(true);
expect(utilsDefault.isCheckedSymbol('/')).toBe(true);
expect(utilsDefault.isCheckedSymbol('-')).toBe(true);
expect(utilsDefault.isCheckedSymbol('?')).toBe(true);
expect(utilsDefault.isCheckedSymbol(' ')).toBe(false); // Только пробел - не отмечено
});
// xOnly mode (xOnlyMode: true)
it('should check symbols correctly in xOnly mode', () => {
expect(utilsXOnly.isCheckedSymbol('x')).toBe(true); // Только 'x' - отмечено
expect(utilsXOnly.isCheckedSymbol('X')).toBe(false);
expect(utilsXOnly.isCheckedSymbol('/')).toBe(false);
expect(utilsXOnly.isCheckedSymbol('-')).toBe(false);
expect(utilsXOnly.isCheckedSymbol('?')).toBe(false);
expect(utilsXOnly.isCheckedSymbol(' ')).toBe(false);
});
});
// --- updateLineCheckboxStateWithInfo ---
describe('updateLineCheckboxStateWithInfo', () => {
it('should check an unchecked box', () => {
const uncheckedLine = ' - [ ] Task';
const uncheckedLineInfo = utilsDefault.matchCheckboxLine(uncheckedLine)!;
const updatedLine = utilsDefault.updateLineCheckboxStateWithInfo(uncheckedLine, true, uncheckedLineInfo);
expect(updatedLine).toBe(' - [x] Task');
});
it('should uncheck a checked box', () => {
const checkedLine = ' - [x] Task';
const checkedLineInfo = utilsDefault.matchCheckboxLine(checkedLine)!;
const updatedLine = utilsDefault.updateLineCheckboxStateWithInfo(checkedLine, false, checkedLineInfo);
expect(updatedLine).toBe(' - [ ] Task');
});
it('should handle other initial check chars', () => {
const otherLine = ' - [?] Task';
const otherLineInfo = utilsDefault.matchCheckboxLine(otherLine)!;
expect(utilsDefault.updateLineCheckboxStateWithInfo(otherLine, true, otherLineInfo)).toBe(' - [x] Task');
expect(utilsDefault.updateLineCheckboxStateWithInfo(otherLine, false, otherLineInfo)).toBe(' - [ ] Task');
});
it('should return original line if position is invalid (and warn)', () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { }); // Подавить вывод в консоль
const uncheckedLine = ' - [ ] Task';
const uncheckedLineInfo = utilsDefault.matchCheckboxLine(uncheckedLine)!;
const invalidInfo = { ...uncheckedLineInfo, checkboxCharPosition: 100 };
const result = utilsDefault.updateLineCheckboxStateWithInfo(uncheckedLine, true, invalidInfo);
expect(result).toBe(uncheckedLine);
expect(warnSpy).toHaveBeenCalled();
warnSpy.mockRestore();
});
});
// --- propagateStateToChildren ---
describe('propagateStateToChildren', () => {
const text = [
'- [ ] Parent 1', // 0
' - [ ] Child 1.1', // 1
' - [x] Child 1.2', // 2
' - [ ] Grandchild 1.2.1', // 3
'- [x] Parent 2', // 4
' - [x] Child 2.1', // 5
].join('\n');
it('should check children when parent is checked', () => {
// Имитируем проверку Parent 1 (индекс 0)
const lines = text.split('\n');
lines[0] = '- [x] Parent 1'; // Manually check parent for the test input
const initialTextModified = lines.join('\n');
const result = utilsDefault.propagateStateToChildren(initialTextModified, 0);
const expected = [
'- [x] Parent 1', // Checked
' - [x] Child 1.1', // Updated
' - [x] Child 1.2', // Updated
' - [x] Grandchild 1.2.1', // Updated
'- [x] Parent 2', // Unchanged
' - [x] Child 2.1', // Unchanged
].join('\n');
expect(result).toBe(expected);
});
it('should uncheck children when parent is unchecked', () => {
// Имитируем снятие отметки с Parent 2 (индекс 4)
const lines = text.split('\n');
lines[4] = '- [ ] Parent 2'; // Manually uncheck parent for the test input
const initialTextModified = lines.join('\n');
const result = utilsDefault.propagateStateToChildren(initialTextModified, 4);
const expected = [
'- [ ] Parent 1', // Unchanged
' - [ ] Child 1.1', // Unchanged
' - [x] Child 1.2', // Unchanged
' - [ ] Grandchild 1.2.1', // Unchanged
'- [ ] Parent 2', // Unchecked
' - [ ] Child 2.1', // Updated
].join('\n');
expect(result).toBe(expected);
});
it('should do nothing if the target line is not a checkbox', () => {
const textWithNonCheckbox = [
'# Header',
'- [ ] Item 1'
].join('\n');
const result = utilsDefault.propagateStateToChildren(textWithNonCheckbox, 0);
expect(result).toBe(textWithNonCheckbox);
});
it('should stop only when indent is less than or equal to the original parent', () => {
const textSibling = [
'- [ ] Parent', // 0, indent 0
' - [ ] Child 1', // 1, indent 2
' - [ ] Sibling C1', // 2, indent 2 (still > 0)
'- [ ] Parent 2' // 3, indent 0 (<= 0, stop here)
].join('\n');
const lines = textSibling.split('\n');
lines[0] = '- [x] Parent'; // Make parent checked
const modifiedText = lines.join('\n');
const result = utilsDefault.propagateStateToChildren(modifiedText, 0);
const expected = [
'- [x] Parent',
' - [x] Child 1', // Changed (indent 2 > 0)
' - [x] Sibling C1', // <<-- ALSO Changed (indent 2 > 0)
'- [ ] Parent 2' // Unchanged (indent 0 <= 0)
].join('\n');
expect(result).toBe(expected); // Проверяем обновленное ожидание
});
});
// --- propagateStateFromChildren ---
describe('propagateStateFromChildren', () => {
it('should check parent if all children are checked', () => {
const text = [
'- [ ] Parent', // Should become [x]
' - [x] Child 1',
' - [x] Child 2',
' - [x] Grandchild 2.1', // Needs to be checked too
].join('\n');
const result = utilsDefault.propagateStateFromChildren(text);
const expected = [
'- [x] Parent', // Updated
' - [x] Child 1',
' - [x] Child 2',
' - [x] Grandchild 2.1',
].join('\n');
expect(result).toBe(expected);
});
it('should uncheck parent if any child is unchecked', () => {
const text = [
'- [x] Parent', // Should become [ ]
' - [x] Child 1',
' - [ ] Child 2', // This one makes parent unchecked
].join('\n');
const result = utilsDefault.propagateStateFromChildren(text);
const expected = [
'- [ ] Parent', // Updated
' - [x] Child 1',
' - [ ] Child 2',
].join('\n');
expect(result).toBe(expected);
});
it('should handle nested propagation', () => {
const text = [
'- [ ] Grandparent', // Should become [ ]
' - [x] Parent 1', // Should become [ ]
' - [x] Child 1.1',
' - [ ] Child 1.2', // Makes Parent 1 unchecked
' - [x] Parent 2', // Stays checked
' - [x] Child 2.1',
].join('\n');
const result = utilsDefault.propagateStateFromChildren(text);
const expected = [
'- [ ] Grandparent', // Updated (because Parent 1 is now [ ])
' - [ ] Parent 1', // Updated
' - [x] Child 1.1',
' - [ ] Child 1.2',
' - [x] Parent 2',
' - [x] Child 2.1',
].join('\n');
expect(result).toBe(expected);
});
it('should not change parent state if it has no children', () => {
const text = [
'- [ ] Parent No Children',
'- [x] Parent No Children Checked',
' Some other line',
].join('\n');
const result = utilsDefault.propagateStateFromChildren(text);
expect(result).toBe(text); // No changes expected
});
it('should work correctly with xOnlyMode', () => {
const text = [
'- [ ] Parent', // Should become [x] in xOnly mode
' - [x] Child 1',
' - [x] Child 2'
].join('\n');
const result = utilsXOnly.propagateStateFromChildren(text);
expect(result).toContain('- [x] Parent'); // Parent checked
const text2 = [
'- [x] Parent', // Should become [ ] because '?' is not 'x'
' - [x] Child 1',
' - [?] Child 2'
].join('\n');
const result2 = utilsXOnly.propagateStateFromChildren(text2);
expect(result2).toContain('- [ ] Parent'); // Parent unchecked
});
it('should handle multiple independent parent checklists', () => {
const text = [
'- [ ] Parent 1',
' - [x] Child 1.1',
'Some other text',
'* [ ] Parent 2',
' * [x] Child 2.1',
' * [x] Child 2.2',
].join('\n');
const result = utilsDefault.propagateStateFromChildren(text);
const expected = [
'- [x] Parent 1', // Updated
' - [x] Child 1.1',
'Some other text',
'* [x] Parent 2', // Updated
' * [x] Child 2.1',
' * [x] Child 2.2',
].join('\n');
expect(result).toBe(expected);
});
it('should stop searching for children for a parent when a line with equal or lesser indent is encountered', () => {
const text = [
'- [ ] Parent 1', // indent 0
'Some regular text', // indent 0. Stops search for Parent 1 children HERE.
' - [x] Child 1.1', // indent 2. Ignored for Parent 1 because search stopped.
'- [ ] Parent 2', // indent 0. Processed independently later.
' - [x] Child 2.1', // indent 2. Child of Parent 2.
].join('\n');
const result = utilsDefault.propagateStateFromChildren(text);
// Ожидания:
// - Parent 1 не найдет детей (из-за "Some regular text") и останется [ ].
// - Parent 2 найдет Child 2.1 ([x]) и станет [x].
const expected = [
'- [ ] Parent 1', // Unchanged
'Some regular text', // Unchanged
' - [x] Child 1.1', // Unchanged
'- [x] Parent 2', // Updated because Child 2.1 is checked
' - [x] Child 2.1', // Unchanged
].join('\n');
expect(result).toBe(expected);
});
it('should stop searching for children when a sibling checkbox is encountered', () => {
const text = [
'- [ ] Parent 1', // indent 0
'- [ ] Sibling Parent', // indent 0. Stops search for Parent 1 children HERE.
' - [x] Child 1.1', // indent 2. Belongs to Sibling Parent if processed.
].join('\n');
const result = utilsDefault.propagateStateFromChildren(text);
// Ожидания:
// - Parent 1 не найдет детей (из-за "Sibling Parent") и останется [ ].
// - Sibling Parent найдет Child 1.1 ([x]) и станет [x].
const expected = [
'- [ ] Parent 1', // Unchanged
'- [x] Sibling Parent', // Updated because Child 1.1 is checked
' - [x] Child 1.1', // Unchanged
].join('\n');
expect(result).toBe(expected);
});
});
// --- propagateStateToChildrenFromSingleDiff ---
describe('propagateStateToChildrenFromSingleDiff', () => {
const textBefore = [
'- [ ] Parent', // 0
' - [ ] Child', // 1
].join('\n');
it('should propagate down if single line changed and it was a checkbox toggle', () => {
const textAfter = [
'- [x] Parent', // Changed state
' - [ ] Child',
].join('\n');
const result = utilsDefault.propagateStateToChildrenFromSingleDiff(textAfter, textBefore);
const expected = [ // Expect children to be updated
'- [x] Parent',
' - [x] Child',
].join('\n');
expect(result).toBe(expected);
});
it('should NOT propagate down if single line changed but it was NOT checkbox state', () => {
const textAfter = [
'- [ ] Parent Edited Text', // Text changed, not checkbox
' - [ ] Child',
].join('\n');
const result = utilsDefault.propagateStateToChildrenFromSingleDiff(textAfter, textBefore);
// Expect original textAfter, no downward propagation
expect(result).toBe(textAfter);
});
it('should NOT propagate down if single line changed into non-checkbox', () => {
const textAfter = [
'Parent removed checkbox', // Structure changed
' - [ ] Child',
].join('\n');
const result = utilsDefault.propagateStateToChildrenFromSingleDiff(textAfter, textBefore);
expect(result).toBe(textAfter);
});
it('should NOT propagate down if multiple lines changed', () => {
const textAfter = [
'- [x] Parent', // Changed
' - [x] Child', // Changed
].join('\n');
const result = utilsDefault.propagateStateToChildrenFromSingleDiff(textAfter, textBefore);
expect(result).toBe(textAfter); // Expect original textAfter
});
it('should NOT propagate down if line counts differ', () => {
const textAfter = [
'- [x] Parent',
' - [ ] Child',
' - [ ] New Child',
].join('\n');
const result = utilsDefault.propagateStateToChildrenFromSingleDiff(textAfter, textBefore);
expect(result).toBe(textAfter); // Expect original textAfter
});
it('should NOT propagate down if textBefore is undefined', () => {
const textAfter = [
'- [x] Parent',
' - [ ] Child',
].join('\n');
const result = utilsDefault.propagateStateToChildrenFromSingleDiff(textAfter, undefined);
expect(result).toBe(textAfter); // Expect original textAfter
});
});
// --- findDifferentLineIndexes ---
describe('findDifferentLineIndexes', () => {
const lines1 = ['a', 'b', 'c'];
it('should return empty array for identical lines', () => {
const lines2 = ['a', 'b', 'c'];
expect(utilsDefault.findDifferentLineIndexes(lines1, lines2)).toEqual([]);
});
it('should return index of the single different line', () => {
const lines2 = ['a', 'B', 'c'];
expect(utilsDefault.findDifferentLineIndexes(lines1, lines2)).toEqual([1]);
});
it('should return indexes of multiple different lines', () => {
const lines2 = ['A', 'b', 'C'];
expect(utilsDefault.findDifferentLineIndexes(lines1, lines2)).toEqual([0, 2]);
});
it('should throw error if line lengths are different', () => {
const lines2 = ['a', 'b'];
expect(() => utilsDefault.findDifferentLineIndexes(lines1, lines2)).toThrow(
'the length of the lines must be equal'
);
});
});
// --- syncText (Integration) ---
describe('syncText', () => {
it('should handle user checking parent (down -> up)', () => {
const textBefore = [
'- [ ] Parent',
' - [ ] Child 1',
' - [ ] Child 2',
].join('\n');
const textAfterUserCheck = [
'- [x] Parent', // User checked this
' - [ ] Child 1',
' - [ ] Child 2',
].join('\n');
const result = utilsDefault.syncText(textAfterUserCheck, textBefore);
const expected = [ // Down propagation first, then up (which changes nothing here)
'- [x] Parent',
' - [x] Child 1',
' - [x] Child 2',
].join('\n');
expect(result).toBe(expected);
});
it('should handle user unchecking parent (down -> up)', () => {
const textBefore = [
'- [x] Parent',
' - [x] Child 1',
' - [x] Child 2',
].join('\n');
const textAfterUserUncheck = [
'- [ ] Parent', // User unchecked this
' - [x] Child 1',
' - [x] Child 2',
].join('\n');
const result = utilsDefault.syncText(textAfterUserUncheck, textBefore);
const expected = [ // Down propagation first, then up
'- [ ] Parent',
' - [ ] Child 1',
' - [ ] Child 2',
].join('\n');
expect(result).toBe(expected);
});
it('should handle user checking the last child, making parent checked (no down -> up)', () => {
const textBefore = [
'- [ ] Parent',
' - [x] Child 1',
' - [ ] Child 2', // User will check this
].join('\n');
const textAfterUserCheck = [
'- [ ] Parent',
' - [x] Child 1',
' - [x] Child 2', // User checked this
].join('\n');
const result = utilsDefault.syncText(textAfterUserCheck, textBefore);
// SingleDiff won't trigger down prop. FromChildren runs.
const expected = [
'- [x] Parent', // Updated by FromChildren
' - [x] Child 1',
' - [x] Child 2',
].join('\n');
expect(result).toBe(expected);
});
it('should handle user unchecking one child, making parent unchecked (no down -> up)', () => {
const textBefore = [
'- [x] Parent',
' - [x] Child 1', // User will uncheck this
' - [x] Child 2',
].join('\n');
const textAfterUserUncheck = [
'- [x] Parent',
' - [ ] Child 1', // User unchecked this
' - [x] Child 2',
].join('\n');
const result = utilsDefault.syncText(textAfterUserUncheck, textBefore);
// SingleDiff won't trigger down prop. FromChildren runs.
const expected = [
'- [ ] Parent', // Updated by FromChildren
' - [ ] Child 1',
' - [x] Child 2',
].join('\n');
expect(result).toBe(expected);
});
it('should handle multiple changes correctly (no down -> up)', () => {
const textBefore = [
'- [ ] Parent',
' - [ ] Child 1',
' - [ ] Child 2',
' - [ ] Child 3',
].join('\n');
// User checks Parent AND Child 1, but leaves Child 2 unchecked
const textAfterMultipleChanges = [
'- [x] Parent', // Changed by user
' - [x] Child 1', // Changed by user
' - [ ] Child 2', // Left unchecked
' - [ ] Child 3', // Left unchecked
].join('\n');
const result = utilsDefault.syncText(textAfterMultipleChanges, textBefore);
// SingleDiff won't run (2 changes). FromChildren runs.
// Since Child 2/3 are unchecked, Parent should become unchecked.
const expected = [
'- [ ] Parent', // Corrected by FromChildren
' - [x] Child 1',
' - [ ] Child 2',
' - [ ] Child 3',
].join('\n');
expect(result).toBe(expected);
});
});
});

View file

@ -1,6 +1,6 @@
import { CheckboxUtils } from "../src/checkboxUtils";
describe("CheckboxUtils", () => {
describe("CheckboxUtils old", () => {
let checkboxUtilsXOnly: CheckboxUtils;
let checkboxUtilsSpaceOnly: CheckboxUtils;
@ -11,88 +11,88 @@ describe("CheckboxUtils", () => {
describe("findCheckboxesLine", () => {
test("recognizes valid checkbox lines", () => {
expect(checkboxUtilsXOnly.findCheckboxesLine("- [ ] Task")).not.toBeNull();
expect(checkboxUtilsXOnly.findCheckboxesLine(" - [ ] Task")).not.toBeNull();
expect(checkboxUtilsXOnly.findCheckboxesLine("- [x] Task")).not.toBeNull();
expect(checkboxUtilsXOnly.findCheckboxesLine("* [ ] Another task")).not.toBeNull();
expect(checkboxUtilsXOnly.findCheckboxesLine("+ [x] Task")).not.toBeNull();
expect(checkboxUtilsXOnly.findCheckboxesLine("1. [ ] Numbered task")).not.toBeNull();
expect(checkboxUtilsXOnly.findCheckboxesLine("1. [x] Numbered task")).not.toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine("- [ ] Task")).not.toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine(" - [ ] Task")).not.toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine("- [x] Task")).not.toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine("* [ ] Another task")).not.toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine("+ [x] Task")).not.toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine("1. [ ] Numbered task")).not.toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine("1. [x] Numbered task")).not.toBeNull();
});
test("correctly handles different checkbox states", () => {
const states = [" ", "x", "-", "?", "!", "*", "#", "@", "[", "]", "y", "X", "Y"];
for (const state of states) {
const match = checkboxUtilsXOnly.findCheckboxesLine(`- [${state}] Task`);
expect(match).not.toBeNull();
expect(match![3]).toBe(state);
const lineInfo = checkboxUtilsXOnly.matchCheckboxLine(`- [${state}] Task`);
expect(lineInfo).not.toBeNull();
expect(lineInfo?.checkChar).toBe(state);
}
});
test("correctly handles indentation", () => {
const noIndentMatch = checkboxUtilsXOnly.findCheckboxesLine("- [ ] Task");
const noIndentMatch = checkboxUtilsXOnly.matchCheckboxLine("- [ ] Task");
expect(noIndentMatch).not.toBeNull();
expect(noIndentMatch![1]).toBe("");
expect(noIndentMatch!.indent).toBe("".length);
const singleSpaceMatch = checkboxUtilsXOnly.findCheckboxesLine(" - [ ] Task");
const singleSpaceMatch = checkboxUtilsXOnly.matchCheckboxLine(" - [ ] Task");
expect(singleSpaceMatch).not.toBeNull();
expect(singleSpaceMatch![1]).toBe(" ");
expect(singleSpaceMatch!.indent).toBe(" ".length);
const multiSpaceMatch = checkboxUtilsXOnly.findCheckboxesLine(" - [ ] Task");
const multiSpaceMatch = checkboxUtilsXOnly.matchCheckboxLine(" - [ ] Task");
expect(multiSpaceMatch).not.toBeNull();
expect(multiSpaceMatch![1]).toBe(" ");
expect(multiSpaceMatch!.indent).toBe(" ".length);
const tabMatch = checkboxUtilsXOnly.findCheckboxesLine("\t- [ ] Task");
const tabMatch = checkboxUtilsXOnly.matchCheckboxLine("\t- [ ] Task");
expect(tabMatch).not.toBeNull();
expect(tabMatch![1]).toBe("\t");
expect(tabMatch!.indent).toBe("\t".length);
});
test("ignores invalid lines", () => {
expect(checkboxUtilsXOnly.findCheckboxesLine("-[ ] Task")).toBeNull();
expect(checkboxUtilsXOnly.findCheckboxesLine("*[x] Task")).toBeNull();
expect(checkboxUtilsXOnly.findCheckboxesLine("1.[ ] Numbered task")).toBeNull();
expect(checkboxUtilsXOnly.findCheckboxesLine("- [] Task")).toBeNull();
expect(checkboxUtilsXOnly.findCheckboxesLine(" - Task")).toBeNull();
expect(checkboxUtilsXOnly.findCheckboxesLine("Just some text")).toBeNull();
expect(checkboxUtilsXOnly.findCheckboxesLine("1. Some numbered task")).toBeNull();
expect(checkboxUtilsXOnly.findCheckboxesLine("")).toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine("-[ ] Task")).toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine("*[x] Task")).toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine("1.[ ] Numbered task")).toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine("- [] Task")).toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine(" - Task")).toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine("Just some text")).toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine("1. Some numbered task")).toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine("")).toBeNull();
});
test("ignores lines with missing opening or closing brackets", () => {
// Missing opening bracket [
expect(checkboxUtilsXOnly.findCheckboxesLine("- ] Task")).toBeNull();
expect(checkboxUtilsXOnly.findCheckboxesLine("* ] Task")).toBeNull();
expect(checkboxUtilsXOnly.findCheckboxesLine("+ ] Task")).toBeNull();
expect(checkboxUtilsXOnly.findCheckboxesLine("1. ] Task")).toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine("- ] Task")).toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine("* ] Task")).toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine("+ ] Task")).toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine("1. ] Task")).toBeNull();
// Missing closing bracket ]
expect(checkboxUtilsXOnly.findCheckboxesLine("- [x Task")).toBeNull();
expect(checkboxUtilsXOnly.findCheckboxesLine("* [ Task")).toBeNull();
expect(checkboxUtilsXOnly.findCheckboxesLine("+ [- Task")).toBeNull();
expect(checkboxUtilsXOnly.findCheckboxesLine("1. [? Task")).toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine("- [x Task")).toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine("* [ Task")).toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine("+ [- Task")).toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine("1. [? Task")).toBeNull();
// Malformed with spaces
expect(checkboxUtilsXOnly.findCheckboxesLine("- [ x Task")).toBeNull();
expect(checkboxUtilsXOnly.findCheckboxesLine("- x] Task")).toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine("- [ x Task")).toBeNull();
expect(checkboxUtilsXOnly.matchCheckboxLine("- x] Task")).toBeNull();
});
test("extracts list marker correctly", () => {
const dashMatch = checkboxUtilsXOnly.findCheckboxesLine("- [ ] Task");
const dashMatch = checkboxUtilsXOnly.matchCheckboxLine("- [ ] Task");
expect(dashMatch).not.toBeNull();
expect(dashMatch![2]).toBe("-");
expect(dashMatch!.marker).toBe("-");
const asteriskMatch = checkboxUtilsXOnly.findCheckboxesLine("* [ ] Task");
const asteriskMatch = checkboxUtilsXOnly.matchCheckboxLine("* [ ] Task");
expect(asteriskMatch).not.toBeNull();
expect(asteriskMatch![2]).toBe("*");
expect(asteriskMatch!.marker).toBe("*");
const plusMatch = checkboxUtilsXOnly.findCheckboxesLine("+ [ ] Task");
const plusMatch = checkboxUtilsXOnly.matchCheckboxLine("+ [ ] Task");
expect(plusMatch).not.toBeNull();
expect(plusMatch![2]).toBe("+");
expect(plusMatch!.marker).toBe("+");
const numberedMatch = checkboxUtilsXOnly.findCheckboxesLine("1. [ ] Task");
const numberedMatch = checkboxUtilsXOnly.matchCheckboxLine("1. [ ] Task");
expect(numberedMatch).not.toBeNull();
expect(numberedMatch![2]).toBe("1.");
expect(numberedMatch!.marker).toBe("1.");
});
});
@ -119,13 +119,15 @@ describe("CheckboxUtils", () => {
describe("updateSettings", () => {
test("updates xOnlyMode setting correctly", () => {
const utils = new CheckboxUtils({ xOnlyMode: true });
const settings = { xOnlyMode: true };
const utils = new CheckboxUtils(settings);
expect(utils.isCheckedSymbol("-")).toBe(false);
utils.updateSettings({ xOnlyMode: false });
settings.xOnlyMode = false;
expect(utils.isCheckedSymbol("-")).toBe(true);
utils.updateSettings({ xOnlyMode: true });
settings.xOnlyMode = true;
expect(utils.isCheckedSymbol("-")).toBe(false);
});
});
@ -134,54 +136,54 @@ describe("CheckboxUtils", () => {
test("updates parent checkbox if all children are checked (xOnlyMode)", () => {
const text = "- [ ] Parent\n - [x] Child 1\n - [x] Child 2";
const expected = "- [x] Parent\n - [x] Child 1\n - [x] Child 2";
expect(checkboxUtilsXOnly.syncCheckboxes(text)).toEqual(expected);
expect(checkboxUtilsXOnly.propagateStateFromChildren(text)).toEqual(expected);
});
test("updates parent checkbox if all children are checked (spaceOnly)", () => {
const text = "- [ ] Parent\n - [?] Child 1\n - [-] Child 2";
const expected = "- [x] Parent\n - [?] Child 1\n - [-] Child 2";
expect(checkboxUtilsSpaceOnly.syncCheckboxes(text)).toEqual(expected);
expect(checkboxUtilsSpaceOnly.propagateStateFromChildren(text)).toEqual(expected);
});
test("unchecks parent if not all children are checked", () => {
const text = "- [x] Parent\n - [x] Child 1\n - [ ] Child 2";
const expected = "- [ ] Parent\n - [x] Child 1\n - [ ] Child 2";
expect(checkboxUtilsXOnly.syncCheckboxes(text)).toEqual(expected);
expect(checkboxUtilsXOnly.propagateStateFromChildren(text)).toEqual(expected);
});
test("doesn't change parent if it has no children", () => {
const text = "- [x] Single";
expect(checkboxUtilsXOnly.syncCheckboxes(text)).toEqual(text);
expect(checkboxUtilsXOnly.propagateStateFromChildren(text)).toEqual(text);
});
test("handles multiple levels of nesting correctly", () => {
const text = "- [ ] Parent\n - [ ] Child\n - [x] Grandchild";
const expected = "- [x] Parent\n - [x] Child\n - [x] Grandchild";
expect(checkboxUtilsXOnly.syncCheckboxes(text)).toEqual(expected);
expect(checkboxUtilsXOnly.propagateStateFromChildren(text)).toEqual(expected);
});
test("handles mixed indentation levels correctly", () => {
const text = "- [ ] Parent\n - [x] Child 1\n - [x] Grandchild 1\n - [x] Child 2";
const expected = "- [x] Parent\n - [x] Child 1\n - [x] Grandchild 1\n - [x] Child 2";
expect(checkboxUtilsXOnly.syncCheckboxes(text)).toEqual(expected);
expect(checkboxUtilsXOnly.propagateStateFromChildren(text)).toEqual(expected);
});
test("handles siblings with different indentation correctly", () => {
const text = "- [ ] Parent\n - [x] Child 1\n - [x] Grandchild 1\n - [ ] Child 2";
const expected = "- [ ] Parent\n - [x] Child 1\n - [x] Grandchild 1\n - [ ] Child 2";
expect(checkboxUtilsXOnly.syncCheckboxes(text)).toEqual(expected);
expect(checkboxUtilsXOnly.propagateStateFromChildren(text)).toEqual(expected);
});
test("handles multiple parents correctly", () => {
const text = "- [ ] Parent 1\n - [x] Child 1\n- [ ] Parent 2\n - [x] Child 2";
const expected = "- [x] Parent 1\n - [x] Child 1\n- [x] Parent 2\n - [x] Child 2";
expect(checkboxUtilsXOnly.syncCheckboxes(text)).toEqual(expected);
expect(checkboxUtilsXOnly.propagateStateFromChildren(text)).toEqual(expected);
});
test("handles mixed content with non-checkbox lines correctly", () => {
const text = "- [ ] Parent\nSome regular text\n - [x] Child 1\nMore text\n - [x] Child 2";
const expected = "- [ ] Parent\nSome regular text\n - [x] Child 1\nMore text\n - [x] Child 2";
expect(checkboxUtilsXOnly.syncCheckboxes(text)).toEqual(expected);
expect(checkboxUtilsXOnly.propagateStateFromChildren(text)).toEqual(expected);
});
});
});

View file

@ -33,6 +33,9 @@ const config: Config = {
testEnvironment: "node",
// Limit the number of workers that run tests in parallel
maxWorkers: 1, // <--- Задает последовательное выполнение
};
export default config;

View file

@ -6,9 +6,10 @@
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"prebuild": "npm test",
"prebuild": "npm run test",
"version": "node version-bump.mjs && git add manifest.json versions.json",
"test": "jest"
"test": "jest",
"test:fast": "jest --no-coverage"
},
"keywords": [],
"author": "",
@ -30,4 +31,4 @@
"dependencies": {
"async-mutex": "^0.5.0"
}
}
}

View file

@ -12,7 +12,7 @@ export class CheckboxSyncPluginSettingTab extends PluginSettingTab {
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName("X-Only Mode")
.setDesc("When enabled, only 'x' marks a task as complete. When disabled, any character except space marks a task as complete")
@ -20,8 +20,9 @@ export class CheckboxSyncPluginSettingTab extends PluginSettingTab {
toggle
.setValue(this.plugin.settings.xOnlyMode)
.onChange(async (value) => {
this.plugin.settings.xOnlyMode = value;
await this.plugin.saveSettings();
await this.plugin.updateSettings((settings) => {
settings.xOnlyMode = value;
});
})
);
}

View file

@ -13,27 +13,26 @@ export default class FileStateHolder {
this.mutex = new Mutex();
}
async update(file: TFile) {
await this.mutex.runExclusive(async () => {
const text = await this.vault.read(file);
this.set(file, text);
});
}
// возвращает понадобилась ли загрузка
async updateIfNeeded(file: TFile, text?: string): Promise<boolean> {
/**
* Initializes the file's data if it hasn't been initialized yet.
*
* @param file - The file object to initialize.
* @param text - Optional text content to associate with the file. If not provided, it will be read from the vault.
* @returns A promise that resolves to `true` if the file was initialized, or `false` if it was already initialized.
*/
async initIfNeeded(file: TFile, text?: string): Promise<boolean> {
if (this.has(file)) {
return false;
return false;
}
let res = await this.mutex.runExclusive<boolean>(async () => {
if (this.has(file)) {
return false;
}
console.log(`updateIfNeeded "${file.name}" start`);
if (!text){
// console.log(`updateIfNeeded "${file.name}" start`);
if (!text) {
text = await this.vault.read(file);
}
this.set(file, text);
}
this.set(file, text);
return true;
});
return res;
@ -44,11 +43,19 @@ export default class FileStateHolder {
}
set(file: TFile, text: string) {
console.log(`File "${file.name}" load to holder`);
this.map.set(file, text);
if (text !== this.map.get(file)) {
console.log(`File "${file.name}" load to holder`);
this.map.set(file, text);
}
}
get(file: TFile) {
return this.map.get(file);
}
getAllFiles(): TFile[] {
const keysIterator = this.map.keys();
const keysArray = Array.from(keysIterator);
return keysArray;
}
}

View file

@ -1,18 +1,19 @@
import { Mutex } from "async-mutex";
import { Editor, EditorChange, MarkdownFileInfo, MarkdownView, TFile } from "obsidian";
import CheckboxSyncPlugin from "./main";
import { Editor, EditorChange, MarkdownFileInfo, MarkdownView, TFile, Vault } from "obsidian";
import { CheckboxUtils } from "./checkboxUtils";
import FileStateHolder from "./FileStateHolder";
export default class SyncController {
private plugin: CheckboxSyncPlugin;//delete
// private plugin: CheckboxSyncPlugin;//delete
private vault: Vault;
private checkboxUtils: CheckboxUtils;
private fileStateHolder: FileStateHolder;
private mutex: Mutex;
constructor(plugin: CheckboxSyncPlugin, checkboxUtils: CheckboxUtils, fileStateHolder: FileStateHolder) {
this.plugin = plugin;//delete
constructor(vault: Vault, checkboxUtils: CheckboxUtils, fileStateHolder: FileStateHolder) {
// this.plugin = plugin;//delete
this.vault = vault;
this.checkboxUtils = checkboxUtils;
this.fileStateHolder = fileStateHolder;
this.mutex = new Mutex();
@ -28,15 +29,15 @@ export default class SyncController {
const text = editor.getValue();
const textBefore = this.fileStateHolder.get(file);
let newText = this.syncText(text, textBefore);
let newText = this.checkboxUtils.syncText(text, textBefore);
if (newText === text) {
console.log(`sync editor "${file.basename}" stop. new text equals old text.`);
return;
}
this.editEditor(editor, text, newText);
this.fileStateHolder.set(file, newText);
this.editEditor(editor, text, newText);
console.log(`syncEditor "${file.basename}" stop.`);
});
}
@ -47,87 +48,46 @@ export default class SyncController {
}
await this.mutex.runExclusive(async () => {
console.log(`sync file "${file.basename}" start.`);
let text = await this.plugin.app.vault.read(file);
let textBefore = this.fileStateHolder.get(file);
let newText = this.syncText(text, textBefore);
if (newText === text) {
console.log(`sync file "${file.basename}" stop. new text equals old text.`);
return;
}
const newText = await this.vault.process(file, (text) => {
let textBefore = this.fileStateHolder.get(file);
let newText = this.checkboxUtils.syncText(text, textBefore);
return newText;
});
this.fileStateHolder.set(file, newText);
await this.plugin.app.vault.modify(file, newText);
console.log(`sync file "${file.basename}" stop.`);
});
}
syncText(text: string, textBefore: string | undefined): string {
let newText;
newText = this.syncTextBefore(text, textBefore);
newText = this.checkboxUtils.syncCheckboxes(newText);
return newText;
}
syncTextBefore(text: string, textBefore: string | undefined): string {
if (!textBefore) return text;
const beforeLines = textBefore.split('\n');
const textLines = text.split('\n');
if (beforeLines.length !== textLines.length) return text;
const diffs = this.findDifferentLineIndexes(beforeLines, textLines);
if (diffs.length !== 1) {
return text;
}
return this.checkboxUtils.syncCheckboxesAfterDifferentLine(text, diffs[0]);
}
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;
}
editEditor(editor: Editor, oldText: string, newText: string){
private editEditor(editor: Editor, oldText: string, newText: string) {
const cursor = editor.getCursor();
const newLines = newText.split("\n");
const oldLines = oldText.split("\n");
const newLines = newText.split("\n");
const oldLines = oldText.split("\n");
const diffIndexes = this.findDifferentLineIndexes(oldLines, newLines);
const diffIndexes = this.checkboxUtils.findDifferentLineIndexes(oldLines, newLines);
const changes: EditorChange[] = [];
const changes: EditorChange[] = [];
for (let ind of diffIndexes) {
changes.push({
from: { line: ind, ch: 0 },
to: { line: ind, ch: oldLines[ind].length },
text: newLines[ind]
});
}
editor.transaction({
changes: changes
for (let ind of diffIndexes) {
changes.push({
from: { line: ind, ch: 0 },
to: { line: ind, ch: oldLines[ind].length },
text: newLines[ind]
});
}
editor.transaction({
changes: changes
});
editor.setCursor(cursor);
editor.setCursor(cursor);
const lastDifferentLineIndex = diffIndexes.length > 0 ? diffIndexes[0] : -1;
if (lastDifferentLineIndex != -1) {
editor.scrollIntoView({
from: { line: lastDifferentLineIndex, ch: 0 },
to: { line: lastDifferentLineIndex, ch: 0 }
});
}
const lastDifferentLineIndex = diffIndexes.length > 0 ? diffIndexes[0] : -1;
if (lastDifferentLineIndex != -1) {
editor.scrollIntoView({
from: { line: lastDifferentLineIndex, ch: 0 },
to: { line: lastDifferentLineIndex, ch: 0 }
});
}
}
}

View file

@ -1,67 +1,90 @@
import { CheckboxSyncPluginSettings } from "./types";
export class CheckboxUtils {
constructor(private settings: CheckboxSyncPluginSettings) { }
export interface CheckboxLineInfo {
indent: number;
marker: string;
checkChar: string;
checkboxCharPosition: number;
}
updateSettings(settings: CheckboxSyncPluginSettings) {
export class CheckboxUtils {
private settings: Readonly<CheckboxSyncPluginSettings>;
constructor(settings: Readonly<CheckboxSyncPluginSettings>) {
this.settings = settings;
}
findCheckboxesLine(line: string): RegExpMatchArray | null {
return line.match(/^(\s*)([*+-]|\d+\.) \[(.)\]\s/);
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;
return {
indent,
marker,
checkChar,
checkboxCharPosition
};
}
isCheckedSymbol(text: string): boolean {
return this.settings.xOnlyMode ? text === "x" : text !== " ";
}
syncCheckboxesAfterDifferentLine(text: string, line: number): string {
console.log("syncCheckboxesAfterDifferentLine");
updateLineCheckboxStateWithInfo(line: string, shouldBeChecked: boolean, lineInfo: CheckboxLineInfo): string {
const newCheckChar = shouldBeChecked ? "x" : " ";
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;
}
propagateStateToChildren(text: string, parentLine: number): string {
// console.log("propagateStateToChildren");
const lines = text.split("\n");
const match = this.findCheckboxesLine(lines[line]);
if (!match) {
console.log(`checkbox not found in line ${line}`)
const parentLineInfo = this.matchCheckboxLine(lines[parentLine]);
if (!parentLineInfo) {
console.log(`checkbox not found in line ${parentLine}`)
return text;
}
const indent = match[1].length;
const isChecked = this.isCheckedSymbol(match[3]);
let j = line + 1;
const isChecked = this.isCheckedSymbol(parentLineInfo.checkChar);
let j = parentLine + 1;
while (j < lines.length) {
const childMatch = this.findCheckboxesLine(lines[j]);
if (!childMatch || childMatch[1].length <= indent) break;
const checkboxPos = childMatch[1].length + childMatch[2].length + 2;
if (isChecked) {
lines[j] = lines[j].substring(0, checkboxPos) + "x" + lines[j].substring(checkboxPos + 1);
} else {
lines[j] = lines[j].substring(0, checkboxPos) + " " + lines[j].substring(checkboxPos + 1);
}
const childLineInfo = this.matchCheckboxLine(lines[j]);
if (!childLineInfo || childLineInfo.indent <= parentLineInfo.indent) break;
lines[j] = this.updateLineCheckboxStateWithInfo(lines[j], isChecked, childLineInfo);
j++;
}
return lines.join("\n");
}
syncCheckboxes(text: string): string {
propagateStateFromChildren(text: string): string {
const lines = text.split("\n");
for (let i = lines.length - 1; i >= 0; i--) {
const match = this.findCheckboxesLine(lines[i]);
if (!match) continue;
const parentLineInfo = this.matchCheckboxLine(lines[i]);
if (!parentLineInfo) continue;
const indent = match[1].length;
const isChecked = this.isCheckedSymbol(match[3]);
const isChecked = this.isCheckedSymbol(parentLineInfo.checkChar);
let allChildrenChecked = true;
let hasChildren = false;
let j = i + 1;
while (j < lines.length) {
const childMatch = this.findCheckboxesLine(lines[j]);
if (!childMatch || childMatch[1].length <= indent) break;
const childLineInfo = this.matchCheckboxLine(lines[j]);
if (!childLineInfo || childLineInfo.indent <= parentLineInfo.indent) break;
hasChildren = true;
const childrenIsChecked = this.isCheckedSymbol(childMatch[3]);
const childrenIsChecked = this.isCheckedSymbol(childLineInfo.checkChar);
if (!childrenIsChecked) {
allChildrenChecked = false;
break;
@ -69,15 +92,68 @@ export class CheckboxUtils {
j++;
}
if (hasChildren) {
const checkboxPos = match[1].length + match[2].length + 2;
if (allChildrenChecked && !isChecked) {
lines[i] = lines[i].substring(0, checkboxPos) + "x" + lines[i].substring(checkboxPos + 1);
} else if (!allChildrenChecked && isChecked) {
lines[i] = lines[i].substring(0, checkboxPos) + " " + lines[i].substring(checkboxPos + 1);
}
if (hasChildren && isChecked !== allChildrenChecked) {
lines[i] = this.updateLineCheckboxStateWithInfo(lines[i], allChildrenChecked, parentLineInfo);
}
}
return lines.join("\n");
}
syncText(text: string, textBefore: string | undefined): string {
let newText;
newText = this.propagateStateToChildrenFromSingleDiff(text, textBefore);
newText = this.propagateStateFromChildren(newText);
return newText;
}
propagateStateToChildrenFromSingleDiff(text: string, textBefore: string | undefined): string {
if (!textBefore) return text;
const textBeforeLines = textBefore.split('\n');
const textLines = text.split('\n');
if (textBeforeLines.length !== textLines.length) return text;
const diffIndexes = this.findDifferentLineIndexes(textBeforeLines, textLines);
if (diffIndexes.length !== 1) {
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.checkChar !== lineInfoAfter.checkChar
) {
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;
}
}

View file

@ -0,0 +1,34 @@
import { App, TFile } from "obsidian";
import CheckboxSyncPlugin from "src/main";
import SyncController from "src/SyncController";
export class FileChangeEventHandler {
private plugin: CheckboxSyncPlugin;
private app: App;
private syncController: SyncController;
constructor(plugin: CheckboxSyncPlugin, app: App, syncController: SyncController) {
this.plugin = plugin;
this.app = app;
this.syncController = syncController;
}
registerEvents() {
//запуск плагина при модификации файла(для обработки в режиме просмотра)
this.plugin.registerEvent(
this.app.vault.on("modify", async (file) => {
if (!(file instanceof TFile)) return;
if (file.extension !== "md") return;
console.log(`modify file ${file.path}`);
await this.syncController.syncFile(file);
})
);
//запуск плагина при изменении в режиме редактора
this.plugin.registerEvent(
this.app.workspace.on("editor-change", async (editor, info) => {
console.log(`editor-change file ${info.file?.path}`);
await this.syncController.syncEditor(editor, info);
})
);
}
}

View file

@ -1,22 +1,23 @@
import { Mutex } from "async-mutex";
import { App, MarkdownPostProcessorContext, MarkdownRenderer, MarkdownView, TFile, WorkspaceLeaf } from "obsidian";
import CheckboxSyncPlugin from "./main";
import SyncController from "./SyncController";
import FileStateHolder from "./FileStateHolder";
import { text } from "stream/consumers";
import FileStateHolder from "src/FileStateHolder";
import CheckboxSyncPlugin from "src/main";
import SyncController from "src/SyncController";
export default class EventService {
export class FileLoadEventHandler {
private plugin: CheckboxSyncPlugin;
private app: App;
private syncController: SyncController;
private fileStateHolder: FileStateHolder;
private markdownPostProcessorMutex: Mutex;//для более последовательной обработки файлов в постпроцессоре. но вообще это лишь замедляет плагин
constructor(plugin: CheckboxSyncPlugin, app: App, syncController: SyncController, fileStateHolder: FileStateHolder) {
this.plugin = plugin;
this.app = app;
this.syncController = syncController;
this.fileStateHolder = fileStateHolder;
this.markdownPostProcessorMutex = new Mutex();
}
registerEvents() {
@ -25,44 +26,30 @@ export default class EventService {
this.app.workspace.on("file-open", async (file) => {
if (file) {
console.log(`file-open ${file.path}`);
const isUpdate = await this.fileStateHolder.updateIfNeeded(file);
const isUpdate = await this.fileStateHolder.initIfNeeded(file);
if (isUpdate) {
await this.syncController.syncFile(file);
}
}
})
);
//запуск плагина при модификации файла(для обработки в режиме просмотра)
this.plugin.registerEvent(
this.app.vault.on("modify", async (file) => {
if (!(file instanceof TFile)) return;
if (file.extension !== "md") return;
console.log(`modify file ${file.path}`);
await this.syncController.syncFile(file);
})
);
//запуск плагина при изменении в режиме редактора
this.plugin.registerEvent(
this.app.workspace.on("editor-change", async (editor, info) => {
console.log(`editor-change file ${info.file?.path}`);
await this.syncController.syncEditor(editor, info);
})
);
this.plugin.registerMarkdownPostProcessor(async (el: HTMLElement, ctx: MarkdownPostProcessorContext) => {
console.log(`MarkdownPostProcessor "${ctx.sourcePath}"`);
const file = this.app.vault.getAbstractFileByPath(ctx.sourcePath);
if (file instanceof TFile) {
const isUpdate = await this.fileStateHolder.updateIfNeeded(file);
if (isUpdate) {
await this.syncController.syncFile(file);
// console.log(`MarkdownPostProcessor "${ctx.sourcePath}"`);
await this.markdownPostProcessorMutex.runExclusive(async () => {
const file = this.app.vault.getAbstractFileByPath(ctx.sourcePath);
if (file instanceof TFile) {
const isUpdate = await this.fileStateHolder.initIfNeeded(file);
if (isUpdate) {
await this.syncController.syncFile(file);
}
}
}
});
});
this.plugin.registerEvent(
this.app.workspace.on('quick-preview', async (file: TFile, data: string) => {
const isUpdate = await this.fileStateHolder.updateIfNeeded(file, data);
const isUpdate = await this.fileStateHolder.initIfNeeded(file, data);
if (isUpdate) {
await this.syncController.syncFile(file);
}
@ -77,15 +64,12 @@ export default class EventService {
//загрузка в кеш и синхронизация открытых файлов
async loadAndSyncActiveFiles() {
const markdownLeaves = this.app.workspace.getLeavesOfType('markdown');
markdownLeaves.forEach(async (leaf: WorkspaceLeaf) => {
await this.loadAndSyncActiveFile(leaf);
});
await Promise.all(markdownLeaves.map(async (leaf) => await this.loadAndSyncActiveFile(leaf)));
}
private async loadAndSyncActiveFile(leaf: WorkspaceLeaf) {
const view = leaf.view as MarkdownView;
if (!view.file) return;
//вызов ререндера, для кеширования через MarkdownPostProcessor
await this.rerenderView(view);
}

View file

@ -1,43 +1,56 @@
import { Plugin } from "obsidian";
import { Plugin, TFile } from "obsidian";
import { CheckboxSyncPluginSettingTab } from "./CheckboxSyncPluginSettingTab";
import { CheckboxUtils } from "./checkboxUtils";
import EventService from "./EventService";
import FileStateHolder from "./FileStateHolder";
import SyncController from "./SyncController";
import { CheckboxSyncPluginSettings } from "./types";
import { FileLoadEventHandler } from "./events/FileLoadEventHandler";
import { FileChangeEventHandler } from "./events/FileChangeEventHandler";
const DEFAULT_SETTINGS: CheckboxSyncPluginSettings = {
xOnlyMode: true,
};
export default class CheckboxSyncPlugin extends Plugin {
settings: CheckboxSyncPluginSettings;
private _settings: CheckboxSyncPluginSettings;
syncController: SyncController;
checkboxUtils: CheckboxUtils;
fileStateHolder: FileStateHolder;
eventService: EventService;
private syncController: SyncController;
private checkboxUtils: CheckboxUtils;
private fileStateHolder: FileStateHolder;
private fileLoadEventHandler: FileLoadEventHandler;
private fileChangeEventHandler: FileChangeEventHandler;
async onload() {
await this.loadSettings();
this.fileStateHolder = new FileStateHolder(this.app.vault);
this.checkboxUtils = new CheckboxUtils(this.settings);
this.syncController = new SyncController(this, this.checkboxUtils, this.fileStateHolder);
this.eventService = new EventService(this, this.app, this.syncController, this.fileStateHolder);
this.syncController = new SyncController(this.app.vault, this.checkboxUtils, this.fileStateHolder);
this.fileLoadEventHandler = new FileLoadEventHandler(this, this.app, this.syncController, this.fileStateHolder);
this.fileChangeEventHandler = new FileChangeEventHandler(this, this.app, this.syncController);
this.addSettingTab(new CheckboxSyncPluginSettingTab(this.app, this));
this.eventService.registerEvents();
this.fileLoadEventHandler.registerEvents();
this.fileChangeEventHandler.registerEvents();
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
this._settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
get settings(): Readonly<CheckboxSyncPluginSettings> {
return this._settings;
}
async updateSettings(callback: (settings: CheckboxSyncPluginSettings) => void | Promise<void>) {
await callback(this._settings);
await this.saveData(this.settings);
this.checkboxUtils.updateSettings(this.settings);
await this.eventService.loadAndSyncActiveFiles();
//надо пересинхронизировать все файлы в кеше
const allFile = this.fileStateHolder.getAllFiles();
await Promise.all(
allFile.map(async (file: TFile) => {
await this.syncController.syncFile(file);
})
);
}
}