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.