feat: add clickable line content with configurable navigation behavior

This commit is contained in:
wz 2025-10-03 14:44:17 +02:00
parent af7de16029
commit a2d13aabc6
5 changed files with 277 additions and 3 deletions

View file

@ -222,14 +222,140 @@ Users can toggle the feature:
4. **No inline tags**: Only frontmatter tags won't show line content
5. **File not readable**: Gracefully handles with empty array return
## Clickable line content
### Overview
Line content is now clickable, allowing users to quickly navigate to the exact location in the file. This feature includes configurable behaviors for different workflows.
### Settings
#### Line content click behavior
Two modes are available:
1. **Jump to line** (default)
- Opens the file at the specified line
- Places cursor at configured position
- Simple and focused navigation
2. **Jump to line and search**
- Opens the file at the specified line
- Places cursor at configured position
- Opens the search pane with tag query
- Shows all occurrences across the vault
- Enables multi-file review
#### Cursor position
Two options for cursor placement:
1. **End of line** (default)
- Cursor placed at the end of the line
- Useful for continuing to write
- Natural reading flow
2. **Start of line**
- Cursor placed at the beginning
- Useful for editing from the start
- Quick deletion or modification
### Implementation
#### Click handler
```typescript
private async handleLineContentClick(
file: TFile,
lineNumber: number,
tagName: string,
): Promise<void> {
const behavior = this.plugin.settings.lineContentClickBehavior;
const cursorPos = this.plugin.settings.cursorPosition;
// Open file and jump to line
const leaf = this.app.workspace.getLeaf(false);
await leaf.openFile(file);
// Get editor and set cursor
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!view || !view.editor) return;
const editor = view.editor;
const line = editor.getLine(lineNumber);
const column = cursorPos === "end" ? line.length : 0;
editor.setCursor({ line: lineNumber, ch: column });
editor.scrollIntoView(...);
// Open search if configured
if (behavior === "jumpAndSearch") {
// Trigger search functionality
}
}
```
#### Search integration
When "Jump to line and search" is enabled:
1. Checks if search pane exists
2. If exists: Sets query and reveals pane
3. If not: Opens search pane, waits, then sets query
4. Query format: `tag:tagname` (without #)
#### Visual feedback
CSS classes provide clear interaction cues:
```css
.tag-index-line-clickable {
cursor: pointer;
}
.tag-index-line-clickable:hover {
background-color: var(--interactive-hover);
border-color: var(--interactive-accent);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.tag-index-line-clickable:active {
background-color: var(--interactive-accent);
transform: translateY(1px);
}
```
### User workflows
#### Workflow 1: Quick navigation
1. User sees interesting line content
2. Clicks on it
3. File opens at exact line
4. Cursor ready to edit
#### Workflow 2: Research mode
1. User wants to review all tag occurrences
2. Clicks on line content
3. File opens at that line
4. Search pane opens showing all matches
5. User can browse other occurrences
### Benefits
- **Faster navigation**: One-click access to exact location
- **Context awareness**: See content before jumping
- **Flexible workflows**: Choose behavior per use case
- **Visual feedback**: Clear hover and active states
- **Search integration**: Seamless cross-file exploration
## Future enhancements
Potential improvements:
- Click on line content to jump to that line in the file
- Syntax highlighting for code blocks
- Configurable line content preview length
- Option to show surrounding context (lines before/after)
- Display block content for block-level tags
- Middle-click to open in new pane
## Related files

View file

@ -14,6 +14,8 @@ export interface TagIndexSettings {
expandedTags: string[]; // Tags with notes expanded
expandedNodes: string[]; // Nodes with children expanded
showLineContent: boolean; // Show line/block content where tags appear
lineContentClickBehavior: "jumpToLine" | "jumpAndSearch"; // What happens when clicking line content
cursorPosition: "start" | "end"; // Where to place cursor when jumping to line
}
export const DEFAULT_SETTINGS: TagIndexSettings = {
@ -23,5 +25,7 @@ export const DEFAULT_SETTINGS: TagIndexSettings = {
autoOpenTagIndexPanel: false,
expandedTags: [],
expandedNodes: [],
showLineContent: false,
showLineContent: true,
lineContentClickBehavior: "jumpToLine",
cursorPosition: "end",
};

View file

@ -61,6 +61,38 @@ export class TagIndexSettingTab extends PluginSettingTab {
}),
);
new Setting(containerEl)
.setName("Line content click behavior")
.setDesc(
"Choose what happens when you click on line content. 'Jump to line' opens the file at that line. 'Jump and search' also opens the search pane to show all occurrences.",
)
.addDropdown((dropdown) =>
dropdown
.addOption("jumpToLine", "Jump to line")
.addOption("jumpAndSearch", "Jump to line and search")
.setValue(this.plugin.settings.lineContentClickBehavior)
.onChange(async (value: "jumpToLine" | "jumpAndSearch") => {
this.plugin.settings.lineContentClickBehavior = value;
await this.plugin.saveSettings();
}),
);
new Setting(containerEl)
.setName("Cursor position")
.setDesc(
"Choose where the cursor is placed when jumping to a line. 'End of line' places the cursor at the end. 'Start of line' places it at the beginning.",
)
.addDropdown((dropdown) =>
dropdown
.addOption("end", "End of line")
.addOption("start", "Start of line")
.setValue(this.plugin.settings.cursorPosition)
.onChange(async (value: "start" | "end") => {
this.plugin.settings.cursorPosition = value;
await this.plugin.saveSettings();
}),
);
// Add a heading for Advanced settings
new Setting(containerEl).setName("Advanced").setHeading();

