obsidian#HeadingCache TypeScript Examples

The following examples show how to use obsidian#HeadingCache. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: fileCachedMetadata.fixture.ts    From obsidian-switcher-plus with GNU General Public License v3.0 6 votes vote down vote up
export function makeHeading(
  heading: string,
  level: number,
  startLoc?: Loc,
  endLoc?: Loc,
): HeadingCache {
  const position = makePos(startLoc, endLoc);

  return {
    position,
    heading,
    level,
  };
}
Example #2
Source File: main.ts    From obsidian-spaced-repetition with MIT License 6 votes vote down vote up
function getCardContext(cardLine: number, headings: HeadingCache[]): string {
    const stack: HeadingCache[] = [];
    for (const heading of headings) {
        if (heading.position.start.line > cardLine) {
            break;
        }

        while (stack.length > 0 && stack[stack.length - 1].level >= heading.level) {
            stack.pop();
        }

        stack.push(heading);
    }

    let context = "";
    for (const headingObj of stack) {
        headingObj.heading = headingObj.heading.replace(/\[\^\d+\]/gm, "").trim();
        context += headingObj.heading + " > ";
    }
    return context.slice(0, -3);
}
Example #3
Source File: path.ts    From obsidian-fantasy-calendar with MIT License 6 votes vote down vote up
selectSuggestion({ item }: FuzzyMatch<TFile | BlockCache | HeadingCache>) {
        let link: string;
        if (item instanceof TFile) {
            this.file = item;
            link = item.basename;
        } else if (Object.prototype.hasOwnProperty.call(item, "heading")) {
            link = this.file.basename + "#" + (<HeadingCache>item).heading;
        } else if (Object.prototype.hasOwnProperty.call(item, "id")) {
            link = this.file.basename + "^" + (<BlockCache>item).id;
        }
        const path = this.file.path.split("/").slice(0, -1);
        if (path.length) {
            this.link = path.join("/") + "/" + link;
        } else {
            this.link = link;
        }
        this.text.setValue(link);

        this.close();
        this.onClose();
    }
Example #4
Source File: path.ts    From obsidian-fantasy-calendar with MIT License 6 votes vote down vote up
onChooseItem(item: TFile | HeadingCache | BlockCache) {
        if (item instanceof TFile) {
            this.text.setValue(item.basename);
            this.file = item;
            this.cache = this.app.metadataCache.getFileCache(this.file);
        } else if (Object.prototype.hasOwnProperty.call(item, "heading")) {
            this.text.setValue(
                this.file.basename + "#" + (<HeadingCache>item).heading
            );
        } else if (Object.prototype.hasOwnProperty.call(item, "id")) {
            this.text.setValue(
                this.file.basename + "^" + (<BlockCache>item).id
            );
        }
    }
Example #5
Source File: path.ts    From obsidian-fantasy-calendar with MIT License 6 votes vote down vote up
getItemText(item: TFile | HeadingCache | BlockCache) {
        if (item instanceof TFile) return item.path;
        if (Object.prototype.hasOwnProperty.call(item, "heading")) {
            return (<HeadingCache>item).heading;
        }
        if (Object.prototype.hasOwnProperty.call(item, "id")) {
            return (<BlockCache>item).id;
        }
    }
Example #6
Source File: utils.ts    From obsidian-switcher-plus with GNU General Public License v3.0 6 votes vote down vote up
export function isHeadingCache(obj: unknown): obj is HeadingCache {
  return isOfType<HeadingCache>(obj, 'level');
}
Example #7
Source File: fileCachedMetadata.fixture.ts    From obsidian-switcher-plus with GNU General Public License v3.0 6 votes vote down vote up
export function getHeadings(): HeadingCache[] {
  return [
    makeHeading('Title heading1', 1, makeLoc(8, 0, 65), makeLoc(8, 16, 81)),
    makeHeading('another heading1', 1, makeLoc(9, 0, 83), makeLoc(9, 16, 89)),
    makeHeading('More headings2', 2, makeLoc(28, 0, 418), makeLoc(28, 17, 435)),
    makeHeading('heading3', 3, makeLoc(30, 0, 437), makeLoc(30, 12, 449)),
    makeHeading('heading4', 4, makeLoc(32, 0, 451), makeLoc(32, 13, 464)),
    makeHeading('heading5', 5, makeLoc(34, 0, 466), makeLoc(34, 14, 480)),
    makeHeading('heading6', 6, makeLoc(36, 0, 482), makeLoc(36, 15, 497)),
  ];
}
Example #8
Source File: file.ts    From Obsidian_to_Anki with GNU General Public License v3.0 6 votes vote down vote up
getContextAtIndex(position: number): string {
        let result: string = this.path
        let currentContext: HeadingCache[] = []
        if (!(this.file_cache.hasOwnProperty('headings'))) {
            return result
        }
        for (let currentHeading of this.file_cache.headings) {
            if (position < currentHeading.position.start.offset) {
                //We've gone past position now with headings, so let's return!
                break
            }
            let insert_index: number = 0
            for (let contextHeading of currentContext) {
                if (currentHeading.level > contextHeading.level) {
                    insert_index += 1
                    continue
                }
                break
            }
            currentContext = currentContext.slice(0, insert_index)
            currentContext.push(currentHeading)
        }
        let heading_strs: string[] = []
        for (let contextHeading of currentContext) {
            heading_strs.push(contextHeading.heading)
        }
        let result_arr: string[] = [result]
        result_arr.push(...heading_strs)
        return result_arr.join(" > ")
    }
Example #9
Source File: headingsHandler.ts    From obsidian-switcher-plus with GNU General Public License v3.0 6 votes vote down vote up
private getRecentFilesSuggestions(): (HeadingSuggestion | FileSuggestion)[] {
    const suggestions: (HeadingSuggestion | FileSuggestion)[] = [];
    const { workspace, vault, metadataCache } = this.app;
    const recentFilePaths = workspace.getLastOpenFiles();

    recentFilePaths.forEach((path) => {
      const file = vault.getAbstractFileByPath(path);

      if (this.shouldIncludeFile(file)) {
        const f = file as TFile;
        let h1: HeadingCache = null;

        const h1s = metadataCache
          .getFileCache(f)
          ?.headings?.filter((h) => h.level === 1)
          .sort((a, b) => a.position.start.line - b.position.start.line);

        if (h1s?.length) {
          h1 = h1s[0];
        }

        const sugg = h1
          ? this.makeHeadingSuggestion(h1, f, null)
          : this.makeFileSuggestion(f, null);

        suggestions.push(sugg);
      }
    });

    return suggestions;
  }
Example #10
Source File: headingsHandler.ts    From obsidian-switcher-plus with GNU General Public License v3.0 6 votes vote down vote up
private makeHeadingSuggestion(
    item: HeadingCache,
    file: TFile,
    match: SearchResult,
  ): HeadingSuggestion {
    return {
      item,
      file,
      match,
      type: 'heading',
    };
  }
Example #11
Source File: headingsHandler.ts    From obsidian-switcher-plus with GNU General Public License v3.0 6 votes vote down vote up
private matchAndPushHeading(
    suggestions: HeadingSuggestion[],
    prepQuery: PreparedQuery,
    file: TFile,
    heading: HeadingCache,
  ): boolean {
    const { match } = this.matchStrings(prepQuery, heading.heading, null);

    if (match) {
      suggestions.push(this.makeHeadingSuggestion(heading, file, match));
    }

    return !!match;
  }
Example #12
Source File: headingsHandler.ts    From obsidian-switcher-plus with GNU General Public License v3.0 6 votes vote down vote up
private addHeadingSuggestions(
    suggestions: HeadingSuggestion[],
    prepQuery: PreparedQuery,
    file: TFile,
    allHeadings: boolean,
  ): boolean {
    const { metadataCache } = this.app;
    const headingList = metadataCache.getFileCache(file)?.headings ?? [];
    let h1: HeadingCache = null;
    let isH1Matched = false;
    let i = headingList.length;

    while (i--) {
      const heading = headingList[i];
      let isMatched = false;

      if (allHeadings) {
        isMatched = this.matchAndPushHeading(suggestions, prepQuery, file, heading);
      }

      if (heading.level === 1) {
        const { line } = heading.position.start;

        if (h1 === null || line < h1.position.start.line) {
          h1 = heading;
          isH1Matched = isMatched;
        }
      }
    }

    if (!allHeadings && h1) {
      isH1Matched = this.matchAndPushHeading(suggestions, prepQuery, file, h1);
    }

    return isH1Matched;
  }
Example #13
Source File: handler.ts    From obsidian-switcher-plus with GNU General Public License v3.0 6 votes vote down vote up
/**
   * Finds and returns the first H1 from sourceFile
   * @param  {TFile} sourceFile
   * @returns HeadingCache
   */
  getFirstH1(sourceFile: TFile): HeadingCache | null {
    let h1: HeadingCache = null;
    const { metadataCache } = this.app;
    const headingList: HeadingCache[] =
      metadataCache.getFileCache(sourceFile)?.headings?.filter((v) => v.level === 1) ??
      [];

    if (headingList.length) {
      h1 = headingList.reduce((acc, curr) => {
        const { line: currLine } = curr.position.start;
        const accLine = acc.position.start.line;

        return currLine < accLine ? curr : acc;
      });
    }

    return h1;
  }
