obsidian#EditorPosition TypeScript Examples

The following examples show how to use obsidian#EditorPosition. 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: utils.ts    From obsidian-editor-shortcuts with MIT License 7 votes vote down vote up
wordRangeAtPos = (
  pos: EditorPosition,
  lineContent: string,
): { anchor: EditorPosition; head: EditorPosition } => {
  let start = pos.ch;
  let end = pos.ch;
  while (start > 0 && isLetterCharacter(lineContent.charAt(start - 1))) {
    start--;
  }
  while (
    end < lineContent.length &&
    isLetterCharacter(lineContent.charAt(end))
  ) {
    end++;
  }
  return {
    anchor: {
      line: pos.line,
      ch: start,
    },
    head: {
      line: pos.line,
      ch: end,
    },
  };
}
Example #2
Source File: utils.ts    From nldates-obsidian with MIT License 7 votes vote down vote up
export function adjustCursor(
  editor: Editor,
  cursor: EditorPosition,
  newStr: string,
  oldStr: string
): void {
  const cursorOffset = newStr.length - oldStr.length;
  editor.setCursor({
    line: cursor.line,
    ch: cursor.ch + cursorOffset,
  });
}
Example #3
Source File: locationSuggest.ts    From obsidian-map-view with GNU General Public License v3.0 6 votes vote down vote up
onTrigger(
        cursor: EditorPosition,
        editor: Editor,
        file: TFile
    ): EditorSuggestTriggerInfo | null {
        const currentLink = this.getGeolinkOfCursor(cursor, editor);
        if (currentLink)
            return {
                start: { line: cursor.line, ch: currentLink.index },
                end: { line: cursor.line, ch: currentLink.linkEnd },
                query: currentLink.name,
            };
        return null;
    }
Example #4
Source File: suggest.ts    From obsidian-admonition with MIT License 6 votes vote down vote up
onTrigger(
        cursor: EditorPosition,
        editor: Editor,
        file: TFile
    ): EditorSuggestTriggerInfo {
        const line = editor.getLine(cursor.line);
        if (!/```ad-\w+/.test(line)) return null;
        const match = line.match(/```ad-(\w+)/);
        if (!match) return null;
        const [_, query] = match;

        if (
            !query ||
            Object.keys(this.plugin.admonitions).find(
                (p) => p.toLowerCase() == query.toLowerCase()
            )
        ) {
            return null;
        }

        const matchData = {
            end: cursor,
            start: {
                ch: match.index + 6,
                line: cursor.line
            },
            query
        };
        return matchData;
    }
Example #5
Source File: suggest.ts    From obsidian-admonition with MIT License 6 votes vote down vote up
onTrigger(
        cursor: EditorPosition,
        editor: Editor,
        file: TFile
    ): EditorSuggestTriggerInfo {
        const line = editor.getLine(cursor.line);
        //not inside the bracket
        if (/> \[!\w+\]/.test(line.slice(0, cursor.ch))) return null;
        if (!/> \[!\w*/.test(line)) return null;

        const match = line.match(/> \[!(\w*)\]?/);
        if (!match) return null;

        const [_, query] = match;

        if (
            !query ||
            Object.keys(this.plugin.admonitions).find(
                (p) => p.toLowerCase() == query.toLowerCase()
            )
        ) {
            return null;
        }
        const matchData = {
            end: cursor,
            start: {
                ch: match.index + 4,
                line: cursor.line
            },
            query
        };
        return matchData;
    }
Example #6
Source File: utils.ts    From obsidian-editor-shortcuts with MIT License 6 votes vote down vote up
findNextMatchPosition = ({
  editor,
  latestMatchPos,
  searchText,
  searchWithinWords,
  documentContent,
}: {
  editor: Editor;
  latestMatchPos: EditorPosition;
  searchText: string;
  searchWithinWords: boolean;
  documentContent: string;
}) => {
  const latestMatchOffset = editor.posToOffset(latestMatchPos);
  const matches = findAllMatches({
    searchText,
    searchWithinWords,
    documentContent,
  });
  let nextMatch: EditorSelection | null = null;

  for (const match of matches) {
    if (match.index > latestMatchOffset) {
      nextMatch = {
        anchor: editor.offsetToPos(match.index),
        head: editor.offsetToPos(match.index + searchText.length),
      };
      break;
    }
  }
  // Circle back to search from the top
  if (!nextMatch) {
    const selectionIndexes = editor.listSelections().map((selection) => {
      const { from } = getSelectionBoundaries(selection);
      return editor.posToOffset(from);
    });
    for (const match of matches) {
      if (!selectionIndexes.includes(match.index)) {
        nextMatch = {
          anchor: editor.offsetToPos(match.index),
          head: editor.offsetToPos(match.index + searchText.length),
        };
        break;
      }
    }
  }

  return nextMatch;
}
Example #7
Source File: utils.ts    From obsidian-editor-shortcuts with MIT License 6 votes vote down vote up
findPosOfNextCharacter = ({
  editor,
  startPos,
  checkCharacter,
  searchDirection,
}: {
  editor: Editor;
  startPos: EditorPosition;
  checkCharacter: CheckCharacter;
  searchDirection: DIRECTION;
}) => {
  let { line, ch } = startPos;
  let lineContent = editor.getLine(line);
  let matchFound = false;
  let matchedChar: string;

  if (searchDirection === DIRECTION.BACKWARD) {
    while (line >= 0) {
      // ch will initially be 0 if searching from start of line
      const char = lineContent.charAt(Math.max(ch - 1, 0));
      matchFound = checkCharacter(char);
      if (matchFound) {
        matchedChar = char;
        break;
      }
      ch--;
      // inclusive because (ch - 1) means the first character will already
      // have been checked
      if (ch <= 0) {
        line--;
        if (line >= 0) {
          lineContent = editor.getLine(line);
          ch = lineContent.length;
        }
      }
    }
  } else {
    while (line < editor.lineCount()) {
      const char = lineContent.charAt(ch);
      matchFound = checkCharacter(char);
      if (matchFound) {
        matchedChar = char;
        break;
      }
      ch++;
      if (ch >= lineContent.length) {
        line++;
        lineContent = editor.getLine(line);
        ch = 0;
      }
    }
  }

  return matchFound
    ? {
        match: matchedChar,
        pos: {
          line,
          ch,
        },
      }
    : null;
}
Example #8
Source File: utils.ts    From obsidian-editor-shortcuts with MIT License 6 votes vote down vote up
getLineEndPos = (
  line: number,
  editor: Editor,
): EditorPosition => ({
  line,
  ch: editor.getLine(line).length,
})
Example #9
Source File: utils.ts    From obsidian-editor-shortcuts with MIT License 6 votes vote down vote up
getLineStartPos = (line: number): EditorPosition => ({
  line,
  ch: 0,
})
Example #10
Source File: actions.ts    From obsidian-editor-shortcuts with MIT License 6 votes vote down vote up
addCursorsToSelectionEnds = (
  editor: Editor,
  emulate: CODE_EDITOR = CODE_EDITOR.VSCODE,
) => {
  // Only apply the action if there is exactly one selection
  if (editor.listSelections().length !== 1) {
    return;
  }
  const selection = editor.listSelections()[0];
  const { from, to } = getSelectionBoundaries(selection);
  const newSelections = [];
  for (let line = from.line; line <= to.line; line++) {
    const head = line === to.line ? to : getLineEndPos(line, editor);
    let anchor: EditorPosition;
    if (emulate === CODE_EDITOR.VSCODE) {
      anchor = head;
    } else {
      anchor = line === from.line ? from : getLineStartPos(line);
    }
    newSelections.push({
      anchor,
      head,
    });
  }
  editor.setSelections(newSelections);
}
Example #11
Source File: main.ts    From obsidian-emoji-shortcodes with MIT License 6 votes vote down vote up
onTrigger(cursor: EditorPosition, editor: Editor, _: TFile): EditorSuggestTriggerInfo | null {
		if (this.plugin.settings.suggester) {
			const sub = editor.getLine(cursor.line).substring(0, cursor.ch);
			const match = sub.match(/:\S+$/)?.first();
			if (match) {
				return {
					end: cursor,
					start: {
						ch: sub.lastIndexOf(match),
						line: cursor.line,
					},
					query: match,
				}
			}
		}
		return null;
	}
Example #12
Source File: urlConvertor.ts    From obsidian-map-view with GNU General Public License v3.0 6 votes vote down vote up
/**
     * Insert a geo link into the editor at the cursor position
     * @param location The geolocation to convert to text and insert
     * @param editor The Obsidian Editor instance
     * @param replaceStart The EditorPosition to start the replacement at. If null will replace any text selected
     * @param replaceLength The EditorPosition to stop the replacement at. If null will replace any text selected
     */
    insertLocationToEditor(
        location: leaflet.LatLng,
        editor: Editor,
        replaceStart?: EditorPosition,
        replaceLength?: number
    ) {
        const locationString = `[](geo:${location.lat},${location.lng})`;
        const cursor = editor.getCursor();
        if (replaceStart && replaceLength) {
            editor.replaceRange(locationString, replaceStart, {
                line: replaceStart.line,
                ch: replaceStart.ch + replaceLength,
            });
        } else editor.replaceSelection(locationString);
        // We want to put the cursor right after the beginning of the newly-inserted link
        const newCursorPos = replaceStart ? replaceStart.ch + 1 : cursor.ch + 1;
        editor.setCursor({ line: cursor.line, ch: newCursorPos });
        utils.verifyOrAddFrontMatter(editor, 'locations', '');
    }
Example #13
Source File: tagSuggest.ts    From obsidian-map-view with GNU General Public License v3.0 6 votes vote down vote up
onTrigger(
        cursor: EditorPosition,
        editor: Editor,
        file: TFile
    ): EditorSuggestTriggerInfo | null {
        const line = editor.getLine(cursor.line);
        // Start by verifying that the current line has an inline location.
        // If it doesn't, we don't wanna trigger the completion even if the user
        // starts typing 'tag:'
        const hasLocationMatch = matchInlineLocation(line);
        if (!hasLocationMatch || hasLocationMatch.length == 0) return null;
        const tagMatch = getTagUnderCursor(line, cursor.ch);
        if (tagMatch)
            return {
                start: { line: cursor.line, ch: tagMatch.index },
                end: {
                    line: cursor.line,
                    ch: tagMatch.index + tagMatch[0].length,
                },
                query: tagMatch[1],
            };
        return null;
    }
Example #14
Source File: locationSuggest.ts    From obsidian-map-view with GNU General Public License v3.0 6 votes vote down vote up
getGeolinkOfCursor(cursor: EditorPosition, editor: Editor) {
        const results = editor
            .getLine(cursor.line)
            .matchAll(this.cursorInsideGeolinkFinder);
        if (!results) return null;
        for (let result of results) {
            const linkName = result[1];
            if (
                cursor.ch >= result.index &&
                cursor.ch < result.index + linkName.length + 2
            )
                return {
                    index: result.index,
                    name: linkName,
                    linkEnd: result.index + result[0].length,
                };
        }
        return null;
    }
Example #15
Source File: main.ts    From luhman-obsidian-plugin with GNU General Public License v3.0 6 votes vote down vote up
async makeNote(
    path: string,
    title: string,
    content: string,
    placeCursorAtStartOfContent: boolean
  ) {
    let app = this.app;
    let titleContent = "# " + title + "\n\n";
    let fullContent = titleContent + content;
    let file = await this.app.vault.create(path, fullContent);
    let active = app.workspace.getLeaf();
    if (active == null) {
      return;
    }

    await active.openFile(file);

    let editor = app.workspace.getActiveViewOfType(MarkdownView)?.editor;
    if (editor == null) {
      return;
    }

    if (placeCursorAtStartOfContent) {
      let position: EditorPosition = { line: 2, ch: 0 };
      editor.setCursor(position);
    } else {
      editor.exec("goEnd");
    }
  }
Example #16
Source File: handler.ts    From obsidian-switcher-plus with GNU General Public License v3.0 6 votes vote down vote up
/**
   * Retrieves the position of the cursor, given that view is in a Mode that supports cursors.
   * @param  {View} view
   * @returns EditorPosition
   */
  getCursorPosition(view: View): EditorPosition {
    let cursor: EditorPosition = null;

    if (view?.getViewType() === 'markdown') {
      const md = view as MarkdownView;

      if (md.getMode() !== 'preview') {
        const { editor } = md;
        cursor = editor.getCursor('head');
      }
    }

    return cursor;
  }
Example #17
Source File: handler.ts    From obsidian-switcher-plus with GNU General Public License v3.0 6 votes vote down vote up
getEditorInfo(leaf: WorkspaceLeaf): SourceInfo {
    const { excludeViewTypes } = this.settings;
    let file: TFile = null;
    let isValidSource = false;
    let cursor: EditorPosition = null;

    if (leaf) {
      const { view } = leaf;

      const viewType = view.getViewType();
      file = view.file;
      cursor = this.getCursorPosition(view);

      // determine if the current active editor pane is valid
      const isCurrentEditorValid = !excludeViewTypes.includes(viewType);

      // whether or not the current active editor can be used as the target for
      // symbol search
      isValidSource = isCurrentEditorValid && !!file;
    }

    return { isValidSource, leaf, file, suggestion: null, cursor };
  }
Example #18
Source File: date-suggest.ts    From nldates-obsidian with MIT License 6 votes vote down vote up
onTrigger(
    cursor: EditorPosition,
    editor: Editor,
    file: TFile
  ): EditorSuggestTriggerInfo {
    if (!this.plugin.settings.isAutosuggestEnabled) {
      return null;
    }

    const triggerPhrase = this.plugin.settings.autocompleteTriggerPhrase;
    const startPos = this.context?.start || {
      line: cursor.line,
      ch: cursor.ch - triggerPhrase.length,
    };

    if (!editor.getRange(startPos, cursor).startsWith(triggerPhrase)) {
      return null;
    }

    const precedingChar = editor.getRange(
      {
        line: startPos.line,
        ch: startPos.ch - 1,
      },
      startPos
    );

    // Short-circuit if `@` as a part of a word (e.g. part of an email address)
    if (precedingChar && /[`a-zA-Z0-9]/.test(precedingChar)) {
      return null;
    }

    return {
      start: startPos,
      end: cursor,
      query: editor.getRange(startPos, cursor).substring(triggerPhrase.length),
    };
  }
Example #19
Source File: CursorJumper.ts    From Templater with GNU Affero General Public License v3.0 6 votes vote down vote up
set_cursor_location(positions: EditorPosition[]): void {
        const active_view =
            this.app.workspace.getActiveViewOfType(MarkdownView);
        if (!active_view) {
            return;
        }

        const editor = active_view.editor;

        const selections: Array<EditorRangeOrCaret> = [];
        for (const pos of positions) {
            selections.push({ from: pos });
        }

        const transaction: EditorTransaction = {
            selections: selections,
        };
        editor.transaction(transaction);
    }
Example #20
Source File: CursorJumper.ts    From Templater with GNU Affero General Public License v3.0 6 votes vote down vote up
get_editor_position_from_index(
        content: string,
        index: number
    ): EditorPosition {
        const substr = content.slice(0, index);

        let l = 0;
        let offset = -1;
        let r = -1;
        for (; (r = substr.indexOf("\n", r + 1)) !== -1; l++, offset = r);
        offset += 1;

        const ch = content.slice(offset, index).length;

        return { line: l, ch: ch };
    }
Example #21
Source File: Autocomplete.ts    From Templater with GNU Affero General Public License v3.0 6 votes vote down vote up
onTrigger(
        cursor: EditorPosition,
        editor: Editor,
        file: TFile
    ): EditorSuggestTriggerInfo | null {
        const range = editor.getRange(
            { line: cursor.line, ch: 0 },
            { line: cursor.line, ch: cursor.ch }
        );
        const match = this.tp_keyword_regex.exec(range);
        if (!match) {
            return null;
        }

        let query: string;
        const module_name = match.groups["module"] || "";
        this.module_name = module_name;

        if (match.groups["fn_trigger"]) {
            if (module_name == "" || !is_module_name(module_name)) {
                return;
            }
            this.function_trigger = true;
            this.function_name = match.groups["fn"] || "";
            query = this.function_name;
        } else {
            this.function_trigger = false;
            query = this.module_name;
        }

        const trigger_info: EditorSuggestTriggerInfo = {
            start: { line: cursor.line, ch: cursor.ch - query.length },
            end: { line: cursor.line, ch: cursor.ch },
            query: query,
        };
        this.latest_trigger_info = trigger_info;
        return trigger_info;
    }
Example #22
Source File: main.ts    From luhman-obsidian-plugin with GNU General Public License v3.0 6 votes vote down vote up
insertTextIntoCurrentNote(text: string) {
    let view = this.app.workspace.getActiveViewOfType(MarkdownView);

    if (view) {
      let editor = view!.editor;

      let position: EditorPosition;
      var prefix: string = "";

      if (editor.getSelection()) {
        let selectionPos = editor.listSelections()[0];
        let positionCH = Math.max(selectionPos.head.ch, selectionPos.anchor.ch);
        position = { line: selectionPos.anchor.line, ch: positionCH + 1 };
        prefix = " ";
      } else {
        position = editor.getCursor();
      }

      editor.replaceRange(" " + text, position, position);
    }
  }
Example #23
Source File: CursorJumper.ts    From Templater with GNU Affero General Public License v3.0 5 votes vote down vote up
replace_and_get_cursor_positions(content: string): {
        new_content?: string;
        positions?: EditorPosition[];
    } {
        let cursor_matches = [];
        let match;
        const cursor_regex = new RegExp(
            "<%\\s*tp.file.cursor\\((?<order>[0-9]{0,2})\\)\\s*%>",
            "g"
        );

        while ((match = cursor_regex.exec(content)) != null) {
            cursor_matches.push(match);
        }
        if (cursor_matches.length === 0) {
            return {};
        }

        cursor_matches.sort((m1, m2) => {
            return Number(m1.groups["order"]) - Number(m2.groups["order"]);
        });
        const match_str = cursor_matches[0][0];

        cursor_matches = cursor_matches.filter((m) => {
            return m[0] === match_str;
        });

        const positions = [];
        let index_offset = 0;
        for (const match of cursor_matches) {
            const index = match.index - index_offset;
            positions.push(this.get_editor_position_from_index(content, index));

            content = content.replace(new RegExp(escape_RegExp(match[0])), "");
            index_offset += match[0].length;

            // For tp.file.cursor(), we keep the default top to bottom
            if (match[1] === "") {
                break;
            }
        }

        return { new_content: content, positions: positions };
    }
Example #24
Source File: main.ts    From luhman-obsidian-plugin with GNU General Public License v3.0 5 votes vote down vote up
makeNoteFunction(idGenerator: (file: TFile) => string) {
    var file = this.app.workspace.getActiveFile();
    if (file == null) {
      return;
    }
    if (this.isZettelFile(file.name)) {
      let fileID = this.fileToId(file.basename);
      let fileLink = "[[" + file.basename + "]]";

      let editor = this.app.workspace.getActiveViewOfType(MarkdownView)?.editor;
      if (editor == null) {
        return;
      }

      let selection = editor.getSelection();

      let nextID = idGenerator.bind(this, file)();
      let nextPath = (title: string) =>
        file?.path
          ? this.app.fileManager.getNewFileParent(file.path).path +
              "/" +
              nextID +
              (this.settings.addTitle
                ? this.settings.separator + title
                : ''
              ) +
              ".md"
          : '';
      let newLink = "[[" + nextID + "]]";

      if (selection) {
        let title = selection
          .split(/\s+/)
          .map((w) => w[0].toUpperCase() + w.slice(1))
          .join(" ");
        let selectionPos = editor!.listSelections()[0];
        let positionCH = Math.max(selectionPos.head.ch, selectionPos.anchor.ch);
        let position: EditorPosition = {
          line: selectionPos.anchor.line,
          ch: positionCH + 1,
        };
        editor!.replaceRange(" " + newLink, position, position);
        this.makeNote(nextPath(title), title, fileLink, true);
      } else {
        new NewZettelModal(this.app, (title: string) => {
          this.insertTextIntoCurrentNote(newLink);
          this.makeNote(nextPath(title), title, fileLink, true);
        }).open();
      }
    } else {

    }
  }
Example #25
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');
  });
});