View file

@ -8,6 +8,7 @@ import {
App,
TagCache,
Notice,
MarkdownView,
} from "obsidian";
import type TagIndexPlugin from "./main";
import { ImportantTag } from "./settings";
@ -533,9 +534,23 @@ export class TagIndexView extends ItemView {
for (const item of tagLineContent) {
const lineEl = linesContainer.createDiv({
cls: "tag-index-line-content",
cls: "tag-index-line-content tag-index-line-clickable",
});
// Add click handler for line content
lineEl.addEventListener(
"click",
async (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
await this.handleLineContentClick(
file,
item.line,
tagName,
);
},
);
// Add line number
const lineNumber = lineEl.createSpan({
cls: "tag-index-line-number",
@ -558,6 +573,87 @@ export class TagIndexView extends ItemView {
}
}
// Handle click on line content
private async handleLineContentClick(
file: TFile,
lineNumber: number,
tagName: string,
): Promise<void> {
const behavior = this.plugin.settings.lineContentClickBehavior;
const cursorPos = this.plugin.settings.cursorPosition;
// Open the file and jump to the line
const leaf = this.app.workspace.getLeaf(false);
await leaf.openFile(file);
// Get the editor from the markdown view
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!view || !view.editor) {
return;
}
const editor = view.editor;
// Calculate cursor position on the line
const line = editor.getLine(lineNumber);
const column = cursorPos === "end" ? line.length : 0;
// Set cursor position
editor.setCursor({
line: lineNumber,
ch: column,
});
// Scroll to the line
editor.scrollIntoView(
{
from: { line: lineNumber, ch: 0 },
to: { line: lineNumber, ch: line.length },
},
true,
);
// If the behavior is to also search, trigger the search
if (behavior === "jumpAndSearch") {
// Use Obsidian's search functionality
const searchLeaf = this.app.workspace.getLeavesOfType("search")[0];
if (searchLeaf) {
// Get the search view
const searchView = searchLeaf.view as any;
// Set the search query to the tag
if (searchView && searchView.setQuery) {
searchView.setQuery(
`tag:${tagName.startsWith("#") ? tagName.substring(1) : tagName}`,
);
}
// Reveal the search pane
this.app.workspace.revealLeaf(searchLeaf);
} else {
// If search pane doesn't exist, execute the global search command
(this.app as any).commands.executeCommandById(
"global-search:open",
);
// Wait a bit for the search pane to open
setTimeout(() => {
const newSearchLeaf =
this.app.workspace.getLeavesOfType("search")[0];
if (newSearchLeaf) {
const searchView = newSearchLeaf.view as any;
if (searchView && searchView.setQuery) {
searchView.setQuery(
`tag:${tagName.startsWith("#") ? tagName.substring(1) : tagName}`,
);
}
}
}, 100);
}
}
}
private async renderTagsAndRestoreExpansion(): Promise<void> {
const expandedTagsBefore = new Set(this.expandedTags);
const expandedNodesBefore = new Set(this.expandedNodes);

View file

@ -466,6 +466,22 @@
border-color: var(--background-modifier-border-hover);
}
/* Clickable line content styling */
.tag-index-line-clickable {
cursor: pointer;
}
.tag-index-line-clickable:hover {
background-color: var(--interactive-hover);
border-color: var(--interactive-accent);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.tag-index-line-clickable:active {
background-color: var(--interactive-accent);
transform: translateY(1px);
}
.tag-index-line-number {
color: var(--text-faint);
font-family: var(--font-monospace);