Example #14
Source File: markdown-file.ts    From obsidian-dataview with MIT License 6 votes vote down vote up
/** Find the header that is most immediately above the given line number. */
export function findPreviousHeader(line: number, headers: HeadingCache[]): string | undefined {
    if (headers.length == 0) return undefined;
    if (headers[0].position.start.line > line) return undefined;

    let index = headers.length - 1;
    while (index >= 0 && headers[index].position.start.line > line) index--;

    return headers[index].heading;
}
Example #15
Source File: path.ts    From obsidian-fantasy-calendar with MIT License 5 votes vote down vote up
renderSuggestion(
        result: FuzzyMatch<TFile | BlockCache | HeadingCache>,
        el: HTMLElement
    ) {
        let { item, match: matches } = result || {};
        let content = el.createDiv({
            cls: "suggestion-content"
        });
        if (!item) {
            content.setText(this.emptyStateText);
            content.parentElement.addClass("is-selected");
            return;
        }

        if (item instanceof TFile) {
            let pathLength = item.path.length - item.name.length;
            const matchElements = matches.matches.map((m) => {
                return createSpan("suggestion-highlight");
            });
            for (
                let i = pathLength;
                i < item.path.length - item.extension.length - 1;
                i++
            ) {
                let match = matches.matches.find((m) => m[0] === i);
                if (match) {
                    let element = matchElements[matches.matches.indexOf(match)];
                    content.appendChild(element);
                    element.appendText(item.path.substring(match[0], match[1]));

                    i += match[1] - match[0] - 1;
                    continue;
                }

                content.appendText(item.path[i]);
            }
            el.createDiv({
                cls: "suggestion-note",
                text: item.path
            });
        } else if (Object.prototype.hasOwnProperty.call(item, "heading")) {
            content.setText((<HeadingCache>item).heading);
            content.prepend(
                createSpan({
                    cls: "suggestion-flair",
                    text: `H${(<HeadingCache>item).level}`
                })
            );
        } else if (Object.prototype.hasOwnProperty.call(item, "id")) {
            content.setText((<BlockCache>item).id);
        }
    }
