From a2d13aabc603489040c11f79a4351ff87e502826 Mon Sep 17 00:00:00 2001 From: wz <44930227+wenlzhang@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:44:17 +0200 Subject: [PATCH] feat: add clickable line content with configurable navigation behavior --- docs/dev/LINE_CONTENT_DISPLAY.md | 128 ++++++++++++++++++++++++++++++- src/settings.ts | 6 +- src/settingsTab.ts | 32 ++++++++ src/tagIndexView.ts | 98 ++++++++++++++++++++++- styles.css | 16 ++++ 5 files changed, 277 insertions(+), 3 deletions(-) diff --git a/docs/dev/LINE_CONTENT_DISPLAY.md b/docs/dev/LINE_CONTENT_DISPLAY.md index ef0e6f7..d317c01 100644 --- a/docs/dev/LINE_CONTENT_DISPLAY.md +++ b/docs/dev/LINE_CONTENT_DISPLAY.md @@ -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 { + 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 diff --git a/src/settings.ts b/src/settings.ts index 7a19788..51259ed 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -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", }; diff --git a/src/settingsTab.ts b/src/settingsTab.ts index f1ecd12..63d04dd 100644 --- a/src/settingsTab.ts +++ b/src/settingsTab.ts @@ -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(); diff --git a/src/tagIndexView.ts b/src/tagIndexView.ts index dd83d36..f0a41fa 100644 --- a/src/tagIndexView.ts +++ b/src/tagIndexView.ts @@ -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 { + 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 { const expandedTagsBefore = new Set(this.expandedTags); const expandedNodesBefore = new Set(this.expandedNodes); diff --git a/styles.css b/styles.css index 32e7a52..9fc20b6 100644 --- a/styles.css +++ b/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);