Select Collapsed Heading

This plugin enhances core usability and should be considered for core inclusion.

It adds a toolbar/ribbon command to select collapsed heading sections.

Especially useful on mobile where long-press or manual selection is impractical.

Lightweight, non-invasive, and built on existing API (cm.findFold()), it integrates cleanly and improves editing fluidity without altering markdown content or structure.

On mobile it was most annoying. This solves the issue.

{
  "id": "select-collapsed-heading",
  "name": "Select Collapsed Heading",
  "version": "1.1.1",
  "minAppVersion": "1.0.0",
  "description": "Adds a toolbar button to select folded content under the cursor (ideal for Android mobile).",
  "author": "Custom Plugin",
  "authorUrl": "",
  "main": "main.js"
}

import { Plugin, MarkdownView, Notice, Editor } from "obsidian";

export default class SelectCollapsedHeadingPlugin extends Plugin {
  async onload() {
    // Add command for toolbar and command palette
    this.addCommand({
      id: "select-collapsed-heading",
      name: "Select Collapsed Heading Under Cursor",
      icon: "expand-vertically",
      editorCallback: (editor: Editor) => {
        try {
          const cursor = editor.getCursor();
          
          // Try multiple methods to find folded content
          const success = this.selectFoldedContent(editor, cursor);
          
          if (success) {
            new Notice("Collapsed section selected!");
          } else {
            new Notice("No collapsed heading found at cursor position.");
          }
        } catch (error) {
          console.error("Select Collapsed Heading error:", error);
          new Notice("Error selecting collapsed content.");
        }
      }
    });

    // Add ribbon icon for desktop (hidden on mobile automatically)
    this.addRibbonIcon("expand-vertically", "Select Collapsed Heading", () => {
      const view = this.app.workspace.getActiveViewOfType(MarkdownView);
      if (!view || !view.editor) {
        new Notice("No active markdown editor found.");
        return;
      }

      const editor = view.editor;
      const cursor = editor.getCursor();
      
      try {
        const success = this.selectFoldedContent(editor, cursor);
        
        if (success) {
          new Notice("Collapsed section selected!");
        } else {
          new Notice("No collapsed heading found at cursor position.");
        }
      } catch (error) {
        console.error("Select Collapsed Heading error:", error);
        new Notice("Error selecting collapsed content.");
      }
    });
  }

