mirror of
https://github.com/wenlzhang/obsidian-tag-index.git
synced 2026-07-22 05:44:03 +00:00
feat: add clickable line content with configurable navigation behavior
This commit is contained in:
parent
af7de16029
commit
a2d13aabc6
5 changed files with 277 additions and 3 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
16
styles.css
16
styles.css
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue