fix(editor): improve date positioning in task content

- Fix cancelled date insertion to appear after task content
- Improve content boundary detection for dates and metadata
- Handle wiki links and special characters in task content correctly
- Ensure dates are positioned before tags and dataview fields
This commit is contained in:
Quorafind 2025-09-27 08:17:34 +08:00
parent 8b48e6a25b
commit dab3ecb5d7
5 changed files with 230 additions and 135 deletions

View file

@ -3,6 +3,7 @@ import { SettingsIndexer } from "@/components/features/settings/core/SettingsInd
import { SearchResult } from "@/types/SettingsSearch";
import { t } from "@/translations/helper";
import { TaskProgressBarSettingTab } from "@/setting";
import { WorkspaceSettingsSelector } from "./WorkspaceSettingsSelector";
/**
*
@ -21,6 +22,7 @@ export class SettingsSearchComponent extends Component {
private debouncedSearch: (query: string) => void;
private isVisible = false;
private blurTimeoutId = 0;
private workspaceSelector: WorkspaceSettingsSelector | null = null;
constructor(
settingTab: TaskProgressBarSettingTab,
@ -46,8 +48,29 @@ export class SettingsSearchComponent extends Component {
*
*/
private createSearchUI(): void {
// 创建主容器
const mainContainer = this.containerEl.createDiv();
mainContainer.addClass("tg-settings-main-container");
// 创建头部栏包含workspace选择器和搜索框
const headerBar = mainContainer.createDiv();
headerBar.addClass("tg-settings-header-bar");
// 创建workspace选择器容器
const workspaceSelectorContainer = headerBar.createDiv();
workspaceSelectorContainer.addClass("tg-workspace-selector-container");
// 初始化workspace选择器
if (this.settingTab.plugin.workspaceManager) {
this.workspaceSelector = new WorkspaceSettingsSelector(
workspaceSelectorContainer,
this.settingTab.plugin,
this.settingTab
);
}
// 创建搜索容器
const searchContainer = this.containerEl.createDiv();
const searchContainer = headerBar.createDiv();
searchContainer.addClass("tg-settings-search-container");
// 创建搜索输入框容器
@ -75,8 +98,8 @@ export class SettingsSearchComponent extends Component {
setIcon(this.clearButton, "x");
this.clearButton.style.display = "none";
// 创建搜索结果容器
this.resultsContainerEl = searchContainer.createDiv();
// 创建搜索结果容器在header bar外面
this.resultsContainerEl = mainContainer.createDiv();
this.resultsContainerEl.addClass("tg-settings-search-results");
this.resultsContainerEl.style.display = "none";
this.resultsContainerEl.setAttribute("role", "listbox");

View file

@ -671,24 +671,94 @@ function findMetadataInsertPosition(
// Work with the full line text, don't extract block reference yet
const blockRef = detectBlockReference(lineText);
// Find the end of the task content, right after the task description
const taskMatch = lineText.match(
/^[\s|\t]*([-*+]|\d+\.)\s\[(.)\]\s*([^#\[📅🚀✅❌🛫▶️⏰🏁]*)/
);
// Find the task marker and status
const taskMatch = lineText.match(/^[\s|\t]*([-*+]|\d+\.)\s\[(.)\]\s*/);
if (!taskMatch) return blockRef ? blockRef.index : lineText.length;
// Start position is right after the task checkbox
let position = taskMatch[0].length;
// For cancelled date, we need special handling to insert after all metadata and start dates
// Find the actual end of task content by scanning through the text
// This handles content with special characters, links, etc.
let contentEnd = position;
let inLink = 0; // Track nested [[links]]
let inDataview = false; // Track [field:: value] metadata
const remainingText = lineText.slice(position);
for (let i = 0; i < remainingText.length; i++) {
const char = remainingText[i];
const nextChar = remainingText[i + 1];
const twoChars = char + (nextChar || '');
// Handle [[wiki links]] - they are part of content
if (twoChars === '[[') {
inLink++;
contentEnd = position + i + 2;
i++; // Skip next char
continue;
}
if (twoChars === ']]' && inLink > 0) {
inLink--;
contentEnd = position + i + 2;
i++; // Skip next char
continue;
}
// If we're inside a link, everything is content
if (inLink > 0) {
contentEnd = position + i + 1;
continue;
}
// Check for dataview metadata [field:: value]
if (char === '[' && !inDataview) {
const afterBracket = remainingText.slice(i + 1);
if (afterBracket.match(/^[a-zA-Z]+::/)) {
// This is dataview metadata, stop here
break;
}
}
// Check for tags (only if preceded by whitespace or at start)
if (char === '#') {
if (i === 0 || remainingText[i - 1] === ' ' || remainingText[i - 1] === '\t') {
// Check if this is actually a tag (followed by word characters)
const afterHash = remainingText.slice(i + 1);
if (afterHash.match(/^[\w-]+/)) {
// This is a tag, stop here
break;
}
}
}
// Check for date emojis (these are metadata markers)
const dateEmojis = ['📅', '🚀', '✅', '❌', '🛫', '▶️', '⏰', '🏁'];
if (dateEmojis.includes(char)) {
// Check if this is followed by a date pattern
const afterEmoji = remainingText.slice(i + 1);
if (afterEmoji.match(/^\s*\d{4}-\d{2}-\d{2}/)) {
// This is a date marker, stop here
break;
}
}
// Regular content character
contentEnd = position + i + 1;
}
position = contentEnd;
// Trim trailing whitespace
while (position > taskMatch[0].length && lineText[position - 1] === ' ') {
position--;
}
// For cancelled date, we need special handling to insert after start dates if present
if (dateType === "cancelled") {
const useDataviewFormat =
plugin.settings.preferMetadataFormat === "dataview";
// Find the last occurrence of either:
// 1. Start date marker (🛫 or [start::)
// 2. If no start date, find the end of all metadata
// Look for start date first
// Look for existing start date
let startDateFound = false;
if (useDataviewFormat) {
const startDateMatch = lineText.match(/\[start::[^\]]*\]/);
@ -717,9 +787,6 @@ function findMetadataInsertPosition(
);
startDateMatch = lineText.match(pattern);
if (startDateMatch) {
console.log(
`[AutoDateManager] Found start date with emoji ${emoji}`
);
break;
}
}
@ -728,47 +795,11 @@ function findMetadataInsertPosition(
if (startDateMatch && startDateMatch.index !== undefined) {
position = startDateMatch.index + startDateMatch[0].length;
startDateFound = true;
console.log(
`[AutoDateManager] Found start date at index ${startDateMatch.index}, length ${startDateMatch[0].length}, new position: ${position}`
);
}
}
console.log(
`[AutoDateManager] Start date found: ${startDateFound}, position: ${position}`
);
if (!startDateFound) {
// No start date found, find the end of all metadata
// This includes tags (#), dataview fields ([field::]), and other date markers
let lastMetadataEnd = position;
// Find all metadata occurrences
const metadataRegexes = [
/#[\w-]+/g, // Tags
/\[[a-zA-Z]+::[^\]]*\]/g, // Dataview fields
/[📅🚀✅❌🛫▶️⏰🏁]\s*\d{4}-\d{2}-\d{2}(?:\s+\d{2}:\d{2}(?::\d{2})?)?/g, // Date markers (including all common start emojis)
];
for (const regex of metadataRegexes) {
let match;
while ((match = regex.exec(lineText)) !== null) {
if (match.index >= position) {
const matchEnd = match.index + match[0].length;
if (matchEnd > lastMetadataEnd) {
lastMetadataEnd = matchEnd;
}
}
}
}
position = lastMetadataEnd;
}
// Ensure we have a space before the cancelled date
if (position > 0 && lineText[position - 1] !== " ") {
// No need to add space here, it will be added with the date
}
// If no start date found, position is already correct from initial parsing
// It points to the end of content before metadata
} else if (dateType === "completed") {
// For completed date, we want to go to the end of the line (before block reference)
// This is different from cancelled/start dates which go after content/metadata
@ -783,56 +814,9 @@ function findMetadataInsertPosition(
}
}
} else {
// For start date, find the end of main content before metadata
let contentEnd = position;
let inBrackets = false;
const chars = lineText.slice(position).split("");
for (let i = 0; i < chars.length; i++) {
const char = chars[i];
// Track if we're inside dataview brackets
if (char === "[" && !inBrackets) {
// Check if this is a dataview field like [due::...]
const remainingText = lineText.slice(position + i);
if (remainingText.match(/^\[[a-zA-Z]+::/)) {
// This is metadata, stop here
break;
}
inBrackets = true;
contentEnd = position + i + 1;
} else if (char === "]" && inBrackets) {
inBrackets = false;
contentEnd = position + i + 1;
} else if (!inBrackets) {
// Check for metadata markers when not inside brackets
if (
char === "#" ||
char === "📅" ||
char === "🚀" ||
char === "✅" ||
char === "❌" ||
char === "🛫" ||
char === "▶️" ||
char === "⏰" ||
char === "🏁"
) {
// This is metadata, stop here
break;
}
contentEnd = position + i + 1;
} else {
// Inside brackets, keep going
contentEnd = position + i + 1;
}
}
position = contentEnd;
// Trim any trailing whitespace from position
while (position > 0 && lineText[position - 1] === " ") {
position--;
}
// For start date, the position has already been calculated correctly
// in the initial content parsing above
// No additional processing needed
}
// Ensure position doesn't exceed the block reference position

View file

@ -19,6 +19,7 @@ import "./styles/setting-v2.css";
import "./styles/beta-warning.css";
import "./styles/settings-search.css";
import "./styles/settings-migration.css";
import "./styles/workspace-settings-selector.css";
import {
renderAboutSettingsTab,
renderBetaTestSettingsTab,
@ -45,7 +46,6 @@ import { renderMcpIntegrationSettingsTab } from "./components/features/settings/
import { IframeModal } from "@/components/ui/modals/IframeModal";
import { renderTaskTimerSettingTab } from "./components/features/settings/tabs/TaskTimerSettingsTab";
import { renderBasesSettingsTab } from "./components/features/settings/tabs/BasesSettingsTab";
import { WorkspaceData } from "./experimental/v2/types/workspace";
import { renderWorkspaceSettingsTab } from "@/components/features/settings/tabs/WorkspaceSettingTab";
export class TaskProgressBarSettingTab extends PluginSettingTab {
@ -321,6 +321,10 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
if (tab.id === "mcp-integration" && !Platform.isDesktopApp) {
return;
}
// Skip workspaces tab from main navigation (accessed via dropdown)
if (tab.id === "workspaces") {
return;
}
const category = tab.category || "core";
if (categories[category as keyof typeof categories]) {
categories[category as keyof typeof categories].tabs.push(tab);
@ -440,6 +444,18 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
(settingsHeader as unknown as HTMLElement).style.display =
"none";
}
// Special handling for workspaces tab - ensure it's still accessible via dropdown
if (tabId === "workspaces") {
// Make sure the workspace section is visible even though the tab is hidden
const workspaceSection = this.containerEl.querySelector(
'[data-tab-id="workspaces"]'
);
if (workspaceSection) {
(workspaceSection as unknown as HTMLElement).style.display = "block";
workspaceSection.addClass("settings-tab-section-active");
}
}
}
public openTab(tabId: string) {

View file

@ -1,11 +1,5 @@
/* Settings Search Component Styles */
/* 搜索容器 */
.tg-settings-search-container {
margin-bottom: var(--size-4-4);
position: relative;
}
/* 搜索输入框容器 - 使用 Obsidian 标准输入框样式 */
.tg-settings-search-input-container {
position: relative;
@ -209,10 +203,6 @@ input.tg-settings-search-input {
/* 响应式设计 */
@media (max-width: 768px) {
.tg-settings-search-container {
margin-bottom: var(--size-4-3);
}
.tg-settings-search-results {
max-height: 280px;
border-radius: var(--radius-s);
@ -258,13 +248,6 @@ input.tg-settings-search-input {
background-clip: content-box;
}
/* 确保搜索框在设置页面中的正确定位 */
.task-genius-settings .tg-settings-search-container {
order: -1; /* 确保搜索框显示在最前面 */
margin-top: 0;
margin-bottom: var(--size-4-4);
}
/* 与设置标签页容器的间距调整 */
.task-genius-settings .settings-tabs-categorized-container {
margin-top: var(--size-4-3);

File diff suppressed because one or more lines are too long