obsidian#SearchResult TypeScript Examples

The following examples show how to use obsidian#SearchResult. 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: commandHandler.ts    From obsidian-switcher-plus with GNU General Public License v3.0 6 votes vote down vote up
override getSuggestions(inputInfo: InputInfo): CommandSuggestion[] {
    const suggestions: CommandSuggestion[] = [];

    if (inputInfo) {
      inputInfo.buildSearchQuery();
      const { hasSearchTerm, prepQuery } = inputInfo.searchQuery;
      const itemsInfo = this.getItems();

      itemsInfo.forEach((item) => {
        let shouldPush = true;
        let match: SearchResult = null;

        if (hasSearchTerm) {
          match = fuzzySearch(prepQuery, item.name);
          shouldPush = !!match;
        }

        if (shouldPush) {
          suggestions.push({
            type: 'command',
            item,
            match,
          });
        }
      });

      if (hasSearchTerm) {
        sortSearchResults(suggestions);
      }
    }

    return suggestions;
  }
Example #2
Source File: editorHandler.ts    From obsidian-switcher-plus with GNU General Public License v3.0 6 votes vote down vote up
override getSuggestions(inputInfo: InputInfo): EditorSuggestion[] {
    const suggestions: EditorSuggestion[] = [];

    if (inputInfo) {
      inputInfo.buildSearchQuery();
      const { hasSearchTerm, prepQuery } = inputInfo.searchQuery;
      const { excludeViewTypes, includeSidePanelViewTypes } = this.settings;

      const items = this.getOpenLeaves(excludeViewTypes, includeSidePanelViewTypes);

      items.forEach((item) => {
        let shouldPush = true;
        let match: SearchResult = null;

        if (hasSearchTerm) {
          match = fuzzySearch(prepQuery, item.getDisplayText());
          shouldPush = !!match;
        }

        if (shouldPush) {
          const file = item.view?.file;
          suggestions.push({ type: 'editor', file, item, match });
        }
      });

      if (hasSearchTerm) {
        sortSearchResults(suggestions);
      }
    }

    return suggestions;
  }
Example #3
Source File: headingsHandler.ts    From obsidian-switcher-plus with GNU General Public License v3.0 6 votes vote down vote up
private makeAliasSuggestion(
    alias: string,
    file: TFile,
    match: SearchResult,
  ): AliasSuggestion {
    return {
      alias,
      file,
      match,
      type: 'alias',
    };
  }
Example #4
Source File: headingsHandler.ts    From obsidian-switcher-plus with GNU General Public License v3.0 6 votes vote down vote up
private makeUnresolvedSuggestion(
    linktext: string,
    match: SearchResult,
  ): UnresolvedSuggestion {
    return {
      linktext,
      match,
      type: 'unresolved',
    };
  }
Example #5
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 #6
Source File: headingsHandler.ts    From obsidian-switcher-plus with GNU General Public License v3.0 6 votes vote down vote up
private matchStrings(
    prepQuery: PreparedQuery,
    primaryString: string,
    secondaryString: string,
  ): { isPrimary: boolean; match?: SearchResult } {
    let isPrimary = false;
    let match: SearchResult = null;

    if (primaryString) {
      match = fuzzySearch(prepQuery, primaryString);
      isPrimary = !!match;
    }

    if (!match && secondaryString) {
      match = fuzzySearch(prepQuery, secondaryString);

      if (match) {
        match.score -= 1;
      }
    }

    return {
      isPrimary,
      match,
    };
  }