  selectFoldedContent(editor: Editor, cursor: any): boolean {
    // Method 1: Try CodeMirror 6 fold detection
    if ((editor as any).cm && (editor as any).cm.state) {
      try {
        const state = (editor as any).cm.state;
        const pos = state.doc.line(cursor.line + 1).from; // Convert to CM6 position
        
        // Check for folds at this position
        const folds = state.field(state.facet((editor as any).cm.constructor.foldable || {}), false);
        if (folds) {
          for (let fold of folds.iter()) {
            if (fold.from <= pos && fold.to >= pos) {
              // Convert back to Obsidian coordinates
              const fromLine = state.doc.lineAt(fold.from);
              const toLine = state.doc.lineAt(fold.to);
              
              editor.setSelection(
                { line: fromLine.number - 1, ch: 0 },
                { line: toLine.number - 1, ch: toLine.text.length }
              );
              return true;
            }
          }
        }
      } catch (e) {
        // Fallback to other methods
      }
    }

    // Method 2: Check for heading patterns and find folded content
    const currentLine = editor.getLine(cursor.line);
    const headingMatch = currentLine.match(/^(#{1,6})\s+(.+)/);
    
    if (headingMatch) {
      const headingLevel = headingMatch[1].length;
      const startLine = cursor.line;
      let endLine = startLine;
      
      // Find the end of this heading section
      for (let i = startLine + 1; i < editor.lineCount(); i++) {
        const line = editor.getLine(i);
        const nextHeadingMatch = line.match(/^(#{1,6})\s+/);
        
        if (nextHeadingMatch && nextHeadingMatch[1].length <= headingLevel) {
          endLine = i - 1;
          break;
        }
        endLine = i;
      }
      
      if (endLine > startLine) {
        editor.setSelection(
          { line: startLine, ch: 0 },
          { line: endLine, ch: editor.getLine(endLine).length }
        );
        return true;
      }
    }

    // Method 3: Try legacy CodeMirror methods
    if ((editor as any).cm) {
      try {
        const cm = (editor as any).cm;
        const pos = { line: cursor.line, ch: cursor.ch };
        
        // Try different fold detection methods
        if (cm.findFold) {
          const fold = cm.findFold(pos);
          if (fold) {
            editor.setSelection(fold.from, fold.to);
            return true;
          }
        }
        
        if (cm.state && cm.state.foldGutter) {
          const folds = cm.state.foldGutter.folds;
          if (folds) {
            for (let fold of folds) {
              if (fold.from.line <= cursor.line && fold.to.line >= cursor.line) {
                editor.setSelection(fold.from, fold.to);
                return true;
              }
            }
          }
        }
      } catch (e) {
        console.log("Legacy fold detection failed:", e);
      }
    }

    return false;
  }

  onunload() {
    // Cleanup
  }
}

const { Plugin, MarkdownView, Notice } = require("obsidian");

class SelectCollapsedHeadingPlugin extends Plugin {
  async onload() {
    // Add command for toolbar and command palette
    this.addCommand({
      id: "select-collapsed-heading",
      name: "Select Collapsed Heading Under Cursor",
      icon: "expand-vertically",
      editorCallback: (editor) => {
        try {
          const cursor = editor.getCursor();
          
          // Try multiple methods to find folded content
          const success = this.selectFoldedContent(editor, cursor);
          
          if (success) {
            new Notice("Collapsed section selected!");
          } else {
            new Notice("No collapsed heading found at cursor position.");
          }
        } catch (error) {
          console.error("Select Collapsed Heading error:", error);
          new Notice("Error selecting collapsed content.");
        }
      }
    });

    // Add ribbon icon for desktop (hidden on mobile automatically)
    this.addRibbonIcon("expand-vertically", "Select Collapsed Heading", () => {
      const view = this.app.workspace.getActiveViewOfType(MarkdownView);
      if (!view || !view.editor) {
        new Notice("No active markdown editor found.");
        return;
      }

      const editor = view.editor;
      const cursor = editor.getCursor();
      
      try {
        const success = this.selectFoldedContent(editor, cursor);
        
        if (success) {
          new Notice("Collapsed section selected!");
        } else {
          new Notice("No collapsed heading found at cursor position.");
        }
      } catch (error) {
        console.error("Select Collapsed Heading error:", error);
        new Notice("Error selecting collapsed content.");
      }
    });
  }

  selectFoldedContent(editor, cursor) {
    // Method 1: Try CodeMirror 6 fold detection
    if (editor.cm && editor.cm.state) {
      try {
        const state = editor.cm.state;
        const pos = state.doc.line(cursor.line + 1).from; // Convert to CM6 position
        
        // Check for folds at this position
        const folds = state.field(state.facet(editor.cm.constructor.foldable || {}), false);
        if (folds) {
          for (let fold of folds.iter()) {
            if (fold.from <= pos && fold.to >= pos) {
              // Convert back to Obsidian coordinates
              const fromLine = state.doc.lineAt(fold.from);
              const toLine = state.doc.lineAt(fold.to);
              
              editor.setSelection(
                { line: fromLine.number - 1, ch: 0 },
                { line: toLine.number - 1, ch: toLine.text.length }
              );
              return true;
            }
          }
        }
      } catch (e) {
        // Fallback to other methods
      }
    }

    // Method 2: Check for heading patterns and find folded content
    const currentLine = editor.getLine(cursor.line);
    const headingMatch = currentLine.match(/^(#{1,6})\s+(.+)/);
    
    if (headingMatch) {
      const headingLevel = headingMatch[1].length;
      const startLine = cursor.line;
      let endLine = startLine;
      
      // Find the end of this heading section
      for (let i = startLine + 1; i < editor.lineCount(); i++) {
        const line = editor.getLine(i);
        const nextHeadingMatch = line.match(/^(#{1,6})\s+/);
        
        if (nextHeadingMatch && nextHeadingMatch[1].length <= headingLevel) {
          endLine = i - 1;
          break;
        }
        endLine = i;
      }
      
      if (endLine > startLine) {
        editor.setSelection(
          { line: startLine, ch: 0 },
          { line: endLine, ch: editor.getLine(endLine).length }
        );
        return true;
      }
    }

    // Method 3: Try legacy CodeMirror methods
    if (editor.cm) {
      try {
        const cm = editor.cm;
        const pos = { line: cursor.line, ch: cursor.ch };
        
        // Try different fold detection methods
        if (cm.findFold) {
          const fold = cm.findFold(pos);
          if (fold) {
            editor.setSelection(fold.from, fold.to);
            return true;
          }
        }
        
        if (cm.state && cm.state.foldGutter) {
          const folds = cm.state.foldGutter.folds;
          if (folds) {
            for (let fold of folds) {
              if (fold.from.line <= cursor.line && fold.to.line >= cursor.line) {
                editor.setSelection(fold.from, fold.to);
                return true;
              }
            }
          }
        }
      } catch (e) {
        console.log("Legacy fold detection failed:", e);
      }
    }

    return false;
  }

  onunload() {
    // Cleanup
  }
}

module.exports = SelectCollapsedHeadingPlugin;

This plugin is a third-party community contribution and is not affiliated with Obsidian.md
This pseudonym is not affiliated with Obsidian.md

© 2025 Obsidiannow. All rights reserved.

On iOS I can select a folded heading by triple tapping it to select the line. If I want to also select the text folded inside the heading, I extend the selection to include the at the end. Does that not work on Android?

That’s a nuisance. Yes it does work though.

Sometimes it’s glitchy too.

What kind of glitches?

I try to select including the three dots and it deselects it. The plugin I gave is the fastest way. You don’t even have to select anything.