Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a69dbf1
feat: enlarge drag handle default size for better visibility
14743768433 Feb 12, 2026
6fab21c
feat: 增加块选中的背景光晕
14743768433 Feb 12, 2026
354d61d
fix: use correct CSS variable for drop highlight color
14743768433 Feb 12, 2026
0fb9a34
feat: use text color for handle by default
14743768433 Feb 12, 2026
34b0a57
feat: add text selection to block selection conversion
14743768433 Feb 12, 2026
9f106b9
fix: clear editor text selection when converting to block selection
14743768433 Feb 12, 2026
8103e78
fix: hide vertical link line for text-selection-based block selection
14743768433 Feb 12, 2026
cce3c28
fix: properly hide link line when committing smart block selection
14743768433 Feb 12, 2026
a78ee79
fix: keep handle in normal state for smart block selection
14743768433 Feb 12, 2026
556a824
feat: hide handles for non-anchor blocks in smart selection
14743768433 Feb 12, 2026
5689603
fix: prevent handle hover effects during block selection
14743768433 Feb 12, 2026
96533ce
fix: hide other handles when committing single-click range selection
14743768433 Feb 12, 2026
1132939
fix: commit selection on quick mouse click and keep handle normal
14743768433 Feb 12, 2026
a91a223
fix: clear stale drag highlight after multi-node move
14743768433 Feb 12, 2026
b8c34b3
feat: improve range-selection drag flow and diagnostics
14743768433 Feb 12, 2026
68732ce
feat: keep single-block selected after drag drop
14743768433 Feb 12, 2026
69d67d7
feat: allow smart text-selection drag with persistent block selection
14743768433 Feb 12, 2026
e4e9187
wip: refine smart text-selection drag and selection visuals
14743768433 Feb 13, 2026
b42c167
fix: smart text-selection drag now correctly highlights all selected …
14743768433 Feb 13, 2026
646d52e
fix: allow native HTML5 drag after smart text selection
14743768433 Feb 13, 2026
4d1673a
fix: stabilize smart range selection and preserve drag workflows
14743768433 Feb 13, 2026
9dddaf5
fix: keep committed block selection stable after view updates
14743768433 Feb 13, 2026
e4c90ec
fix: allow clearing block selection by clicking selected area
14743768433 Feb 13, 2026
56f3067
fix: harden range selection restore and snapshot lifecycle
14743768433 Feb 13, 2026
d5246a0
chore: remove debug logs from drag interaction flow
14743768433 Feb 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion esbuild.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import builtins from "builtin-modules";
import fs from "fs";

const prod = process.argv[2] === "production";
const pluginDir = "C:/Users/19411_4bs7lzt/OneDrive/obsidian/.obsidian/plugins/dragger";
const pluginDir = "/Users/jinyujia/Downloads/test/.obsidian/plugins/dragger";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

这个文件里硬编码了一个个人本地开发路径。这会导致其他开发者的构建失败,并且可能会泄露个人信息。建议使用环境变量来配置此路径,或者在提交前移除此更改。

const pluginDir = process.env.OBSIDIAN_PLUGIN_DIR || "dist";