Example #7
Source File: relatedItemsHandler.ts    From obsidian-switcher-plus with GNU General Public License v3.0 6 votes vote down vote up
override getSuggestions(inputInfo: InputInfo): RelatedItemsSuggestion[] {
    const suggestions: RelatedItemsSuggestion[] = [];

    if (inputInfo) {
      this.inputInfo = inputInfo;
      inputInfo.buildSearchQuery();

      const { hasSearchTerm, prepQuery } = inputInfo.searchQuery;
      const cmd = inputInfo.parsedCommand(Mode.RelatedItemsList) as SourcedParsedCommand;
      const items = this.getRelatedFiles(cmd.source.file);

      items.forEach((item) => {
        let shouldPush = true;
        let match: SearchResult = null;

        if (hasSearchTerm) {
          match = fuzzySearch(prepQuery, this.getTitleText(item));
          shouldPush = !!match;
        }

        if (shouldPush) {
          suggestions.push({
            type: 'relatedItems',
            relationType: 'diskLocation',
            file: item,
            match,
          });
        }
      });

      if (hasSearchTerm) {
        sortSearchResults(suggestions);
      }
    }

    return suggestions;
  }
Example #8
Source File: starredHandler.ts    From obsidian-switcher-plus with GNU General Public License v3.0 6 votes vote down vote up
override getSuggestions(inputInfo: InputInfo): StarredSuggestion[] {
    const suggestions: StarredSuggestion[] = [];

    if (inputInfo) {
      inputInfo.buildSearchQuery();
      const { hasSearchTerm, prepQuery } = inputInfo.searchQuery;
      const itemsInfo = this.getItems();

      itemsInfo.forEach(({ file, item }) => {
        let shouldPush = true;
        let match: SearchResult = null;

        if (hasSearchTerm) {
          match = fuzzySearch(prepQuery, item.title);
          shouldPush = !!match;
        }

        if (shouldPush) {
          suggestions.push({ type: 'starred', file, item, match });
        }
      });

      if (hasSearchTerm) {
        sortSearchResults(suggestions);
      }
    }

    return suggestions;
  }
Example #9
Source File: symbolHandler.ts    From obsidian-switcher-plus with GNU General Public License v3.0 6 votes vote down vote up
override getSuggestions(inputInfo: InputInfo): SymbolSuggestion[] {
    const suggestions: SymbolSuggestion[] = [];

    if (inputInfo) {
      this.inputInfo = inputInfo;

      inputInfo.buildSearchQuery();
      const { hasSearchTerm, prepQuery } = inputInfo.searchQuery;
      const symbolCmd = inputInfo.parsedCommand(Mode.SymbolList) as SourcedParsedCommand;
      const items = this.getItems(symbolCmd.source, hasSearchTerm);

      items.forEach((item) => {
        let shouldPush = true;
        let match: SearchResult = null;

        if (hasSearchTerm) {
          match = fuzzySearch(prepQuery, SymbolHandler.getSuggestionTextForSymbol(item));
          shouldPush = !!match;
        }

        if (shouldPush) {
          const { file } = symbolCmd.source;
          suggestions.push({ type: 'symbol', file, item, match });
        }
      });

      if (hasSearchTerm) {
        sortSearchResults(suggestions);
      }
    }

    return suggestions;
  }
Example #10
Source File: workspaceHandler.ts    From obsidian-switcher-plus with GNU General Public License v3.0 6 votes vote down vote up
override getSuggestions(inputInfo: InputInfo): WorkspaceSuggestion[] {
    const suggestions: WorkspaceSuggestion[] = [];

    if (inputInfo) {
      inputInfo.buildSearchQuery();
      const { hasSearchTerm, prepQuery } = inputInfo.searchQuery;
      const items = this.getItems();

      items.forEach((item) => {
        let shouldPush = true;
        let match: SearchResult = null;

        if (hasSearchTerm) {
          match = fuzzySearch(prepQuery, item.id);
          shouldPush = !!match;
        }

        if (shouldPush) {
          suggestions.push({ type: 'workspace', item, match });
        }
      });

      if (hasSearchTerm) {
        sortSearchResults(suggestions);
      }
    }

    return suggestions;
  }