Example #16
Source File: symbolHandler.test.ts    From obsidian-switcher-plus with GNU General Public License v3.0 4 votes vote down vote up
describe('symbolHandler', () => {
  const rootFixture = rootSplitEditorFixtures[0];
  const leftFixture = leftSplitEditorFixtures[0];
  let settings: SwitcherPlusSettings;
  let mockApp: MockProxy<App>;
  let mockWorkspace: MockProxy<Workspace>;
  let sut: SymbolHandler;
  let mockMetadataCache: MockProxy<MetadataCache>;
  let mockRootSplitLeaf: MockProxy<WorkspaceLeaf>;
  let mockLeftSplitLeaf: MockProxy<WorkspaceLeaf>;
  let inputText: string;
  let startIndex: number;
  let filterText: string;
  let symbolSugg: SymbolSuggestion;

  beforeAll(() => {
    mockMetadataCache = mock<MetadataCache>();
    mockMetadataCache.getFileCache.mockImplementation((_f) => rootFixture.cachedMetadata);

    mockWorkspace = mock<Workspace>({ activeLeaf: null });
    mockApp = mock<App>({
      workspace: mockWorkspace,
      metadataCache: mockMetadataCache,
      vault: mock<Vault>(),
    });

    settings = new SwitcherPlusSettings(null);
    jest.spyOn(settings, 'symbolListCommand', 'get').mockReturnValue(symbolTrigger);

    mockRootSplitLeaf = makeLeaf();
    mockLeftSplitLeaf = makeLeaf();

    symbolSugg = {
      type: 'symbol',
      file: new TFile(),
      item: {
        type: 'symbolInfo',
        symbol: getHeadings()[0],
        symbolType: SymbolType.Heading,
      },
      match: null,
    };
  });

  beforeEach(() => {
    // reset for each test because symbol mode will use saved data from previous runs
    sut = new SymbolHandler(mockApp, settings);
  });

  describe('commandString', () => {
    it('should return symbolListCommand trigger', () => {
      expect(sut.commandString).toBe(symbolTrigger);
    });
  });

  describe('validateCommand', () => {
    filterText = 'foo';

    beforeAll(() => {
      inputText = `${symbolTrigger}${filterText}`;
      startIndex = 0;
    });

    it('should validate parsed input in symbol prefix (active editor) mode', () => {
      const inputInfo = new InputInfo(inputText);

      sut.validateCommand(inputInfo, startIndex, filterText, null, mockRootSplitLeaf);
      expect(inputInfo.mode).toBe(Mode.SymbolList);

      const symbolCmd = inputInfo.parsedCommand();
      expect(symbolCmd.parsedInput).toBe(filterText);
      expect(symbolCmd.isValidated).toBe(true);
    });

    it('should validate parsed input for file based suggestion', () => {
      const targetFile = new TFile();

      const sugg: AliasSuggestion = {
        file: targetFile,
        alias: 'foo',
        type: 'alias',
        match: null,
      };

      const inputInfo = new InputInfo('', Mode.Standard);
      sut.validateCommand(inputInfo, 0, '', sugg, null);

      expect(inputInfo.mode).toBe(Mode.SymbolList);

      const symbolCmd = inputInfo.parsedCommand() as SourcedParsedCommand;
      expect(symbolCmd.isValidated).toBe(true);
      expect(symbolCmd.source).toEqual(
        expect.objectContaining({
          file: targetFile,
          leaf: null,
          suggestion: sugg,
          isValidSource: true,
        }),
      );
    });

    it('should validate parsed input for editor suggestion', () => {
      const targetLeaf = makeLeaf();
      mockWorkspace.activeLeaf = targetLeaf; // <-- set the target as a currently open leaf

      const sugg: EditorSuggestion = {
        item: targetLeaf,
        file: targetLeaf.view.file,
        type: 'editor',
        match: null,
      };

      const inputInfo = new InputInfo('', Mode.EditorList);
      sut.validateCommand(inputInfo, 0, '', sugg, null);

      expect(inputInfo.mode).toBe(Mode.SymbolList);

      const symbolCmd = inputInfo.parsedCommand() as SourcedParsedCommand;
      expect(symbolCmd.isValidated).toBe(true);
      expect(symbolCmd.source).toEqual(
        expect.objectContaining({
          file: targetLeaf.view.file,
          leaf: targetLeaf,
          suggestion: sugg,
          isValidSource: true,
        }),
      );

      mockWorkspace.activeLeaf = null;
    });

    it('should validate parsed input for starred file suggestion', () => {
      const targetFile = new TFile();
      const item = makeFileStarredItem(targetFile.basename);

      const sugg: StarredSuggestion = {
        item,
        type: 'starred',
        file: targetFile,
        match: null,
      };

      (mockApp.vault as MockProxy<Vault>).getAbstractFileByPath
        .calledWith(targetFile.path)
        .mockReturnValueOnce(targetFile);

      const inputInfo = new InputInfo('', Mode.StarredList);
      sut.validateCommand(inputInfo, 0, '', sugg, null);

      expect(inputInfo.mode).toBe(Mode.SymbolList);

      const symbolCmd = inputInfo.parsedCommand() as SourcedParsedCommand;
      expect(symbolCmd.isValidated).toBe(true);
      expect(symbolCmd.source).toEqual(
        expect.objectContaining({
          file: targetFile,
          leaf: null,
          suggestion: sugg,
          isValidSource: true,
        }),
      );
    });

    it('should not validate parsed input for starred search suggestion', () => {
      const item = makeSearchStarredItem();

      const sugg: StarredSuggestion = {
        item,
        file: null,
        type: 'starred',
        match: null,
      };

      const inputInfo = new InputInfo('', Mode.StarredList);
      sut.validateCommand(inputInfo, 0, '', sugg, null);

      expect(inputInfo.mode).toBe(Mode.StarredList);

      const symbolCmd = inputInfo.parsedCommand(Mode.SymbolList) as SourcedParsedCommand;
      expect(symbolCmd.isValidated).toBe(false);
      expect(symbolCmd.source).toBeNull();
    });

    it('should validate and identify active editor as matching the file suggestion target', () => {
      const targetLeaf = makeLeaf();
      mockWorkspace.activeLeaf = targetLeaf; // <-- set the target as a currently open leaf

      const sugg: AliasSuggestion = {
        file: targetLeaf.view.file,
        alias: 'foo',
        type: 'alias',
        match: null,
      };

      const inputInfo = new InputInfo('', Mode.Standard);
      sut.validateCommand(inputInfo, 0, '', sugg, null);

      expect(inputInfo.mode).toBe(Mode.SymbolList);

      const symbolCmd = inputInfo.parsedCommand() as SourcedParsedCommand;
      expect(symbolCmd.isValidated).toBe(true);
      expect(symbolCmd.source).toEqual(
        expect.objectContaining({
          file: targetLeaf.view.file,
          leaf: targetLeaf,
          suggestion: sugg,
          isValidSource: true,
        }),
      );

      mockWorkspace.activeLeaf = null;
    });

    it('should validate and identify in-active editor as matching the file suggestion target file', () => {
      const targetLeaf = makeLeaf();

      const sugg: AliasSuggestion = {
        file: targetLeaf.view.file,
        alias: 'foo',
        type: 'alias',
        match: null,
      };

      mockWorkspace.activeLeaf = null; // <-- clear out active leaf
      mockWorkspace.iterateAllLeaves.mockImplementation((callback) => {
        callback(targetLeaf); // <-- report targetLeaf and an in-active open leaf
      });

      const inputInfo = new InputInfo('', Mode.Standard);
      sut.validateCommand(inputInfo, 0, '', sugg, null);

      expect(inputInfo.mode).toBe(Mode.SymbolList);

      const symbolCmd = inputInfo.parsedCommand() as SourcedParsedCommand;
      expect(symbolCmd.isValidated).toBe(true);
      expect(symbolCmd.source).toEqual(
        expect.objectContaining({
          file: targetLeaf.view.file,
          leaf: targetLeaf,
          suggestion: sugg,
          isValidSource: true,
        }),
      );

      mockWorkspace.iterateAllLeaves.mockReset();
    });
  });

  describe('getSuggestions', () => {
    const mockPrepareQuery = jest.mocked<typeof prepareQuery>(prepareQuery);
    const mockFuzzySearch = jest.mocked<typeof fuzzySearch>(fuzzySearch);

    test('with falsy input, it should return an empty array', () => {
      const results = sut.getSuggestions(null);

      expect(results).not.toBeNull();
      expect(results).toBeInstanceOf(Array);
      expect(results).toHaveLength(0);
    });

    test('that SymbolSuggestion have a file property to enable interop with other plugins (like HoverEditor)', () => {
      const inputInfo = new InputInfo(symbolTrigger);
      const results = sut.getSuggestions(inputInfo);

      expect(results.every((v) => v.file !== null)).toBe(true);
    });

    test('with default settings, it should return symbol suggestions', () => {
      const inputInfo = new InputInfo(symbolTrigger);
      sut.validateCommand(inputInfo, 0, '', null, mockRootSplitLeaf);
      expect(inputInfo.mode).toBe(Mode.SymbolList);

      const results = sut.getSuggestions(inputInfo);

      expect(results).not.toBeNull();
      expect(results).toBeInstanceOf(Array);
      expect(results.every((sugg) => sugg.type === 'symbol')).toBe(true);

      const set = new Set(results.map((sugg) => sugg.item.symbol));

      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
      const cached: AnySymbolInfoPayload[] = Object.values(
        rootFixture.cachedMetadata,
      ).flat();

      expect(results).toHaveLength(cached.length);
      expect(cached.every((item) => set.has(item))).toBe(true);

      expect(mockMetadataCache.getFileCache).toHaveBeenCalledWith(
        mockRootSplitLeaf.view.file,
      );
      expect(mockPrepareQuery).toHaveBeenCalled();
    });

    test('with selectNearestHeading set to true, it should set the isSelected property of the nearest preceding heading suggestion to true when the file is open in the active editor for any file based suggestion modes', () => {
      const selectNearestHeadingSpy = jest
        .spyOn(settings, 'selectNearestHeading', 'get')
        .mockReturnValue(true);

      // there should be a heading in the fixture that starts on this line number
      const expectedHeadingStartLineNumber = 9;
      const expectedSelectedHeading = rootFixture.cachedMetadata.headings.find(
        (val) => val.position.start.line === expectedHeadingStartLineNumber,
      );
      expect(expectedSelectedHeading).not.toBeNull();

      const mockEditor = (mockRootSplitLeaf.view as MarkdownView)
        .editor as MockProxy<Editor>;

      mockEditor.getCursor.mockReturnValue({
        line: expectedHeadingStartLineNumber + 1,
        ch: 0,
      });

      mockWorkspace.activeLeaf = mockRootSplitLeaf;

      const activeSugg: HeadingSuggestion = {
        item: makeHeading('foo heading', 1),
        file: mockWorkspace.activeLeaf.view.file, // <-- here, use the same TFile as ActiveLeaf
        match: null,
        type: 'heading',
      };

      // use headings prefix mode along with heading suggestion, note that the suggestion
      // has to point to the same TFile as 'activeLeaf'
      const inputInfo = new InputInfo('', Mode.HeadingsList);
      sut.validateCommand(
        inputInfo,
        headingsTrigger.length,
        '',
        activeSugg,
        mockWorkspace.activeLeaf,
      );

      const results = sut.getSuggestions(inputInfo);

      expect(inputInfo.mode).toBe(Mode.SymbolList);
      expect(results).toBeInstanceOf(Array);
      expect(results.every((sugg) => sugg.type === 'symbol')).toBe(true);

      const selectedSuggestions = results.filter((v) => v.item.isSelected === true);
      expect(selectedSuggestions).toHaveLength(1);
      expect(selectedSuggestions[0].item.symbol).toBe(expectedSelectedHeading);

      expect(mockMetadataCache.getFileCache).toHaveBeenCalledWith(
        mockWorkspace.activeLeaf.view.file,
      );
      expect(mockPrepareQuery).toHaveBeenCalled();

      selectNearestHeadingSpy.mockReset();
      mockEditor.getCursor.mockReset();
      mockWorkspace.activeLeaf = null;
    });

    test('with selectNearestHeading set to true, it should set the isSelected property of the nearest preceding heading suggestion to true', () => {
      const selectNearestHeadingSpy = jest
        .spyOn(settings, 'selectNearestHeading', 'get')
        .mockReturnValue(true);

      // there should be a heading in the fixture that starts on this line number
      const expectedHeadingStartLineNumber = 9;
      const expectedSelectedHeading = rootFixture.cachedMetadata.headings.find(
        (val) => val.position.start.line === expectedHeadingStartLineNumber,
      );
      expect(expectedSelectedHeading).not.toBeNull();

      const mockEditor = (mockRootSplitLeaf.view as MarkdownView)
        .editor as MockProxy<Editor>;

      mockEditor.getCursor.mockReturnValueOnce({
        line: expectedHeadingStartLineNumber + 1,
        ch: 0,
      });

      const inputInfo = new InputInfo(symbolTrigger);
      sut.validateCommand(inputInfo, 0, '', null, mockRootSplitLeaf);
      expect(inputInfo.mode).toBe(Mode.SymbolList);

      const results = sut.getSuggestions(inputInfo);

      expect(results).not.toBeNull();
      expect(results).toBeInstanceOf(Array);
      expect(results.every((sugg) => sugg.type === 'symbol')).toBe(true);

      const selectedSuggestions = results.filter((v) => v.item.isSelected === true);
      expect(selectedSuggestions).toHaveLength(1);
      expect(selectedSuggestions[0].item.symbol).toBe(expectedSelectedHeading);

      expect(mockMetadataCache.getFileCache).toHaveBeenCalledWith(
        mockRootSplitLeaf.view.file,
      );
      expect(mockPrepareQuery).toHaveBeenCalled();

      selectNearestHeadingSpy.mockReset();
    });

    test('with filter search term, it should return only matching symbol suggestions', () => {
      mockMetadataCache.getFileCache.mockReturnValueOnce(leftFixture.cachedMetadata);
      mockPrepareQuery.mockReturnValueOnce(makePreparedQuery(filterText));
      mockFuzzySearch.mockImplementation(
        (_q: PreparedQuery, text: string): SearchResult => {
          const match = makeFuzzyMatch();
          return text === 'tag1' || text === 'tag2' ? match : null;
        },
      );

      filterText = 'tag';
      inputText = `${symbolTrigger}${filterText}`;
      startIndex = 0;
      const inputInfo = new InputInfo(inputText);
      sut.validateCommand(inputInfo, startIndex, filterText, null, mockLeftSplitLeaf);
      expect(inputInfo.mode).toBe(Mode.SymbolList);

      const results = sut.getSuggestions(inputInfo);

      expect(results).not.toBeNull();
      expect(results).toBeInstanceOf(Array);
      expect(results).toHaveLength(2);
      expect(results.every((sugg) => sugg.type === 'symbol')).toBe(true);

      const { tags } = leftFixture.cachedMetadata;
      const resTags = new Set(results.map((sugg) => sugg.item.symbol));
      expect(tags.every((tag) => resTags.has(tag))).toBe(true);

      expect(mockMetadataCache.getFileCache).toHaveBeenCalledWith(
        mockLeftSplitLeaf.view.file,
      );
      expect(mockPrepareQuery).toHaveBeenCalled();
      expect(mockFuzzySearch).toHaveBeenCalled();

      mockFuzzySearch.mockReset();
    });

    test('with existing filter search term, it should continue refining suggestions for the previous target', () => {
      mockMetadataCache.getFileCache.mockReturnValue(leftFixture.cachedMetadata);

      // 1) setup first initial run
      filterText = 'tag';
      inputText = `${symbolTrigger}${filterText}`;
      startIndex = 0;

      mockPrepareQuery.mockReturnValueOnce(makePreparedQuery(filterText));

      mockFuzzySearch.mockImplementation((_q: PreparedQuery, text: string) => {
        const match = makeFuzzyMatch();
        return text === 'tag1' || text === 'tag2' ? match : null;
      });

      let inputInfo = new InputInfo(inputText);

      sut.validateCommand(inputInfo, startIndex, filterText, null, mockLeftSplitLeaf);
      expect(inputInfo.mode).toBe(Mode.SymbolList);

      let results = sut.getSuggestions(inputInfo);

      expect(results).not.toBeNull();
      expect(results).toBeInstanceOf(Array);
      expect(results).toHaveLength(2);
      expect(results.every((sugg) => sugg.type === 'symbol')).toBe(true);
      expect(mockMetadataCache.getFileCache).toHaveBeenCalledWith(
        mockLeftSplitLeaf.view.file,
      );
      mockFuzzySearch.mockReset();

      // 2) setup second run, which refines the filterText from the first run
      filterText = 'tag2';
      mockPrepareQuery.mockReturnValueOnce(makePreparedQuery(filterText));

      mockFuzzySearch.mockImplementation((q: PreparedQuery, text: string) => {
        const match = makeFuzzyMatch([[0, 4]], -0.0104);
        return text === q.query ? match : null;
      });

      const mockTempLeaf = makeLeaf();
      const mockTempLeafFile = mockTempLeaf.view.file;

      inputText = `${symbolTrigger}${filterText}`;
      inputInfo = new InputInfo(inputText);

      // note the use of a different leaf than the first run
      sut.validateCommand(inputInfo, startIndex, filterText, null, mockTempLeaf);
      expect(inputInfo.mode).toBe(Mode.SymbolList);

      results = sut.getSuggestions(inputInfo);

      expect(results).not.toBeNull();
      expect(results).toBeInstanceOf(Array);
      expect(results).toHaveLength(1); // expect just 1 this time
      expect(results[0]).toHaveProperty('type', 'symbol');

      const tag = leftFixture.cachedMetadata.tags.find((item) => item.tag === '#tag2');
      expect(results[0]).toHaveProperty('item.symbol', tag);

      // getFileCache should be called with leftSplitLeaf.view.file both times
      expect(mockMetadataCache.getFileCache).not.toHaveBeenCalledWith(mockTempLeafFile);
      expect(mockMetadataCache.getFileCache).toHaveBeenCalledWith(
        mockLeftSplitLeaf.view.file,
      );
      expect(mockPrepareQuery).toHaveBeenCalled();

      mockMetadataCache.getFileCache.mockReset();
      mockFuzzySearch.mockReset();
    });

    it('should not return suggestions for a symbol type that is disabled', () => {
      const inputInfo = new InputInfo(symbolTrigger);

      const isSymbolTypeEnabledSpy = jest
        .spyOn(settings, 'isSymbolTypeEnabled')
        .mockImplementation((type) => (type === SymbolType.Tag ? false : true));

      mockMetadataCache.getFileCache.mockReturnValueOnce({ tags: getTags() });
      sut.validateCommand(inputInfo, 0, '', null, mockRootSplitLeaf);
      expect(inputInfo.mode).toBe(Mode.SymbolList);

      const results = sut.getSuggestions(inputInfo);

      expect(results).toBeInstanceOf(Array);
      expect(results).toHaveLength(0);

      expect(mockMetadataCache.getFileCache).toHaveBeenCalledWith(
        mockRootSplitLeaf.view.file,
      );
      expect(mockPrepareQuery).toHaveBeenCalled();

      isSymbolTypeEnabledSpy.mockRestore();
    });

    it('should not return suggestions for links if the Link symbol type is disabled', () => {
      const inputInfo = new InputInfo(symbolTrigger);

      const isSymbolTypeEnabledSpy = jest
        .spyOn(settings, 'isSymbolTypeEnabled')
        .mockImplementation((type) => (type === SymbolType.Link ? false : true));

      mockMetadataCache.getFileCache.mockReturnValueOnce({ links: getLinks() });
      sut.validateCommand(inputInfo, 0, '', null, mockRootSplitLeaf);
      expect(inputInfo.mode).toBe(Mode.SymbolList);

      const results = sut.getSuggestions(inputInfo);

      expect(results).toBeInstanceOf(Array);
      expect(results).toHaveLength(0);

      expect(mockMetadataCache.getFileCache).toHaveBeenCalledWith(
        mockRootSplitLeaf.view.file,
      );
      expect(mockPrepareQuery).toHaveBeenCalled();

      isSymbolTypeEnabledSpy.mockRestore();
    });

    it('should not return suggestions for a sub-link type that is disabled', () => {
      const inputInfo = new InputInfo(symbolTrigger);

      const excludeLinkSubTypesSpy = jest
        .spyOn(settings, 'excludeLinkSubTypes', 'get')
        .mockReturnValue(LinkType.Block | LinkType.Heading);

      mockMetadataCache.getFileCache.mockReturnValueOnce({ links: getLinks() });
      sut.validateCommand(inputInfo, 0, '', null, mockRootSplitLeaf);
      expect(inputInfo.mode).toBe(Mode.SymbolList);

      const results = sut.getSuggestions(inputInfo);

      expect(results).toBeInstanceOf(Array);

      // getLinks fixture returns 2 links, 1 block, 1 normal
      expect(results).toHaveLength(1);

      expect(mockMetadataCache.getFileCache).toHaveBeenCalledWith(
        mockRootSplitLeaf.view.file,
      );
      expect(mockPrepareQuery).toHaveBeenCalled();

      excludeLinkSubTypesSpy.mockRestore();
    });
  });

  describe('renderSuggestion', () => {
    const mockRenderResults = jest.mocked<typeof renderResults>(renderResults);
    let mockTextSpan: MockProxy<HTMLSpanElement>;
    let mockParentEl: MockProxy<HTMLElement>;

    beforeAll(() => {
      mockTextSpan = mock<HTMLSpanElement>();
      mockParentEl = mock<HTMLElement>();
      mockParentEl.createSpan.mockImplementation(() => mockTextSpan);
    });

    it('should not throw an error with a null suggestion', () => {
      expect(() => sut.renderSuggestion(null, null)).not.toThrow();
    });

    it('should render Heading suggestion', () => {
      sut.renderSuggestion(symbolSugg, mockParentEl);

      expect(mockRenderResults).toHaveBeenCalledWith(
        mockTextSpan,
        (symbolSugg.item.symbol as HeadingCache).heading,
        symbolSugg.match,
      );

      expect(mockParentEl.createSpan).toHaveBeenCalledWith(
        expect.objectContaining({ cls: 'qsp-symbol-text' }),
      );
    });

    it('should render Tag suggestion', () => {
      const tagSugg: SymbolSuggestion = {
        type: 'symbol',
        file: null,
        item: {
          type: 'symbolInfo',
          symbol: getTags()[0],
          symbolType: SymbolType.Tag,
        },
        match: null,
      };

      sut.renderSuggestion(tagSugg, mockParentEl);

      expect(mockRenderResults).toHaveBeenCalledWith(
        mockTextSpan,
        (tagSugg.item.symbol as TagCache).tag.slice(1),
        tagSugg.match,
      );

      expect(mockParentEl.createSpan).toHaveBeenCalledWith(
        expect.objectContaining({ cls: 'qsp-symbol-text' }),
      );
    });

    it('should render Link suggestion', () => {
      const linkSugg: SymbolSuggestion = {
        type: 'symbol',
        file: null,
        item: {
          type: 'symbolInfo',
          symbol: getLinks()[1],
          symbolType: SymbolType.Link,
        },
        match: null,
      };

      sut.renderSuggestion(linkSugg, mockParentEl);

      const { link, displayText } = linkSugg.item.symbol as ReferenceCache;
      expect(mockRenderResults).toHaveBeenCalledWith(
        mockTextSpan,
        `${link}|${displayText}`,
        linkSugg.match,
      );

      expect(mockParentEl.createSpan).toHaveBeenCalledWith(
        expect.objectContaining({ cls: 'qsp-symbol-text' }),
      );
    });

    it('should add a symbol indicator', () => {
      sut.renderSuggestion(symbolSugg, mockParentEl);

      expect(mockParentEl.createDiv).toHaveBeenCalledWith(
        expect.objectContaining({
          text: HeadingIndicators[(symbolSugg.item.symbol as HeadingCache).level],
          cls: 'qsp-symbol-indicator',
        }),
      );
    });

    test('with symbolsInLineOrder enabled and no search term, it should indent symbols', () => {
      const settings = new SwitcherPlusSettings(null);
      jest.spyOn(settings, 'symbolsInLineOrder', 'get').mockReturnValue(true);

      const inputInfo = new InputInfo(symbolTrigger);
      sut.validateCommand(inputInfo, 0, 'foo', null, mockRootSplitLeaf);

      sut = new SymbolHandler(mockApp, settings);
      sut.getSuggestions(inputInfo);

      sut.renderSuggestion(symbolSugg, mockParentEl);

      expect(mockParentEl.addClass).toHaveBeenCalledWith(
        `qsp-symbol-l${symbolSugg.item.indentLevel}`,
      );
    });
  });

  describe('onChooseSuggestion', () => {
    const mockKeymap = jest.mocked<typeof Keymap>(Keymap);

    beforeAll(() => {
      const fileContainerLeaf = makeLeaf();
      fileContainerLeaf.openFile.mockResolvedValueOnce();
      mockWorkspace.getLeaf.mockReturnValue(fileContainerLeaf);
    });

    const getExpectedEphemeralState = (sugg: SymbolSuggestion): OpenViewState => {
      const {
        start: { line, col },
        end: endLoc,
      } = sugg.item.symbol.position;

      const state: Record<string, unknown> = {
        active: true,
        eState: {
          active: true,
          focus: true,
          startLoc: { line, col },
          endLoc,
          line,
          cursor: {
            from: { line, ch: col },
            to: { line, ch: col },
          },
        },
      };

      return state;
    };

    it('should not throw an error with a null suggestion', () => {
      expect(() => sut.onChooseSuggestion(null, null)).not.toThrow();
    });

    it('should activate the existing workspaceLeaf that contains the target symbol and scroll that view via eState', () => {
      const isModDown = false;
      const expectedState = getExpectedEphemeralState(symbolSugg);
      const mockLeaf = makeLeaf(symbolSugg.file);

      mockKeymap.isModEvent.mockReturnValueOnce(isModDown);
      const navigateToLeafOrOpenFileSpy = jest.spyOn(
        Handler.prototype,
        'navigateToLeafOrOpenFile',
      );

      const inputInfo = new InputInfo(symbolTrigger);
      sut.validateCommand(inputInfo, 0, '', null, mockLeaf);
      sut.getSuggestions(inputInfo);
      expect(inputInfo.mode).toBe(Mode.SymbolList);

      sut.onChooseSuggestion(symbolSugg, null);

      expect(inputInfo.mode).toBe(Mode.SymbolList);
      expect(mockKeymap.isModEvent).toHaveBeenCalled();
      expect(navigateToLeafOrOpenFileSpy).toHaveBeenCalledWith(
        isModDown,
        symbolSugg.file,
        expect.any(String),
        expectedState,
        mockLeaf,
        Mode.SymbolList,
      );

      navigateToLeafOrOpenFileSpy.mockRestore();
    });
  });
});
Example #17
Source File: handler.test.ts    From obsidian-switcher-plus with GNU General Public License v3.0 4 votes vote down vote up
describe('Handler', () => {
  let mockApp: MockProxy<App>;
  let mockWorkspace: MockProxy<Workspace>;
  let mockMetadataCache: MockProxy<MetadataCache>;
  let mockSettings: MockProxy<SwitcherPlusSettings>;
  let sut: SUT;

  beforeAll(() => {
    mockWorkspace = mock<Workspace>({
      rootSplit: mock<WorkspaceSplit>(),
      leftSplit: mock<WorkspaceSplit>(),
      rightSplit: mock<WorkspaceSplit>(),
    });

    mockMetadataCache = mock<MetadataCache>();
    mockApp = mock<App>({ workspace: mockWorkspace, metadataCache: mockMetadataCache });

    mockSettings = mock<SwitcherPlusSettings>({
      excludeViewTypes: [],
      referenceViews: [],
      includeSidePanelViewTypes: [],
    });

    sut = new SUT(mockApp, mockSettings);
  });

  describe('commandString property', () => {
    it('should return null', () => {
      expect(sut.commandString).toBeNull();
    });
  });

  describe('validateCommand', () => {
    it('should not throw', () => {
      expect(() => sut.validateCommand(null, 0, null, null, null)).not.toThrow();
    });
  });

  describe('getSuggestions', () => {
    it('should return an empy array', () => {
      const result = sut.getSuggestions(null);

      expect(result).toBeInstanceOf(Array);
      expect(result).toHaveLength(0);
    });
  });

  describe('renderSuggestion', () => {
    it('should not throw', () => {
      expect(() => sut.renderSuggestion(null, null)).not.toThrow();
    });
  });

  describe('onChooseSuggestion', () => {
    it('should not throw', () => {
      expect(() => sut.onChooseSuggestion(null, null)).not.toThrow();
    });
  });

  describe('getEditorInfo', () => {
    it('should return an object with falsy values for falsy input', () => {
      const result = sut.getEditorInfo(null);

      expect(result).toEqual(
        expect.objectContaining({
          isValidSource: false,
          leaf: null,
          file: null,
          suggestion: null,
          cursor: null,
        }),
      );
    });

    it('should return TargetInfo for a markdown WorkspaceLeaf', () => {
      const mockFile = new TFile();
      const mockCursorPos = mock<EditorPosition>();
      const mockView = mock<MarkdownView>({
        file: mockFile,
      });

      mockView.getViewType.mockReturnValueOnce('markdown');
      const getCursorPosSpy = jest.spyOn(sut, 'getCursorPosition');
      getCursorPosSpy.mockReturnValueOnce(mockCursorPos);

      const mockLeaf = mock<WorkspaceLeaf>({ view: mockView });

      const result = sut.getEditorInfo(mockLeaf);

      expect(mockView.getViewType).toHaveBeenCalled();
      expect(getCursorPosSpy).toHaveBeenCalledWith(mockView);
      expect(result).toEqual(
        expect.objectContaining({
          isValidSource: true,
          leaf: mockLeaf,
          file: mockFile,
          suggestion: null,
          cursor: mockCursorPos,
        }),
      );

      getCursorPosSpy.mockRestore();
    });
  });

  describe('getSuggestionInfo', () => {
    it('should return an object with falsy values for falsy input', () => {
      const result = sut.getSuggestionInfo(null);

      expect(result).toEqual(
        expect.objectContaining({
          isValidSource: false,
          leaf: null,
          file: null,
          suggestion: null,
          cursor: null,
        }),
      );
    });

    it('should return TargetInfo for EditorSuggestion using active workspace leaf', () => {
      const mockFile = new TFile();
      const mockCursorPos = mock<EditorPosition>();
      const mockView = mock<MarkdownView>({
        file: mockFile,
      });

      const getCursorPosSpy = jest.spyOn(sut, 'getCursorPosition');
      getCursorPosSpy.mockReturnValueOnce(mockCursorPos);

      const mockLeaf = mock<WorkspaceLeaf>({ view: mockView });

      mockWorkspace.activeLeaf = mockLeaf; // <- set as active leaf

      const sugg: EditorSuggestion = {
        type: 'editor',
        file: mockFile,
        item: mockLeaf,
        match: null,
      };

      const result = sut.getSuggestionInfo(sugg);

      expect(getCursorPosSpy).toHaveBeenCalledWith(mockView);
      expect(result).toEqual(
        expect.objectContaining({
          isValidSource: true,
          leaf: mockWorkspace.activeLeaf,
          file: mockFile,
          suggestion: sugg,
          cursor: mockCursorPos,
        }),
      );

      getCursorPosSpy.mockRestore();
      mockWorkspace.activeLeaf = null;
    });
  });

  describe('getCursorPosition', () => {
    let mockView: MockProxy<MarkdownView>;
    let mockEditor: MockProxy<Editor>;

    beforeAll(() => {
      mockEditor = mock<Editor>();
      mockView = mock<MarkdownView>({
        editor: mockEditor,
      });
    });

    it('should not throw on falsy input', () => {
      let result;

      expect(() => {
        result = sut.getCursorPosition(null);
      }).not.toThrow();

      expect(result).toBe(null);
    });

    it('should return null for view type that is not markdown', () => {
      mockView.getViewType.mockReturnValueOnce('not markdown');
      const result = sut.getCursorPosition(mockView);

      expect(result).toBe(null);
      expect(mockView.getViewType).toHaveBeenCalled();
    });

    it('should return null for view that is in preview mode', () => {
      mockView.getViewType.mockReturnValueOnce('markdown');
      mockView.getMode.mockReturnValueOnce('preview');

      const result = sut.getCursorPosition(mockView);

      expect(result).toBe(null);
      expect(mockView.getMode).toHaveBeenCalled();
    });

    it('should return cursor position for markdown view that is not in preview mode', () => {
      const mockCursorPos = mock<EditorPosition>();

      mockView.getViewType.mockReturnValueOnce('markdown');
      mockView.getMode.mockReturnValueOnce('source');
      mockEditor.getCursor.mockReturnValueOnce(mockCursorPos);

      const result = sut.getCursorPosition(mockView);

      expect(result).toBe(mockCursorPos);
      expect(mockView.getViewType).toHaveBeenCalled();
      expect(mockView.getMode).toHaveBeenCalled();
      expect(mockEditor.getCursor).toHaveBeenCalledWith('head');
    });
  });

  describe('getTitleText', () => {
    it('should return file path for file without H1', () => {
      const mockFile = new TFile();

      const result = sut.getTitleText(mockFile);

      expect(result).toBe(stripMDExtensionFromPath(mockFile));
    });

    it('should return H1 text for file with H1', () => {
      const mockFile = new TFile();
      const headingText = 'h1 heading text';
      const mockHeading = mock<HeadingCache>({ heading: headingText, level: 1 });

      mockMetadataCache.getFileCache
        .calledWith(mockFile)
        .mockReturnValueOnce({ headings: [mockHeading] });

      const result = sut.getTitleText(mockFile);

      expect(mockMetadataCache.getFileCache).toHaveBeenCalledWith(mockFile);
      expect(result).toBe(headingText);
    });
  });

  describe('getFirstH1', () => {
    let mockH1: MockProxy<HeadingCache>;
    let mockH2: MockProxy<HeadingCache>;

    beforeAll(() => {
      mockH1 = mock<HeadingCache>({
        level: 1,
        position: mock<Pos>({
          start: mock<Loc>({ line: 5 }),
        }),
      });

      mockH2 = mock<HeadingCache>({
        level: 2,
        position: mock<Pos>({
          start: mock<Loc>({ line: 10 }),
        }),
      });
    });

    it('should return null if there is no fileCache available', () => {
      const mockFile = new TFile();
      mockMetadataCache.getFileCache.calledWith(mockFile).mockReturnValueOnce(null);

      const result = sut.getFirstH1(mockFile);

      expect(result).toBe(null);
      expect(mockMetadataCache.getFileCache).toHaveBeenCalledWith(mockFile);
    });

    it('should return null if there are no headings', () => {
      const mockFile = new TFile();
      mockMetadataCache.getFileCache
        .calledWith(mockFile)
        .mockReturnValueOnce({ headings: [] });

      const result = sut.getFirstH1(mockFile);

      expect(result).toBe(null);
      expect(mockMetadataCache.getFileCache).toHaveBeenCalledWith(mockFile);
    });

    it('should return the H1 when there is only one', () => {
      const mockFile = new TFile();
      mockMetadataCache.getFileCache
        .calledWith(mockFile)
        .mockReturnValueOnce({ headings: [mockH1, mockH2] });

      const result = sut.getFirstH1(mockFile);

      expect(mockMetadataCache.getFileCache).toHaveBeenCalledWith(mockFile);
      expect(result).toBe(mockH1);
    });

    it('should return the first H1 when there is more than one regardless of position in headings list', () => {
      const mockFile = new TFile();
      const mockH1Mid = mock<HeadingCache>({
        level: 1,
        position: mock<Pos>({
          start: mock<Loc>({ line: 7 }),
        }),
      });

      const mockH1Last = mock<HeadingCache>({
        level: 1,
        position: mock<Pos>({
          start: mock<Loc>({ line: 15 }),
        }),
      });

      mockMetadataCache.getFileCache
        .calledWith(mockFile)
        .mockReturnValueOnce({ headings: [mockH2, mockH1Mid, mockH1, mockH1Last] });

      const result = sut.getFirstH1(mockFile);

      expect(mockMetadataCache.getFileCache).toHaveBeenCalledWith(mockFile);
      expect(result).toBe(mockH1);
    });

    it('should return the first H1 even when it appears after other lower level headings', () => {
      const mockFile = new TFile();
      const mockH3First = mock<HeadingCache>({
        level: 3,
        position: mock<Pos>({
          start: mock<Loc>({ line: 1 }),
        }),
      });

      mockMetadataCache.getFileCache
        .calledWith(mockFile)
        .mockReturnValueOnce({ headings: [mockH1, mockH2, mockH3First] });

      const result = sut.getFirstH1(mockFile);

      expect(mockMetadataCache.getFileCache).toHaveBeenCalledWith(mockFile);
      expect(result).toBe(mockH1);
    });
  });

  describe('shouldCreateNewLeaf', () => {
    let mockPlatform: MockProxy<typeof Platform>;

    beforeAll(() => {
      mockPlatform = jest.mocked<typeof Platform>(Platform);
    });

    test('with onOpenPreferNewPane enabled it should return true', () => {
      const isModDown = false;
      mockSettings.onOpenPreferNewPane = true;

      const result = sut.shouldCreateNewLeaf(isModDown);

      expect(result).toBe(true);

      mockReset(mockSettings);
    });

    test('with isAlreadyOpen enabled it should return false', () => {
      const isModDown = false;
      const isAlreadyOpen = true;
      mockSettings.onOpenPreferNewPane = true;

      const result = sut.shouldCreateNewLeaf(isModDown, isAlreadyOpen);

      expect(result).toBe(false);

      mockReset(mockSettings);
    });

    test('with isModDown enabled it should return true', () => {
      const isModDown = true;
      mockSettings.onOpenPreferNewPane = false;

      const result = sut.shouldCreateNewLeaf(isModDown);

      expect(result).toBe(true);

      mockReset(mockSettings);
    });

    test('with isModDown, and isAlreadyOpen enabled it should return true', () => {
      const isModDown = true;
      const isAlreadyOpen = true;
      mockSettings.onOpenPreferNewPane = false;

      const result = sut.shouldCreateNewLeaf(isModDown, isAlreadyOpen);

      expect(result).toBe(true);

      mockReset(mockSettings);
    });

    test('with onOpenPreferNewPane and isModDown enabled it should return true', () => {
      const isModDown = true;
      mockSettings.onOpenPreferNewPane = true;

      const result = sut.shouldCreateNewLeaf(isModDown);

      expect(result).toBe(true);

      mockReset(mockSettings);
    });

    test('with onOpenPreferNewPane, isModDown, isAlreadyOpen enabled it should return true', () => {
      const isModDown = true;
      const isAlreadyOpen = true;
      mockSettings.onOpenPreferNewPane = true;

      const result = sut.shouldCreateNewLeaf(isModDown, isAlreadyOpen);

      expect(result).toBe(true);

      mockReset(mockSettings);
    });

    test('with onOpenPreferNewPane enabled, and in Symbol mode, it should return true. This overrides all symbol mode new pane settings', () => {
      const isModDown = false;
      const isAlreadyOpen = false;
      mockSettings.onOpenPreferNewPane = true;

      const result = sut.shouldCreateNewLeaf(isModDown, isAlreadyOpen, Mode.SymbolList);

      expect(result).toBe(true);

      mockReset(mockSettings);
    });

    test('with onOpenPreferNewPane and isModDown enabled, and in Symbol mode, it should return true. This overrides all symbol mode new pane settings', () => {
      const isModDown = true;
      const isAlreadyOpen = false;
      mockSettings.onOpenPreferNewPane = true;

      const result = sut.shouldCreateNewLeaf(isModDown, isAlreadyOpen, Mode.SymbolList);

      expect(result).toBe(true);

      mockReset(mockSettings);
    });

    test('with onOpenPreferNewPane, isModDown, isAlreadyOpen enabled, and in Symbol mode, it should return true. This overrides all symbol mode new pane settings', () => {
      const isModDown = true;
      const isAlreadyOpen = true;
      mockSettings.onOpenPreferNewPane = true;

      const result = sut.shouldCreateNewLeaf(isModDown, isAlreadyOpen, Mode.SymbolList);

      expect(result).toBe(true);

      mockReset(mockSettings);
    });

    test('with alwaysNewPaneForSymbols enabled, and in Symbol mode, it should return true.', () => {
      const isModDown = false;
      const isAlreadyOpen = false;
      mockSettings.onOpenPreferNewPane = false;
      mockSettings.alwaysNewPaneForSymbols = true;

      const result = sut.shouldCreateNewLeaf(isModDown, isAlreadyOpen, Mode.SymbolList);

      expect(result).toBe(true);

      mockReset(mockSettings);
    });

    test('with isModDown enabled, and in Symbol mode, it should return true.', () => {
      const isModDown = true;
      const isAlreadyOpen = true;
      mockSettings.onOpenPreferNewPane = false;
      mockSettings.alwaysNewPaneForSymbols = false;

      const result = sut.shouldCreateNewLeaf(isModDown, isAlreadyOpen, Mode.SymbolList);

      expect(result).toBe(true);

      mockReset(mockSettings);
    });

    test('with useActivePaneForSymbolsOnMobile enabled, and in Symbol mode, it should return false.', () => {
      const isModDown = false;
      const isAlreadyOpen = true;
      mockSettings.onOpenPreferNewPane = false;
      mockSettings.alwaysNewPaneForSymbols = true;
      mockSettings.useActivePaneForSymbolsOnMobile = true;
      mockPlatform.isMobile = true;

      const result = sut.shouldCreateNewLeaf(isModDown, isAlreadyOpen, Mode.SymbolList);

      expect(result).toBe(false);

      mockReset(mockSettings);
    });

    test('with useActivePaneForSymbolsOnMobile disabled, and in Symbol mode, it should return true.', () => {
      const isModDown = false;
      const isAlreadyOpen = false;
      mockSettings.onOpenPreferNewPane = false;
      mockSettings.alwaysNewPaneForSymbols = true;
      mockSettings.useActivePaneForSymbolsOnMobile = false;
      mockPlatform.isMobile = true;

      const result = sut.shouldCreateNewLeaf(isModDown, isAlreadyOpen, Mode.SymbolList);

      expect(result).toBe(true);

      mockReset(mockSettings);
    });
  });

  describe('isMainPanelLeaf', () => {
    const mockLeaf = makeLeaf();

    it('should return true for main panel leaf', () => {
      mockLeaf.getRoot.mockReturnValueOnce(mockWorkspace.rootSplit);

      const result = sut.isMainPanelLeaf(mockLeaf);

      expect(result).toBe(true);
      expect(mockLeaf.getRoot).toHaveBeenCalled();
    });

    it('should return false for side panel leaf', () => {
      mockLeaf.getRoot.mockReturnValueOnce(mockWorkspace.leftSplit);

      const result = sut.isMainPanelLeaf(mockLeaf);

      expect(result).toBe(false);
      expect(mockLeaf.getRoot).toHaveBeenCalled();
    });
  });

  describe('activateLeaf', () => {
    const mockLeaf = makeLeaf();
    const mockView = mockLeaf.view as MockProxy<View>;

    it('should activate main panel leaf', () => {
      mockLeaf.getRoot.mockReturnValueOnce(mockWorkspace.rootSplit);

      sut.activateLeaf(mockLeaf, true);

      expect(mockLeaf.getRoot).toHaveBeenCalled();
      expect(mockWorkspace.setActiveLeaf).toHaveBeenCalledWith(mockLeaf, true);
      expect(mockView.setEphemeralState).toHaveBeenCalled();
    });

    it('should activate side panel leaf', () => {
      mockLeaf.getRoot.mockReturnValueOnce(mockWorkspace.rightSplit);

      sut.activateLeaf(mockLeaf, true);

      expect(mockLeaf.getRoot).toHaveBeenCalled();
      expect(mockWorkspace.setActiveLeaf).toHaveBeenCalledWith(mockLeaf, true);
      expect(mockView.setEphemeralState).toHaveBeenCalled();
      expect(mockWorkspace.revealLeaf).toHaveBeenCalledWith(mockLeaf);
    });
  });

  describe('getOpenLeaves', () => {
    it('should return all leaves', () => {
      const excludeMainViewTypes = ['exclude'];
      const includeSideViewTypes = ['include'];

      const l1 = makeLeaf();
      l1.getRoot.mockReturnValue(mockWorkspace.rootSplit);

      const l2 = makeLeaf();
      l2.getRoot.mockReturnValue(mockWorkspace.rootSplit);
      (l2.view as MockProxy<View>).getViewType.mockReturnValue(excludeMainViewTypes[0]);

      const l3 = makeLeaf();
      l3.getRoot.mockReturnValue(mockWorkspace.rightSplit);
      (l3.view as MockProxy<View>).getViewType.mockReturnValue(includeSideViewTypes[0]);

      mockWorkspace.iterateAllLeaves.mockImplementation((callback) => {
        const leaves = [l1, l2, l3];
        // eslint-disable-next-line @typescript-eslint/no-unsafe-return
        leaves.forEach((l) => callback(l));
      });

      const results = sut.getOpenLeaves(excludeMainViewTypes, includeSideViewTypes);

      expect(results).toHaveLength(2);
      expect(results).toContain(l1);
      expect(results).not.toContain(l2);
      expect(results).toContain(l3);
      expect(mockWorkspace.iterateAllLeaves).toHaveBeenCalled();
    });
  });

  describe('openFileInLeaf', () => {
    it('should log a message to the console if falsy values are passed in', () => {
      let logWasCalled = false;
      const consoleLogSpy = jest
        .spyOn(console, 'log')
        .mockImplementation((message: string) => {
          if (message.startsWith('Switcher++: error opening file. ')) {
            logWasCalled = true;
          }
        });

      sut.openFileInLeaf(null, false);

      expect(logWasCalled).toBe(true);

      consoleLogSpy.mockRestore();
    });

    it('should load a file in a leaf', () => {
      const mockLeaf = makeLeaf();
      const mockFile = new TFile();
      const shouldCreateNewLeaf = false;
      const openState = { active: true };

      mockLeaf.openFile.mockResolvedValueOnce();
      mockWorkspace.getLeaf.mockReturnValueOnce(mockLeaf);

      sut.openFileInLeaf(
        mockFile,
        shouldCreateNewLeaf,
        openState,
        'panelUtils unit test.',
      );

      expect(mockWorkspace.getLeaf).toHaveBeenCalledWith(shouldCreateNewLeaf);
      expect(mockLeaf.openFile).toHaveBeenCalledWith(mockFile, openState);
    });

    it('should log a message to the console if openFile fails', () => {
      const mockLeaf = makeLeaf();
      const mockFile = new TFile();
      const openState = { active: true };

      // Promise used to trigger the error condition
      const openFilePromise = Promise.resolve();

      mockWorkspace.getLeaf.mockReturnValueOnce(mockLeaf);

      mockLeaf.openFile.mockImplementationOnce((_file, _openState) => {
        // throw to simulate openFile() failing
        return openFilePromise.then(() => {
          throw new Error('openFile() unit test mock error');
        });
      });

      // Promise used to track the call to console.log
      let consoleLogPromiseResolveFn: (value: void | PromiseLike<void>) => void;
      const consoleLogPromise = new Promise<void>((resolve, _reject) => {
        consoleLogPromiseResolveFn = resolve;
      });

      const consoleLogSpy = jest
        .spyOn(console, 'log')
        .mockImplementation((message: string) => {
          if (message.startsWith('Switcher++: error opening file. ')) {
            // resolve the consoleLogPromise. This allows allPromises to resolve itself
            consoleLogPromiseResolveFn();
          }
        });

      // wait for the other promises to resolve before this promise can resolve
      const allPromises = Promise.all([openFilePromise, consoleLogPromise]);

      sut.openFileInLeaf(mockFile, false, openState);

      // when all the promises are resolved check expectations and clean up
      return allPromises.finally(() => {
        expect(mockLeaf.openFile).toHaveBeenCalledWith(mockFile, openState);
        expect(consoleLogSpy).toHaveBeenCalled();

        consoleLogSpy.mockRestore();
      });
    });
  });

  describe('navigateToLeafOrOpenFile', () => {
    let openFileInLeafSpy: jest.SpyInstance;
    let activateLeafSpy: jest.SpyInstance;

    beforeAll(() => {
      openFileInLeafSpy = jest.spyOn(Handler.prototype, 'openFileInLeaf');
      activateLeafSpy = jest.spyOn(Handler.prototype, 'activateLeaf');
    });

    beforeEach(() => {
      openFileInLeafSpy.mockReset();
      activateLeafSpy.mockReset();
    });

    afterAll(() => {
      openFileInLeafSpy.mockRestore();
      activateLeafSpy.mockRestore();
    });

    it('should open the file', () => {
      const file = new TFile();
      const isModDown = false;
      const errorContext = chance.sentence();

      sut.navigateToLeafOrOpenFile(isModDown, file, errorContext);

      expect(openFileInLeafSpy).toHaveBeenCalledWith(
        file,
        false,
        defaultOpenViewState,
        errorContext,
      );
    });

    it('should open the file in a new leaf with isModDown enabled', () => {
      const file = new TFile();
      const isModDown = true;
      const errorContext = chance.sentence();

      sut.navigateToLeafOrOpenFile(isModDown, file, errorContext);

      expect(openFileInLeafSpy).toHaveBeenCalledWith(
        file,
        true,
        defaultOpenViewState,
        errorContext,
      );
    });

    test('with existing leaf and isModDown disabled, it should activate the existing leaf', () => {
      const file = new TFile();
      const isModDown = false;
      const leaf = makeLeaf();

      sut.navigateToLeafOrOpenFile(isModDown, file, null, null, leaf);

      expect(openFileInLeafSpy).not.toHaveBeenCalled();
      expect(activateLeafSpy).toHaveBeenCalledWith(
        leaf,
        true,
        defaultOpenViewState.eState,
      );
    });

    test('with existing leaf and isModDown enabled, it should create a new leaf', () => {
      const file = new TFile();
      const isModDown = true;
      const leaf = makeLeaf();
      const errorContext = chance.sentence();

      sut.navigateToLeafOrOpenFile(isModDown, file, errorContext, null, leaf);

      expect(activateLeafSpy).not.toHaveBeenCalled();
      expect(openFileInLeafSpy).toBeCalledWith(
        file,
        isModDown,
        defaultOpenViewState,
        errorContext,
      );
    });

    it('should use the default OpenViewState when a falsy value is passed in for opening files', () => {
      const file = new TFile();
      const isModDown = false;

      sut.navigateToLeafOrOpenFile(isModDown, file, null);

      expect(openFileInLeafSpy).toHaveBeenCalledWith(
        file,
        isModDown,
        defaultOpenViewState,
        null,
      );
    });
  });

  describe('findOpenEditor', () => {
    it.todo('should match a file in the active editor');
    it.todo('should match a file in an in-active editor');
    it.todo('should match using a reference WorkspaceLeaf as a source');
    it.todo('should not match any reference view types');
  });
});
Example #18
Source File: path.ts    From obsidian-fantasy-calendar with MIT License 4 votes vote down vote up
export default class PathSuggestionModal extends SuggestionModal<
    TFile | BlockCache | HeadingCache