// 复制 styles.css 到插件目录
function copyStyles() {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions src/editor/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const HANDLE_INTERACTION_ZONE_PX = 64;
* Centralised mutable config – set once per plugin load via `applySettings()`.
*/
const handleConfig = {
sizePx: 16,
sizePx: 24,
horizontalOffsetPx: 0,
alignToLineNumber: true,
};
Expand All @@ -39,7 +39,7 @@ export function getHandleSizePx(): number {
}

export function setHandleSizePx(size: number): void {
handleConfig.sizePx = Math.max(12, Math.min(28, size));
handleConfig.sizePx = Math.max(12, Math.min(36, size));
}

export function getHandleHorizontalOffsetPx(): number {
Expand All @@ -63,3 +63,4 @@ export function setAlignToLineNumber(align: boolean): void {
*/
export const HOVER_HIDDEN_LINE_NUMBER_CLASS = 'dnd-line-number-hover-hidden';
export const GRAB_HIDDEN_LINE_NUMBER_CLASS = 'dnd-line-number-grab-hidden';
export const BLOCK_SELECTION_ACTIVE_CLASS = 'dnd-block-selection-active';
2 changes: 0 additions & 2 deletions src/editor/core/line-map.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,6 @@ describe('line-map', () => {
get: summarizeDurations(getDurations),
};

console.debug('[Dragger][PerfTest] typing_line_map', JSON.stringify(report, null, 2));

expect(report.total.count).toBe(iterations);
expect(report.prime.p95).toBeGreaterThanOrEqual(0);
expect(report.get.p95).toBeGreaterThanOrEqual(0);
Expand Down
10 changes: 2 additions & 8 deletions src/editor/core/perf-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,6 @@ function summarize(values: number[]): { count: number; p50: number; p95: number;
};
}

function serializeSnapshot(snapshot: DragPerfSnapshot): string {
return JSON.stringify(snapshot, null, 2);
}

export function createDragPerfSession(input: DragPerfSessionInput): DragPerfSession {
const startedAtMs = nowMs();
const durations = createDurationStore();
Expand Down Expand Up @@ -145,8 +141,6 @@ export function createDragPerfSession(input: DragPerfSessionInput): DragPerfSess
};
}

export function logDragPerfSession(session: DragPerfSession | null, reason: string): void {
if (!session) return;
const snapshot = session.snapshot();
console.debug('[Dragger][Perf]', reason, serializeSnapshot(snapshot));
export function logDragPerfSession(_session: DragPerfSession | null, _reason: string): void {
// no-op in normal runtime
}
1 change: 1 addition & 0 deletions src/editor/core/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ export const DRAG_SOURCE_LINE_NUMBER_CLASS = 'dnd-drag-source-line-number';
export const RANGE_SELECTED_LINE_CLASS = 'dnd-range-selected-line';
export const RANGE_SELECTED_HANDLE_CLASS = 'dnd-range-selected-handle';
export const RANGE_SELECTION_LINK_CLASS = 'dnd-range-selection-link';
export const SELECTION_HIGHLIGHT_LINE_CLASS = 'dnd-selection-highlight-line';
export const MOBILE_GESTURE_LOCK_CLASS = 'dnd-mobile-gesture-lock';
132 changes: 132 additions & 0 deletions src/editor/core/services/EditorSelectionBridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import type { EditorView } from '@codemirror/view';
import type { LineRange } from '../../../types';
import {
mergeLineRanges,
resolveBlockAlignedLineRange,
resolveBlockBoundaryAtLine,
} from '../../interaction/RangeSelectionLogic';

export type EditorTextSelection = {
from: number;
to: number;
fromLine: number;
toLine: number;
};

/**
* Bridge between CodeMirror editor selection and block-based selection.
* Reads the editor's native text selection and converts it to line ranges
* that can be used for block-aligned multi-line selection.
*/
export class EditorSelectionBridge {
constructor(private readonly view: EditorView) {}

/**
* Get the current text selection in the editor.
* @returns Selection info if there's a valid non-empty selection, null otherwise
*/
getTextSelection(): EditorTextSelection | null {
return this.getTextSelections()[0] ?? null;
}

/**
* Get all non-empty text selections in the editor.
* Supports multi-range selections (e.g. multiple carets/ranges).
*/
getTextSelections(): EditorTextSelection[] {
const doc = this.view.state.doc;
const ranges = this.view.state.selection.ranges;
const selections: EditorTextSelection[] = [];

for (const range of ranges) {
if (range.empty) continue;
const fromLine = doc.lineAt(range.from).number;
const toLine = doc.lineAt(range.to).number;
selections.push({
from: range.from,
to: range.to,
fromLine,
toLine,
});
}
return selections;
}

/**
* Check if a line number is within the current editor selection.
*/
isLineInSelection(lineNumber: number): boolean {
const selections = this.getTextSelections();
if (selections.length === 0) return false;
return selections.some((selection) => (
lineNumber >= selection.fromLine && lineNumber <= selection.toLine
));
}

/**
* Convert the current editor selection to block-aligned line ranges.
* This ensures that partial selections are expanded to include complete blocks.
* @returns Block-aligned line ranges, or null if no valid selection
*/
resolveBlockAlignedSelection(): LineRange[] | null {
const selections = this.getTextSelections();
if (selections.length === 0) return null;

const state = this.view.state;
const docLines = state.doc.lines;
const ranges: LineRange[] = [];

for (const selection of selections) {
// Get the block boundaries at the selection start and end
const startBoundary = resolveBlockBoundaryAtLine(state, selection.fromLine);
const endBoundary = resolveBlockBoundaryAtLine(state, selection.toLine);

// Resolve to block-aligned range
const aligned = resolveBlockAlignedLineRange(
state,
startBoundary.startLineNumber,
startBoundary.endLineNumber,
endBoundary.startLineNumber,
endBoundary.endLineNumber
);
ranges.push({
startLineNumber: aligned.startLineNumber,
endLineNumber: aligned.endLineNumber,
});
}
return mergeLineRanges(docLines, ranges);
}

/**
* Check if a line number intersects with the editor selection.
* If it does, return the block-aligned selection ranges.
* @param lineNumber The line number to check (1-indexed)
* @returns Block-aligned ranges if the line intersects, null otherwise
*/
getBlockAlignedRangeIfIntersecting(lineNumber: number): LineRange[] | null {
return this.getBlockAlignedRangeIfRangeIntersecting(lineNumber, lineNumber);
}

/**
* Check if a line range intersects with the editor selection.
* If it does, return the block-aligned selection ranges.
* @param startLineNumber Inclusive start line number (1-indexed)
* @param endLineNumber Inclusive end line number (1-indexed)
*/
getBlockAlignedRangeIfRangeIntersecting(startLineNumber: number, endLineNumber: number): LineRange[] | null {
const selections = this.getTextSelections();
if (selections.length === 0) return null;

const safeStart = Math.min(startLineNumber, endLineNumber);
const safeEnd = Math.max(startLineNumber, endLineNumber);

// Trigger only when clicked range intersects at least one text selection range.
const intersects = selections.some((selection) => (
safeEnd >= selection.fromLine && safeStart <= selection.toLine
));
if (!intersects) return null;

// Return the block-aligned selection
return this.resolveBlockAlignedSelection();
}
}
26 changes: 25 additions & 1 deletion src/editor/drag-handle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import DragNDropPlugin from '../main';
import {
ROOT_EDITOR_CLASS,
MAIN_EDITOR_CONTENT_CLASS,
DRAG_HANDLE_CLASS,
MOBILE_GESTURE_LOCK_CLASS,
DRAGGING_BODY_CLASS,
} from './core/selectors';
Expand Down Expand Up @@ -63,6 +64,7 @@ function createDragHandleViewPlugin(_plugin: DragNDropPlugin) {
private readonly semanticRefreshScheduler: SemanticRefreshScheduler;
private lastPointerPos: { x: number; y: number } | null = null;
private readonly onDocumentPointerMove = (e: PointerEvent) => this.handleDocumentPointerMove(e);
private readonly onDocumentPointerDown = (e: PointerEvent) => this.handleDocumentPointerDown(e);
private readonly onSettingsUpdated = () => this.handleSettingsUpdated();

constructor(view: EditorView) {
Expand Down Expand Up @@ -177,6 +179,10 @@ function createDragHandleViewPlugin(_plugin: DragNDropPlugin) {
performDropAtPoint: (sourceBlock, clientX, clientY, pointerType) =>
this.orchestrator.performDropAtPoint(sourceBlock, clientX, clientY, pointerType ?? null),
onDragLifecycleEvent: (event) => this.orchestrator.emitDragLifecycle(event),
setHiddenRangesForSelection: (ranges, anchorHandle) =>
this.handleVisibility.setHiddenRangesForSelection(ranges, anchorHandle),
clearHiddenRangesForSelection: () =>
this.handleVisibility.clearHiddenRangesForSelection(),
});

this.semanticRefreshScheduler = new SemanticRefreshScheduler(this.view, {
Expand All @@ -189,6 +195,7 @@ function createDragHandleViewPlugin(_plugin: DragNDropPlugin) {
this.dragEventHandler.attach();
this.semanticRefreshScheduler.bindViewportScrollFallback();
document.addEventListener('pointermove', this.onDocumentPointerMove, { passive: true });
document.addEventListener('pointerdown', this.onDocumentPointerDown, { passive: true });
window.addEventListener('dnd:settings-updated', this.onSettingsUpdated);

// Pre-warm fence scan during idle to ensure code/math block boundaries are ready
Expand Down Expand Up @@ -219,6 +226,8 @@ function createDragHandleViewPlugin(_plugin: DragNDropPlugin) {
this.handleVisibility.setActiveVisibleHandle(null);
this.reResolveActiveHandle();
}
this.handleVisibility.reapplySelectionHighlight();
this.handleVisibility.reapplySelectionHandleVisibility();
return;
}

Expand All @@ -230,20 +239,28 @@ function createDragHandleViewPlugin(_plugin: DragNDropPlugin) {
this.refreshDecorationsAndEmbeds();
}

if (update.docChanged || update.geometryChanged) {
if (
update.docChanged
|| update.geometryChanged
|| update.selectionSet
|| this.dragEventHandler.hasCommittedSelection()
) {
this.dragEventHandler.refreshSelectionVisual();
}
const activeHandle2 = this.handleVisibility.getActiveHandle();
if (activeHandle2 && !activeHandle2.isConnected) {
this.handleVisibility.setActiveVisibleHandle(null);
this.reResolveActiveHandle();
}
this.handleVisibility.reapplySelectionHighlight();
this.handleVisibility.reapplySelectionHandleVisibility();
}

destroy(): void {
this.lineMapPrewarmer.clear();
this.semanticRefreshScheduler.destroy();
document.removeEventListener('pointermove', this.onDocumentPointerMove);
document.removeEventListener('pointerdown', this.onDocumentPointerDown);
window.removeEventListener('dnd:settings-updated', this.onSettingsUpdated);
this.handleVisibility.clearGrabbedLineNumbers();
this.handleVisibility.setActiveVisibleHandle(null);
Expand All @@ -264,6 +281,13 @@ function createDragHandleViewPlugin(_plugin: DragNDropPlugin) {
});
}

private handleDocumentPointerDown(e: PointerEvent): void {
// Clear selection highlight when clicking anywhere that's not a drag handle
if (!(e.target instanceof HTMLElement)) return;
if (e.target.closest(`.${DRAG_HANDLE_CLASS}`)) return;
this.handleVisibility.clearSelectionHighlight();
}

private handleDocumentPointerMove(e: PointerEvent): void {
this.lastPointerPos = { x: e.clientX, y: e.clientY };
if (document.body.classList.contains(MOBILE_GESTURE_LOCK_CLASS)) {
Expand Down
Loading
Loading