Example #11
Source File: fixtureUtils.ts    From obsidian-switcher-plus with GNU General Public License v3.0 6 votes vote down vote up
export function makeFuzzyMatch(
  matches: SearchMatches = [[0, 5]],
  score = -0.0115,
): SearchResult {
  return {
    matches,
    score,
  };
}
Example #12
Source File: headingsHandler.ts    From obsidian-switcher-plus with GNU General Public License v3.0 5 votes vote down vote up
private makeFileSuggestion(file: TFile, match: SearchResult): FileSuggestion {
    return {
      file,
      match,
      type: 'file',
    };
  }
Example #13
Source File: relatedItemsHandler.test.ts    From obsidian-switcher-plus with GNU General Public License v3.0 4 votes vote down vote up
describe('relatedItemsHandler', () => {
  const rootFixture = rootSplitEditorFixtures[0];
  let settings: SwitcherPlusSettings;
  let mockApp: MockProxy<App>;
  let mockWorkspace: MockProxy<Workspace>;
  let sut: RelatedItemsHandler;
  let mockMetadataCache: MockProxy<MetadataCache>;
  let mockRootSplitLeaf: MockProxy<WorkspaceLeaf>;
  let filterText: string;

  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, 'relatedItemsListCommand', 'get')
      .mockReturnValue(relatedItemsTrigger);

    const rootSplitSourceFile = new TFile();
    rootSplitSourceFile.parent = makeFileTree(rootSplitSourceFile);

    mockRootSplitLeaf = makeLeaf();
    mockRootSplitLeaf.view.file = rootSplitSourceFile;
  });

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

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

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

    it('should validate parsed input in prefix (active editor) mode', () => {
      const inputInfo = new InputInfo(`${relatedItemsTrigger}${filterText}`);

      sut.validateCommand(inputInfo, 0, filterText, null, mockRootSplitLeaf);

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

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

    it('should validate parsed input for file based suggestion', () => {
      const targetFile = new TFile();
      const inputInfo = new InputInfo('', Mode.Standard);
      const sugg: AliasSuggestion = {
        file: targetFile,
        alias: 'foo',
        type: 'alias',
        match: null,
      };

      sut.validateCommand(inputInfo, 0, '', sugg, null);

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

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

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

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

      sut.validateCommand(inputInfo, 0, '', sugg, null);

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

      const cmd = inputInfo.parsedCommand() as SourcedParsedCommand;
      expect(cmd.isValidated).toBe(true);
      expect(cmd.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 inputInfo = new InputInfo('', Mode.StarredList);
      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);

      sut.validateCommand(inputInfo, 0, '', sugg, null);

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

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

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

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

      sut.validateCommand(inputInfo, 0, '', sugg, null);

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

      const cmd = inputInfo.parsedCommand() as SourcedParsedCommand;
      expect(cmd.isValidated).toBe(true);
      expect(cmd.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 inputInfo = new InputInfo('', Mode.Standard);
      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
      });

      sut.validateCommand(inputInfo, 0, '', sugg, null);

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

      const cmd = inputInfo.parsedCommand() as SourcedParsedCommand;
      expect(cmd.isValidated).toBe(true);
      expect(cmd.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 RelatedItemsSuggestion have a file property to enable interop with other plugins (like HoverEditor)', () => {
      const inputInfo = new InputInfo(relatedItemsTrigger);
      sut.validateCommand(inputInfo, 0, '', null, mockRootSplitLeaf);

      const results = sut.getSuggestions(inputInfo);

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

    test('with default settings, it should return suggestions', () => {
      const inputInfo = new InputInfo(relatedItemsTrigger);
      sut.validateCommand(inputInfo, 0, '', null, mockRootSplitLeaf);

      const results = sut.getSuggestions(inputInfo);

      expect(inputInfo.mode).toBe(Mode.RelatedItemsList);
      expect(results).toBeInstanceOf(Array);
      expect(results).toHaveLength(2);
      expect(results.every((sugg) => sugg.type === 'relatedItems')).toBe(true);
      expect(results.every((sugg) => sugg.relationType === 'diskLocation')).toBe(true);

      const files = results.map((v) => v.file);
      expect(files).toEqual(expect.arrayContaining([file1, file2]));

      expect(mockPrepareQuery).toHaveBeenCalled();
    });

    test('with filter search term, it should return only matching symbol suggestions', () => {
      filterText = file1.basename;
      mockPrepareQuery.mockReturnValueOnce(makePreparedQuery(filterText));
      mockFuzzySearch.mockImplementation(
        (_q: PreparedQuery, text: string): SearchResult => {
          const match = makeFuzzyMatch();
          return text.includes(filterText) ? match : null;
        },
      );

      const inputInfo = new InputInfo(`${relatedItemsTrigger}${filterText}`);
      sut.validateCommand(inputInfo, 0, filterText, null, mockRootSplitLeaf);

      const results = sut.getSuggestions(inputInfo);

      expect(inputInfo.mode).toBe(Mode.RelatedItemsList);
      expect(results).toBeInstanceOf(Array);
      expect(results).toHaveLength(1);
      expect(results.every((sugg) => sugg.type === 'relatedItems')).toBe(true);
      expect(results.every((sugg) => sugg.relationType === 'diskLocation')).toBe(true);

      expect(mockPrepareQuery).toHaveBeenCalled();
      expect(mockFuzzySearch).toHaveBeenCalled();

      mockFuzzySearch.mockReset();
    });

    test('with existing filter search term, it should continue refining suggestions for the previous target', () => {
      // 1) setup first initial run
      filterText = file1.basename.slice(0, file1.basename.length / 2);
      mockPrepareQuery.mockReturnValueOnce(makePreparedQuery(filterText));

      mockFuzzySearch.mockImplementation((_q: PreparedQuery, text: string) => {
        const match = makeFuzzyMatch();
        return text.includes(filterText) ? match : null;
      });

      let inputInfo = new InputInfo(`${relatedItemsTrigger}${filterText}`);

      sut.validateCommand(inputInfo, 0, filterText, null, mockRootSplitLeaf);

      let results = sut.getSuggestions(inputInfo);

      expect(inputInfo.mode).toBe(Mode.RelatedItemsList);
      expect(results).toBeInstanceOf(Array);
      expect(results.every((sugg) => sugg.type === 'relatedItems')).toBe(true);
      expect(results.every((sugg) => sugg.relationType === 'diskLocation')).toBe(true);

      let cmd = inputInfo.parsedCommand() as SourcedParsedCommand;
      expect(cmd.source.file).toBe(mockRootSplitLeaf.view.file);
      mockFuzzySearch.mockReset();

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

      mockFuzzySearch.mockImplementation((q: PreparedQuery, text: string) => {
        const match = makeFuzzyMatch();
        return text.endsWith(q.query) ? match : null;
      });

      const mockTempLeaf = makeLeaf();
      inputInfo = new InputInfo(`${relatedItemsTrigger}${filterText}`);

      // note the use of a different leaf than the first run, because it should use the
      // leaf from the previous run
      sut.validateCommand(inputInfo, 0, filterText, null, mockTempLeaf);

      results = sut.getSuggestions(inputInfo);

      expect(inputInfo.mode).toBe(Mode.RelatedItemsList);
      expect(results).toBeInstanceOf(Array);
      expect(results).toHaveLength(1);
      expect(results[0].file).toEqual(file1);
      expect(results.every((sugg) => sugg.type === 'relatedItems')).toBe(true);
      expect(results.every((sugg) => sugg.relationType === 'diskLocation')).toBe(true);
      expect(mockPrepareQuery).toHaveBeenCalled();

      cmd = inputInfo.parsedCommand() as SourcedParsedCommand;
      expect(cmd.source.file).not.toBe(mockTempLeaf.view.file);

      // expect the source file to be the same as the first run
      expect(cmd.source.file).toBe(mockRootSplitLeaf.view.file);
      mockFuzzySearch.mockReset();
    });
  });

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

    it('should render a suggestion with match offsets', () => {
      const mockParentEl = mock<HTMLElement>();
      const mockRenderResults = jest.mocked<typeof renderResults>(renderResults);

      const match = makeFuzzyMatch();
      const sugg = mock<RelatedItemsSuggestion>({ file: file1, match });

      sut.renderSuggestion(sugg, mockParentEl);

      expect(mockRenderResults).toHaveBeenCalledWith(
        mockParentEl,
        stripMDExtensionFromPath(file1),
        match,
      );
    });
  });

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

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

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

    test('with Mod down, it should it should create a new workspaceLeaf for the target file', () => {
      const isModDown = true;
      const navigateToLeafOrOpenFileSpy = jest.spyOn(
        Handler.prototype,
        'navigateToLeafOrOpenFile',
      );

      mockKeymap.isModEvent.mockReturnValueOnce(isModDown);

      const sugg: RelatedItemsSuggestion = {
        type: 'relatedItems',
        relationType: 'diskLocation',
        file: file1,
        match: null,
      };

      sut.onChooseSuggestion(sugg, null);

      expect(mockKeymap.isModEvent).toHaveBeenCalled();
      expect(navigateToLeafOrOpenFileSpy).toHaveBeenCalledWith(
        isModDown,
        sugg.file,
        expect.any(String),
      );

      navigateToLeafOrOpenFileSpy.mockRestore();
    });
  });

  describe('getRelatedFiles', () => {
    test('with excludeRelatedFolders unset, it should include files from subfolders', () => {
      const sourceFile = new TFile();
      sourceFile.parent = makeFileTree(sourceFile);

      // don't set any folder filter
      jest.spyOn(settings, 'excludeRelatedFolders', 'get').mockReturnValueOnce([]);

      const results = sut.getRelatedFiles(sourceFile);

      expect(results).toHaveLength(4);
      expect(results).toEqual(expect.arrayContaining([file1, file2, file3, file4]));
    });

    it('should exclude files from subfolders', () => {
      const sourceFile = new TFile();
      sourceFile.parent = makeFileTree(sourceFile);

      const results = sut.getRelatedFiles(sourceFile);

      expect(results).toHaveLength(2);
      expect(results).toEqual(expect.arrayContaining([file1, file2]));
    });

    it('should include files that are already open in an editor', () => {
      const findOpenEditorSpy = jest.spyOn(sut, 'findOpenEditor');
      const sourceFile = new TFile();
      sourceFile.parent = makeFileTree(sourceFile);

      // set file1 as the active leaf
      mockWorkspace.activeLeaf = makeLeaf();

      // set file1 as the file for active leaf
      mockWorkspace.activeLeaf.view.file = file1;

      const results = sut.getRelatedFiles(sourceFile);

      expect(results).toHaveLength(2);
      expect(results).toEqual(expect.arrayContaining([file1, file2]));
      expect(findOpenEditorSpy).not.toHaveBeenCalled();

      findOpenEditorSpy.mockRestore();
    });

    test('with excludeOpenRelatedFiles enabled, it should exclude files that are already open in an editor', () => {
      const sourceFile = new TFile();
      sourceFile.parent = makeFileTree(sourceFile);

      // exclude files already open
      jest.spyOn(settings, 'excludeOpenRelatedFiles', 'get').mockReturnValueOnce(true);

      mockWorkspace.activeLeaf = makeLeaf();

      // set file1 as the file for active leaf
      mockWorkspace.activeLeaf.view.file = file1;

      const results = sut.getRelatedFiles(sourceFile);

      expect(results).toHaveLength(1);
      expect(results).toEqual(expect.arrayContaining([file2]));
    });
  });
});
Example #14
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();
    });
  });
});