> {
    file: TFile;
    files: TFile[];
    text: TextComponent;
    cache: CachedMetadata;
    constructor(app: App, input: TextComponent, items: TFile[]) {
        super(app, input.inputEl, items);
        this.files = [...items];
        this.text = input;

        this.createPrompts();

        this.inputEl.addEventListener("input", this.getFile.bind(this));
    }
    createPrompts() {
        this.createPrompt([
            createSpan({
                cls: "prompt-instruction-command",
                text: "Type #"
            }),
            createSpan({ text: "to link heading" })
        ]);
        this.createPrompt([
            createSpan({
                cls: "prompt-instruction-command",
                text: "Type ^"
            }),
            createSpan({ text: "to link blocks" })
        ]);
        this.createPrompt([
            createSpan({
                cls: "prompt-instruction-command",
                text: "Note: "
            }),
            createSpan({
                text: "Blocks must have been created already"
            })
        ]);
    }
    getFile() {
        const v = this.inputEl.value,
            file = this.app.metadataCache.getFirstLinkpathDest(
                v.split(/[\^#]/).shift() || "",
                ""
            );
        if (file == this.file) return;
        this.file = file;
        if (this.file)
            this.cache = this.app.metadataCache.getFileCache(this.file);
        this.onInputChanged();
    }
    getItemText(item: TFile | HeadingCache | BlockCache) {
        if (item instanceof TFile) return item.path;
        if (Object.prototype.hasOwnProperty.call(item, "heading")) {
            return (<HeadingCache>item).heading;
        }
        if (Object.prototype.hasOwnProperty.call(item, "id")) {
            return (<BlockCache>item).id;
        }
    }
    onChooseItem(item: TFile | HeadingCache | BlockCache) {
        if (item instanceof TFile) {
            this.text.setValue(item.basename);
            this.file = item;
            this.cache = this.app.metadataCache.getFileCache(this.file);
        } else if (Object.prototype.hasOwnProperty.call(item, "heading")) {
            this.text.setValue(
                this.file.basename + "#" + (<HeadingCache>item).heading
            );
        } else if (Object.prototype.hasOwnProperty.call(item, "id")) {
            this.text.setValue(
                this.file.basename + "^" + (<BlockCache>item).id
            );
        }
    }
    link: string;
    selectSuggestion({ item }: FuzzyMatch<TFile | BlockCache | HeadingCache>) {
        let link: string;
        if (item instanceof TFile) {
            this.file = item;
            link = item.basename;
        } else if (Object.prototype.hasOwnProperty.call(item, "heading")) {
            link = this.file.basename + "#" + (<HeadingCache>item).heading;
        } else if (Object.prototype.hasOwnProperty.call(item, "id")) {
            link = this.file.basename + "^" + (<BlockCache>item).id;
        }
        const path = this.file.path.split("/").slice(0, -1);
        if (path.length) {
            this.link = path.join("/") + "/" + link;
        } else {
            this.link = link;
        }
        this.text.setValue(link);

        this.close();
        this.onClose();
    }
    renderSuggestion(
        result: FuzzyMatch<TFile | BlockCache | HeadingCache>,
        el: HTMLElement
    ) {
        let { item, match: matches } = result || {};
        let content = el.createDiv({
            cls: "suggestion-content"
        });
        if (!item) {
            content.setText(this.emptyStateText);
            content.parentElement.addClass("is-selected");
            return;
        }

        if (item instanceof TFile) {
            let pathLength = item.path.length - item.name.length;
            const matchElements = matches.matches.map((m) => {
                return createSpan("suggestion-highlight");
            });
            for (
                let i = pathLength;
                i < item.path.length - item.extension.length - 1;
                i++
            ) {
                let match = matches.matches.find((m) => m[0] === i);
                if (match) {
                    let element = matchElements[matches.matches.indexOf(match)];
                    content.appendChild(element);
                    element.appendText(item.path.substring(match[0], match[1]));

                    i += match[1] - match[0] - 1;
                    continue;
                }

                content.appendText(item.path[i]);
            }
            el.createDiv({
                cls: "suggestion-note",
                text: item.path
            });
        } else if (Object.prototype.hasOwnProperty.call(item, "heading")) {
            content.setText((<HeadingCache>item).heading);
            content.prepend(
                createSpan({
                    cls: "suggestion-flair",
                    text: `H${(<HeadingCache>item).level}`
                })
            );
        } else if (Object.prototype.hasOwnProperty.call(item, "id")) {
            content.setText((<BlockCache>item).id);
        }
    }
    get headings() {
        if (!this.file) return [];
        if (!this.cache) {
            this.cache = this.app.metadataCache.getFileCache(this.file);
        }
        return this.cache.headings || [];
    }
    get blocks() {
        if (!this.file) return [];
        if (!this.cache) {
            this.cache = this.app.metadataCache.getFileCache(this.file);
        }
        return Object.values(this.cache.blocks || {}) || [];
    }
    getItems() {
        const v = this.inputEl.value;
        if (/#/.test(v)) {
            this.modifyInput = (i) => i.split(/#/).pop();
            return this.headings;
        } else if (/\^/.test(v)) {
            this.modifyInput = (i) => i.split(/\^/).pop();
            return this.blocks;
        }
        return this.files;
    }
}
Example #19
Source File: main.ts    From obsidian-spaced-repetition with MIT License 4 votes vote down vote up
async findFlashcardsInNote(
        note: TFile,
        deckPath: string[],
        buryOnly = false,
        ignoreStats = false
    ): Promise<number> {
        let fileText: string = await this.app.vault.read(note);
        const fileCachedData = this.app.metadataCache.getFileCache(note) || {};
        const headings: HeadingCache[] = fileCachedData.headings || [];
        let fileChanged = false,
            totalNoteEase = 0,
            scheduledCount = 0;
        const settings: SRSettings = this.data.settings;
        const noteDeckPath = deckPath;

        const now: number = Date.now();
        const parsedCards: [CardType, string, number][] = parse(
            fileText,
            settings.singlelineCardSeparator,
            settings.singlelineReversedCardSeparator,
            settings.multilineCardSeparator,
            settings.multilineReversedCardSeparator,
            settings.convertHighlightsToClozes,
            settings.convertBoldTextToClozes
        );
        for (const parsedCard of parsedCards) {
            deckPath = noteDeckPath;
            const cardType: CardType = parsedCard[0],
                lineNo: number = parsedCard[2];
            let cardText: string = parsedCard[1];

            if (!settings.convertFoldersToDecks) {
                const tagInCardRegEx = /^#[^\s#]+/gi;
                const cardDeckPath = cardText
                    .match(tagInCardRegEx)
                    ?.slice(-1)[0]
                    .replace("#", "")
                    .split("/");
                if (cardDeckPath) {
                    deckPath = cardDeckPath;
                    cardText = cardText.replaceAll(tagInCardRegEx, "");
                }
            }

            this.deckTree.createDeck([...deckPath]);

            const cardTextHash: string = cyrb53(cardText);

            if (buryOnly) {
                this.data.buryList.push(cardTextHash);
                continue;
            }

            const siblingMatches: [string, string][] = [];
            if (cardType === CardType.Cloze) {
                const siblings: RegExpMatchArray[] = [];
                if (settings.convertHighlightsToClozes) {
                    siblings.push(...cardText.matchAll(/==(.*?)==/gm));
                }
                if (settings.convertBoldTextToClozes) {
                    siblings.push(...cardText.matchAll(/\*\*(.*?)\*\*/gm));
                }
                siblings.sort((a, b) => {
                    if (a.index < b.index) {
                        return -1;
                    }
                    if (a.index > b.index) {
                        return 1;
                    }
                    return 0;
                });

                let front: string, back: string;
                for (const m of siblings) {
                    const deletionStart: number = m.index,
                        deletionEnd: number = deletionStart + m[0].length;
                    front =
                        cardText.substring(0, deletionStart) +
                        "<span style='color:#2196f3'>[...]</span>" +
                        cardText.substring(deletionEnd);
                    front = front.replace(/==/gm, "").replace(/\*\*/gm, "");
                    back =
                        cardText.substring(0, deletionStart) +
                        "<span style='color:#2196f3'>" +
                        cardText.substring(deletionStart, deletionEnd) +
                        "</span>" +
                        cardText.substring(deletionEnd);
                    back = back.replace(/==/gm, "").replace(/\*\*/gm, "");
                    siblingMatches.push([front, back]);
                }
            } else {
                let idx: number;
                if (cardType === CardType.SingleLineBasic) {
                    idx = cardText.indexOf(settings.singlelineCardSeparator);
                    siblingMatches.push([
                        cardText.substring(0, idx),
                        cardText.substring(idx + settings.singlelineCardSeparator.length),
                    ]);
                } else if (cardType === CardType.SingleLineReversed) {
                    idx = cardText.indexOf(settings.singlelineReversedCardSeparator);
                    const side1: string = cardText.substring(0, idx),
                        side2: string = cardText.substring(
                            idx + settings.singlelineReversedCardSeparator.length
                        );
                    siblingMatches.push([side1, side2]);
                    siblingMatches.push([side2, side1]);
                } else if (cardType === CardType.MultiLineBasic) {
                    idx = cardText.indexOf("\n" + settings.multilineCardSeparator + "\n");
                    siblingMatches.push([
                        cardText.substring(0, idx),
                        cardText.substring(idx + 2 + settings.multilineCardSeparator.length),
                    ]);
                } else if (cardType === CardType.MultiLineReversed) {
                    idx = cardText.indexOf("\n" + settings.multilineReversedCardSeparator + "\n");
                    const side1: string = cardText.substring(0, idx),
                        side2: string = cardText.substring(
                            idx + 2 + settings.multilineReversedCardSeparator.length
                        );
                    siblingMatches.push([side1, side2]);
                    siblingMatches.push([side2, side1]);
                }
            }

            let scheduling: RegExpMatchArray[] = [...cardText.matchAll(MULTI_SCHEDULING_EXTRACTOR)];
            if (scheduling.length === 0)
                scheduling = [...cardText.matchAll(LEGACY_SCHEDULING_EXTRACTOR)];

            // we have some extra scheduling dates to delete
            if (scheduling.length > siblingMatches.length) {
                const idxSched: number = cardText.lastIndexOf("<!--SR:") + 7;
                let newCardText: string = cardText.substring(0, idxSched);
                for (let i = 0; i < siblingMatches.length; i++)
                    newCardText += `!${scheduling[i][1]},${scheduling[i][2]},${scheduling[i][3]}`;
                newCardText += "-->";

                const replacementRegex = new RegExp(escapeRegexString(cardText), "gm");
                fileText = fileText.replace(replacementRegex, () => newCardText);
                fileChanged = true;
            }

            const context: string = settings.showContextInCards
                ? getCardContext(lineNo, headings)
                : "";
            const siblings: Card[] = [];
            for (let i = 0; i < siblingMatches.length; i++) {
                const front: string = siblingMatches[i][0].trim(),
                    back: string = siblingMatches[i][1].trim();

                const cardObj: Card = {
                    isDue: i < scheduling.length,
                    note,
                    lineNo,
                    front,
                    back,
                    cardText,
                    context,
                    cardType,
                    siblingIdx: i,
                    siblings,
                };

                // card scheduled
                if (ignoreStats) {
                    this.cardStats.newCount++;
                    cardObj.isDue = true;
                    this.deckTree.insertFlashcard([...deckPath], cardObj);
                } else if (i < scheduling.length) {
                    const dueUnix: number = window
                        .moment(scheduling[i][1], ["YYYY-MM-DD", "DD-MM-YYYY"])
                        .valueOf();
                    const nDays: number = Math.ceil((dueUnix - now) / (24 * 3600 * 1000));
                    if (!Object.prototype.hasOwnProperty.call(this.dueDatesFlashcards, nDays)) {
                        this.dueDatesFlashcards[nDays] = 0;
                    }
                    this.dueDatesFlashcards[nDays]++;

                    const interval: number = parseInt(scheduling[i][2]),
                        ease: number = parseInt(scheduling[i][3]);
                    if (!Object.prototype.hasOwnProperty.call(this.cardStats.intervals, interval)) {
                        this.cardStats.intervals[interval] = 0;
                    }
                    this.cardStats.intervals[interval]++;
                    if (!Object.prototype.hasOwnProperty.call(this.cardStats.eases, ease)) {
                        this.cardStats.eases[ease] = 0;
                    }
                    this.cardStats.eases[ease]++;
                    totalNoteEase += ease;
                    scheduledCount++;

                    if (interval >= 32) {
                        this.cardStats.matureCount++;
                    } else {
                        this.cardStats.youngCount++;
                    }

                    if (this.data.buryList.includes(cardTextHash)) {
                        this.deckTree.countFlashcard([...deckPath]);
                        continue;
                    }

                    if (dueUnix <= now) {
                        cardObj.interval = interval;
                        cardObj.ease = ease;
                        cardObj.delayBeforeReview = now - dueUnix;
                        this.deckTree.insertFlashcard([...deckPath], cardObj);
                    } else {
                        this.deckTree.countFlashcard([...deckPath]);
                        continue;
                    }
                } else {
                    this.cardStats.newCount++;
                    if (this.data.buryList.includes(cyrb53(cardText))) {
                        this.deckTree.countFlashcard([...deckPath]);
                        continue;
                    }
                    this.deckTree.insertFlashcard([...deckPath], cardObj);
                }

                siblings.push(cardObj);
            }
        }

        if (fileChanged) {
            await this.app.vault.modify(note, fileText);
        }

        if (scheduledCount > 0) {
            const flashcardsInNoteAvgEase: number = totalNoteEase / scheduledCount;
            const flashcardContribution: number = Math.min(
                1.0,
                Math.log(scheduledCount + 0.5) / Math.log(64)
            );
            return (
                flashcardsInNoteAvgEase * flashcardContribution +
                settings.baseEase * (1.0 - flashcardContribution)
            );
        }

        return 0;
    }