mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
feat: update minimal capture
This commit is contained in:
parent
c28eef166b
commit
e2a8b23d79
22 changed files with 15709 additions and 2195 deletions
262
src/__tests__/SuggestBackwardCompatibility.test.ts
Normal file
262
src/__tests__/SuggestBackwardCompatibility.test.ts
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
import { App, Editor, EditorPosition, EditorSuggestContext } from "obsidian";
|
||||
import { MinimalQuickCaptureSuggest } from "../components/MinimalQuickCaptureSuggest";
|
||||
import TaskProgressBarPlugin from "../index";
|
||||
|
||||
// Mock Obsidian modules
|
||||
jest.mock("obsidian", () => ({
|
||||
App: jest.fn(),
|
||||
Editor: jest.fn(),
|
||||
EditorPosition: jest.fn(),
|
||||
EditorSuggest: class {
|
||||
constructor() {}
|
||||
},
|
||||
setIcon: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock plugin
|
||||
const mockPlugin = {
|
||||
app: {
|
||||
workspace: {
|
||||
getLastOpenFiles: () => ["test1.md", "test2.md"],
|
||||
},
|
||||
metadataCache: {
|
||||
getTags: () => ({
|
||||
"#work": 5,
|
||||
"#personal": 3,
|
||||
}),
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
quickCapture: {
|
||||
minimalModeSettings: {
|
||||
suggestTrigger: "/",
|
||||
},
|
||||
},
|
||||
preferMetadataFormat: "tasks",
|
||||
},
|
||||
} as any as TaskProgressBarPlugin;
|
||||
|
||||
describe("Backward Compatibility Tests", () => {
|
||||
let suggest: MinimalQuickCaptureSuggest;
|
||||
let app: App;
|
||||
|
||||
beforeEach(() => {
|
||||
app = new App();
|
||||
suggest = new MinimalQuickCaptureSuggest(app, mockPlugin);
|
||||
});
|
||||
|
||||
test("should maintain original MinimalQuickCaptureSuggest interface", () => {
|
||||
// Test that all original methods exist
|
||||
expect(typeof suggest.setMinimalMode).toBe("function");
|
||||
expect(typeof suggest.onTrigger).toBe("function");
|
||||
expect(typeof suggest.getSuggestions).toBe("function");
|
||||
expect(typeof suggest.renderSuggestion).toBe("function");
|
||||
expect(typeof suggest.selectSuggestion).toBe("function");
|
||||
});
|
||||
|
||||
test("should handle legacy @ trigger mapping to * for target", () => {
|
||||
suggest.setMinimalMode(true);
|
||||
|
||||
const mockContext: EditorSuggestContext = {
|
||||
query: "@",
|
||||
start: { line: 0, ch: 0 },
|
||||
end: { line: 0, ch: 1 },
|
||||
editor: {} as Editor,
|
||||
};
|
||||
|
||||
const suggestions = suggest.getSuggestions(mockContext);
|
||||
|
||||
// Should return target suggestions when @ is used
|
||||
expect(suggestions.length).toBeGreaterThan(0);
|
||||
expect(suggestions.some(s => s.id.includes("target") || s.replacement === "*")).toBe(true);
|
||||
});
|
||||
|
||||
test("should provide fallback suggestions when new system returns empty", () => {
|
||||
suggest.setMinimalMode(true);
|
||||
|
||||
// Test with an unknown trigger that would return empty from new system
|
||||
const mockContext: EditorSuggestContext = {
|
||||
query: "unknown",
|
||||
start: { line: 0, ch: 0 },
|
||||
end: { line: 0, ch: 1 },
|
||||
editor: {} as Editor,
|
||||
};
|
||||
|
||||
const suggestions = suggest.getSuggestions(mockContext);
|
||||
|
||||
// Should return fallback suggestions
|
||||
expect(suggestions.length).toBe(4); // date, priority, target, tag
|
||||
expect(suggestions.map(s => s.id)).toEqual(["date", "priority", "target", "tag"]);
|
||||
});
|
||||
|
||||
test("should handle selectSuggestion with both new and legacy actions", () => {
|
||||
const mockEditor = {
|
||||
replaceRange: jest.fn(),
|
||||
setCursor: jest.fn(),
|
||||
} as any as Editor;
|
||||
|
||||
const mockCursor = { line: 0, ch: 1 };
|
||||
|
||||
// Mock the context
|
||||
(suggest as any).context = {
|
||||
editor: mockEditor,
|
||||
end: mockCursor,
|
||||
};
|
||||
|
||||
// Test with new system suggestion (has action)
|
||||
const newSuggestion = {
|
||||
id: "priority-high",
|
||||
label: "High Priority",
|
||||
icon: "arrow-up",
|
||||
description: "High priority task",
|
||||
replacement: "! ⏫",
|
||||
trigger: "!",
|
||||
action: jest.fn(),
|
||||
};
|
||||
|
||||
suggest.selectSuggestion(newSuggestion, {} as MouseEvent);
|
||||
|
||||
expect(mockEditor.replaceRange).toHaveBeenCalledWith("! ⏫", { line: 0, ch: 0 }, { line: 0, ch: 1 });
|
||||
expect(mockEditor.setCursor).toHaveBeenCalledWith({ line: 0, ch: 4 });
|
||||
expect(newSuggestion.action).toHaveBeenCalledWith(mockEditor, { line: 0, ch: 4 });
|
||||
});
|
||||
|
||||
test("should handle legacy modal-based actions", () => {
|
||||
const mockEditor = {
|
||||
replaceRange: jest.fn(),
|
||||
setCursor: jest.fn(),
|
||||
} as any as Editor;
|
||||
|
||||
const mockCursor = { line: 0, ch: 1 };
|
||||
|
||||
// Mock the context
|
||||
(suggest as any).context = {
|
||||
editor: mockEditor,
|
||||
end: mockCursor,
|
||||
};
|
||||
|
||||
// Mock the modal element and modal instance
|
||||
const mockModal = {
|
||||
showDatePickerAtCursor: jest.fn(),
|
||||
showPriorityMenuAtCursor: jest.fn(),
|
||||
showLocationMenuAtCursor: jest.fn(),
|
||||
showTagSelectorAtCursor: jest.fn(),
|
||||
};
|
||||
|
||||
const mockModalEl = {
|
||||
closest: jest.fn().mockReturnValue({
|
||||
__minimalQuickCaptureModal: mockModal,
|
||||
}),
|
||||
};
|
||||
|
||||
const mockEditorEl = {
|
||||
cm: {
|
||||
dom: mockModalEl,
|
||||
},
|
||||
coordsAtPos: jest.fn().mockReturnValue({ left: 100, top: 200 }),
|
||||
};
|
||||
|
||||
(mockEditor as any).cm = { dom: mockModalEl };
|
||||
(mockEditor as any).coordsAtPos = mockEditorEl.coordsAtPos;
|
||||
|
||||
// Test legacy date suggestion
|
||||
const dateSuggestion = {
|
||||
id: "date",
|
||||
label: "Date",
|
||||
icon: "calendar",
|
||||
description: "Add date",
|
||||
replacement: "~",
|
||||
};
|
||||
|
||||
suggest.selectSuggestion(dateSuggestion, {} as MouseEvent);
|
||||
|
||||
expect(mockEditor.replaceRange).toHaveBeenCalledWith("~", { line: 0, ch: 0 }, { line: 0, ch: 1 });
|
||||
expect(mockModal.showDatePickerAtCursor).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should maintain original trigger character behavior", () => {
|
||||
const mockEditor = {
|
||||
getLine: jest.fn().mockReturnValue("test /"),
|
||||
} as any as Editor;
|
||||
|
||||
const mockFile = {} as any;
|
||||
const mockCursor = { line: 0, ch: 6 };
|
||||
|
||||
// Mock minimal mode context
|
||||
(mockEditor as any).cm = {
|
||||
dom: {
|
||||
closest: jest.fn().mockReturnValue({}),
|
||||
},
|
||||
};
|
||||
|
||||
suggest.setMinimalMode(true);
|
||||
|
||||
const triggerInfo = suggest.onTrigger(mockCursor, mockEditor, mockFile);
|
||||
|
||||
expect(triggerInfo).toEqual({
|
||||
start: { line: 0, ch: 5 },
|
||||
end: { line: 0, ch: 6 },
|
||||
query: "/",
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle disabled state correctly", () => {
|
||||
const mockEditor = {} as Editor;
|
||||
const mockFile = {} as any;
|
||||
const mockCursor = { line: 0, ch: 1 };
|
||||
|
||||
// When not in minimal mode, should return null
|
||||
suggest.setMinimalMode(false);
|
||||
const triggerInfo = suggest.onTrigger(mockCursor, mockEditor, mockFile);
|
||||
|
||||
expect(triggerInfo).toBeNull();
|
||||
});
|
||||
|
||||
test("should render suggestions with correct DOM structure", () => {
|
||||
const mockEl = {
|
||||
addClass: jest.fn(),
|
||||
createDiv: jest.fn().mockReturnValue({
|
||||
createDiv: jest.fn(),
|
||||
}),
|
||||
} as any as HTMLElement;
|
||||
|
||||
const suggestion = {
|
||||
id: "test",
|
||||
label: "Test Label",
|
||||
icon: "star",
|
||||
description: "Test Description",
|
||||
replacement: "test",
|
||||
};
|
||||
|
||||
suggest.renderSuggestion(suggestion, mockEl);
|
||||
|
||||
expect(mockEl.addClass).toHaveBeenCalledWith("menu-item");
|
||||
expect(mockEl.addClass).toHaveBeenCalledWith("tappable");
|
||||
expect(mockEl.createDiv).toHaveBeenCalledWith("menu-item-icon");
|
||||
});
|
||||
|
||||
test("should integrate with new suggest system while maintaining compatibility", () => {
|
||||
suggest.setMinimalMode(true);
|
||||
|
||||
// Test all original trigger characters
|
||||
const triggerChars = ["!", "~", "#"];
|
||||
|
||||
for (const trigger of triggerChars) {
|
||||
const mockContext: EditorSuggestContext = {
|
||||
query: trigger,
|
||||
start: { line: 0, ch: 0 },
|
||||
end: { line: 0, ch: 1 },
|
||||
editor: {} as Editor,
|
||||
};
|
||||
|
||||
const suggestions = suggest.getSuggestions(mockContext);
|
||||
expect(suggestions.length).toBeGreaterThan(0);
|
||||
|
||||
// Should have suggestions that match the trigger
|
||||
const hasMatchingSuggestion = suggestions.some(s =>
|
||||
s.replacement.includes(trigger) || s.trigger === trigger
|
||||
);
|
||||
expect(hasMatchingSuggestion).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
243
src/__tests__/SuggestPerformance.test.ts
Normal file
243
src/__tests__/SuggestPerformance.test.ts
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
import { App, Editor, TFile } from "obsidian";
|
||||
import { SuggestManager, UniversalEditorSuggest } from "../components/suggest";
|
||||
import TaskProgressBarPlugin from "../index";
|
||||
import { getSuggestOptionsByTrigger } from "../components/suggest/SpecialCharacterSuggests";
|
||||
|
||||
// Mock Obsidian modules
|
||||
jest.mock("obsidian", () => ({
|
||||
App: jest.fn(),
|
||||
Editor: jest.fn(),
|
||||
TFile: jest.fn(),
|
||||
EditorSuggest: class {
|
||||
constructor() {}
|
||||
},
|
||||
setIcon: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock plugin with realistic data
|
||||
const mockPlugin = {
|
||||
app: {
|
||||
workspace: {
|
||||
getLastOpenFiles: () => [
|
||||
"file1.md",
|
||||
"file2.md",
|
||||
"file3.md",
|
||||
"file4.md",
|
||||
"file5.md",
|
||||
],
|
||||
},
|
||||
metadataCache: {
|
||||
getTags: () => ({
|
||||
"#work": 10,
|
||||
"#personal": 8,
|
||||
"#urgent": 5,
|
||||
"#important": 7,
|
||||
"#project": 12,
|
||||
"#meeting": 3,
|
||||
"#todo": 15,
|
||||
"#review": 4,
|
||||
}),
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
preferMetadataFormat: "tasks",
|
||||
},
|
||||
} as any as TaskProgressBarPlugin;
|
||||
|
||||
describe("Suggest Performance Tests", () => {
|
||||
let app: App;
|
||||
let manager: SuggestManager;
|
||||
|
||||
beforeEach(() => {
|
||||
app = new App();
|
||||
(app as any).workspace = {
|
||||
editorSuggest: {
|
||||
suggests: [],
|
||||
},
|
||||
};
|
||||
manager = new SuggestManager(app, mockPlugin);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
manager.cleanup();
|
||||
});
|
||||
|
||||
test("should handle rapid suggest creation and destruction", () => {
|
||||
const startTime = performance.now();
|
||||
|
||||
manager.startManaging();
|
||||
|
||||
// Create and destroy 100 suggests rapidly
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const suggest = manager.createUniversalSuggest(`test-${i}`);
|
||||
suggest.enable();
|
||||
manager.removeManagedSuggest(`universal-test-${i}`);
|
||||
}
|
||||
|
||||
manager.stopManaging();
|
||||
|
||||
const endTime = performance.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
// Should complete within reasonable time (adjust threshold as needed)
|
||||
expect(duration).toBeLessThan(1000); // 1 second
|
||||
expect(manager.getActiveSuggests().size).toBe(0);
|
||||
});
|
||||
|
||||
test("should efficiently generate suggestions for all trigger characters", () => {
|
||||
const triggerChars = ["!", "~", "*", "#"];
|
||||
const iterations = 1000;
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
for (const trigger of triggerChars) {
|
||||
const suggestions = getSuggestOptionsByTrigger(trigger, mockPlugin);
|
||||
expect(suggestions.length).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
|
||||
const endTime = performance.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
// Should generate suggestions efficiently
|
||||
expect(duration).toBeLessThan(500); // 500ms for 4000 operations
|
||||
|
||||
console.log(`Generated ${iterations * triggerChars.length} suggestions in ${duration}ms`);
|
||||
});
|
||||
|
||||
test("should handle large number of active suggests", () => {
|
||||
manager.startManaging();
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
// Create 50 active suggests
|
||||
const suggests: UniversalEditorSuggest[] = [];
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const suggest = manager.createUniversalSuggest(`bulk-test-${i}`);
|
||||
suggest.enable();
|
||||
suggests.push(suggest);
|
||||
}
|
||||
|
||||
expect(manager.getActiveSuggests().size).toBe(50);
|
||||
|
||||
// Cleanup all at once
|
||||
manager.removeAllManagedSuggests();
|
||||
|
||||
const endTime = performance.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
expect(duration).toBeLessThan(100); // Should be very fast
|
||||
expect(manager.getActiveSuggests().size).toBe(0);
|
||||
|
||||
console.log(`Managed 50 suggests in ${duration}ms`);
|
||||
});
|
||||
|
||||
test("should efficiently handle context filtering", () => {
|
||||
const mockEditor = {} as Editor;
|
||||
const mockFile = {} as TFile;
|
||||
|
||||
// Create context filters
|
||||
const filters = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const filter = (editor: Editor, file: TFile) => {
|
||||
// Simulate some filtering logic
|
||||
return editor === mockEditor && file === mockFile;
|
||||
};
|
||||
manager.addContextFilter(`filter-${i}`, filter);
|
||||
filters.push(filter);
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
// Test context filtering performance
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const suggest = manager.createUniversalSuggest(`context-test-${i % 10}`, {
|
||||
contextFilter: filters[i % filters.length],
|
||||
});
|
||||
// Simulate context check
|
||||
const config = suggest.getConfig();
|
||||
if (config.contextFilter) {
|
||||
config.contextFilter(mockEditor, mockFile);
|
||||
}
|
||||
}
|
||||
|
||||
const endTime = performance.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
expect(duration).toBeLessThan(200); // Should be reasonably fast
|
||||
|
||||
console.log(`Context filtering test completed in ${duration}ms`);
|
||||
|
||||
// Cleanup
|
||||
for (let i = 0; i < 100; i++) {
|
||||
manager.removeContextFilter(`filter-${i}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("should handle memory efficiently during suggest lifecycle", () => {
|
||||
const initialMemory = (performance as any).memory?.usedJSHeapSize || 0;
|
||||
|
||||
manager.startManaging();
|
||||
|
||||
// Create and destroy suggests in cycles
|
||||
for (let cycle = 0; cycle < 10; cycle++) {
|
||||
// Create 20 suggests
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const suggest = manager.createUniversalSuggest(`memory-test-${cycle}-${i}`);
|
||||
suggest.enable();
|
||||
}
|
||||
|
||||
// Remove all suggests
|
||||
manager.removeAllManagedSuggests();
|
||||
}
|
||||
|
||||
manager.stopManaging();
|
||||
|
||||
// Force garbage collection if available
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
|
||||
const finalMemory = (performance as any).memory?.usedJSHeapSize || 0;
|
||||
const memoryDiff = finalMemory - initialMemory;
|
||||
|
||||
// Memory usage should not grow significantly
|
||||
// This is a rough check - actual values depend on environment
|
||||
if (initialMemory > 0) {
|
||||
expect(memoryDiff).toBeLessThan(1024 * 1024); // Less than 1MB growth
|
||||
console.log(`Memory difference: ${memoryDiff} bytes`);
|
||||
}
|
||||
});
|
||||
|
||||
test("should maintain performance with workspace suggest array manipulation", () => {
|
||||
const mockSuggests = Array.from({ length: 100 }, (_, i) => ({ id: `mock-${i}` }));
|
||||
(app as any).workspace.editorSuggest.suggests = [...mockSuggests];
|
||||
|
||||
manager.startManaging();
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
// Add suggests to beginning of array (high priority)
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const suggest = manager.createUniversalSuggest(`priority-test-${i}`);
|
||||
suggest.enable();
|
||||
}
|
||||
|
||||
// Verify they were added to the beginning
|
||||
const workspaceSuggests = (app as any).workspace.editorSuggest.suggests;
|
||||
expect(workspaceSuggests.length).toBe(150); // 100 original + 50 new
|
||||
|
||||
// Remove all managed suggests
|
||||
manager.removeAllManagedSuggests();
|
||||
|
||||
const endTime = performance.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
// Should handle array manipulation efficiently
|
||||
expect(duration).toBeLessThan(50);
|
||||
expect(workspaceSuggests.length).toBe(100); // Back to original
|
||||
|
||||
console.log(`Array manipulation completed in ${duration}ms`);
|
||||
});
|
||||
});
|
||||
207
src/__tests__/UniversalSuggest.test.ts
Normal file
207
src/__tests__/UniversalSuggest.test.ts
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
import { App, Editor, EditorPosition, TFile } from "obsidian";
|
||||
import { UniversalEditorSuggest, SuggestManager } from "../components/suggest";
|
||||
import TaskProgressBarPlugin from "../index";
|
||||
import { getSuggestOptionsByTrigger } from "../components/suggest/SpecialCharacterSuggests";
|
||||
|
||||
// Mock Obsidian modules
|
||||
jest.mock("obsidian", () => ({
|
||||
App: jest.fn(),
|
||||
Editor: jest.fn(),
|
||||
EditorPosition: jest.fn(),
|
||||
TFile: jest.fn(),
|
||||
EditorSuggest: class {
|
||||
constructor() {}
|
||||
},
|
||||
setIcon: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock plugin
|
||||
const mockPlugin = {
|
||||
app: {} as App,
|
||||
settings: {
|
||||
preferMetadataFormat: "tasks",
|
||||
},
|
||||
} as TaskProgressBarPlugin;
|
||||
|
||||
describe("UniversalEditorSuggest", () => {
|
||||
let suggest: UniversalEditorSuggest;
|
||||
let app: App;
|
||||
|
||||
beforeEach(() => {
|
||||
app = new App();
|
||||
suggest = new UniversalEditorSuggest(app, mockPlugin, {
|
||||
triggerChars: ["!", "~", "*", "#"],
|
||||
});
|
||||
});
|
||||
|
||||
test("should initialize with correct trigger characters", () => {
|
||||
const config = suggest.getConfig();
|
||||
expect(config.triggerChars).toEqual(["!", "~", "*", "#"]);
|
||||
});
|
||||
|
||||
test("should enable and disable correctly", () => {
|
||||
suggest.enable();
|
||||
expect(suggest["isEnabled"]).toBe(true);
|
||||
|
||||
suggest.disable();
|
||||
expect(suggest["isEnabled"]).toBe(false);
|
||||
});
|
||||
|
||||
test("should add and remove suggest options", () => {
|
||||
const customOption = {
|
||||
id: "custom",
|
||||
label: "Custom",
|
||||
icon: "star",
|
||||
description: "Custom option",
|
||||
replacement: "%",
|
||||
trigger: "%",
|
||||
};
|
||||
|
||||
suggest.addSuggestOption(customOption);
|
||||
const config = suggest.getConfig();
|
||||
expect(config.triggerChars).toContain("%");
|
||||
|
||||
suggest.removeSuggestOption("custom");
|
||||
// Note: This test would need access to internal suggestOptions to verify removal
|
||||
});
|
||||
});
|
||||
|
||||
describe("SuggestManager", () => {
|
||||
let manager: SuggestManager;
|
||||
let app: App;
|
||||
|
||||
beforeEach(() => {
|
||||
app = new App();
|
||||
// Mock the workspace.editorSuggest.suggests array
|
||||
(app as any).workspace = {
|
||||
editorSuggest: {
|
||||
suggests: [],
|
||||
},
|
||||
};
|
||||
manager = new SuggestManager(app, mockPlugin);
|
||||
});
|
||||
|
||||
test("should start and stop managing correctly", () => {
|
||||
expect(manager.isCurrentlyManaging()).toBe(false);
|
||||
|
||||
manager.startManaging();
|
||||
expect(manager.isCurrentlyManaging()).toBe(true);
|
||||
|
||||
manager.stopManaging();
|
||||
expect(manager.isCurrentlyManaging()).toBe(false);
|
||||
});
|
||||
|
||||
test("should add suggests with priority", () => {
|
||||
const mockSuggest = {} as any;
|
||||
manager.startManaging();
|
||||
|
||||
manager.addSuggestWithPriority(mockSuggest, "test");
|
||||
|
||||
const activeSuggests = manager.getActiveSuggests();
|
||||
expect(activeSuggests.has("test")).toBe(true);
|
||||
expect(activeSuggests.get("test")).toBe(mockSuggest);
|
||||
});
|
||||
|
||||
test("should remove managed suggests", () => {
|
||||
const mockSuggest = {} as any;
|
||||
manager.startManaging();
|
||||
|
||||
manager.addSuggestWithPriority(mockSuggest, "test");
|
||||
expect(manager.getActiveSuggests().has("test")).toBe(true);
|
||||
|
||||
manager.removeManagedSuggest("test");
|
||||
expect(manager.getActiveSuggests().has("test")).toBe(false);
|
||||
});
|
||||
|
||||
test("should cleanup properly", () => {
|
||||
manager.startManaging();
|
||||
manager.addSuggestWithPriority({} as any, "test1");
|
||||
manager.addSuggestWithPriority({} as any, "test2");
|
||||
|
||||
expect(manager.getActiveSuggests().size).toBe(2);
|
||||
|
||||
manager.cleanup();
|
||||
expect(manager.isCurrentlyManaging()).toBe(false);
|
||||
expect(manager.getActiveSuggests().size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SpecialCharacterSuggests", () => {
|
||||
test("should return priority suggestions for ! trigger", () => {
|
||||
const suggestions = getSuggestOptionsByTrigger("!", mockPlugin);
|
||||
expect(suggestions.length).toBeGreaterThan(0);
|
||||
expect(suggestions[0].trigger).toBe("!");
|
||||
expect(suggestions.some(s => s.id.includes("priority"))).toBe(true);
|
||||
});
|
||||
|
||||
test("should return date suggestions for ~ trigger", () => {
|
||||
const suggestions = getSuggestOptionsByTrigger("~", mockPlugin);
|
||||
expect(suggestions.length).toBeGreaterThan(0);
|
||||
expect(suggestions[0].trigger).toBe("~");
|
||||
expect(suggestions.some(s => s.id.includes("date"))).toBe(true);
|
||||
});
|
||||
|
||||
test("should return target suggestions for * trigger", () => {
|
||||
const suggestions = getSuggestOptionsByTrigger("*", mockPlugin);
|
||||
expect(suggestions.length).toBeGreaterThan(0);
|
||||
expect(suggestions[0].trigger).toBe("*");
|
||||
expect(suggestions.some(s => s.id.includes("target"))).toBe(true);
|
||||
});
|
||||
|
||||
test("should return tag suggestions for # trigger", () => {
|
||||
const suggestions = getSuggestOptionsByTrigger("#", mockPlugin);
|
||||
expect(suggestions.length).toBeGreaterThan(0);
|
||||
expect(suggestions[0].trigger).toBe("#");
|
||||
expect(suggestions.some(s => s.id.includes("tag"))).toBe(true);
|
||||
});
|
||||
|
||||
test("should return empty array for unknown trigger", () => {
|
||||
const suggestions = getSuggestOptionsByTrigger("?", mockPlugin);
|
||||
expect(suggestions).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Integration Tests", () => {
|
||||
test("should create universal suggest for minimal modal context", () => {
|
||||
const app = new App();
|
||||
(app as any).workspace = {
|
||||
editorSuggest: {
|
||||
suggests: [],
|
||||
},
|
||||
};
|
||||
|
||||
const manager = new SuggestManager(app, mockPlugin);
|
||||
manager.startManaging();
|
||||
|
||||
const mockEditor = {} as Editor;
|
||||
const suggest = manager.enableForMinimalModal(mockEditor);
|
||||
|
||||
expect(suggest).toBeInstanceOf(UniversalEditorSuggest);
|
||||
expect(manager.getActiveSuggests().has("universal-minimal-modal")).toBe(true);
|
||||
|
||||
manager.cleanup();
|
||||
});
|
||||
|
||||
test("should handle context filters correctly", () => {
|
||||
const app = new App();
|
||||
(app as any).workspace = {
|
||||
editorSuggest: {
|
||||
suggests: [],
|
||||
},
|
||||
};
|
||||
|
||||
const manager = new SuggestManager(app, mockPlugin);
|
||||
|
||||
// Add custom context filter
|
||||
const testFilter = (editor: Editor, file: TFile) => true;
|
||||
manager.addContextFilter("test", testFilter);
|
||||
|
||||
const config = manager.getConfig();
|
||||
expect(config.contextFilters["test"]).toBe(testFilter);
|
||||
|
||||
// Remove context filter
|
||||
manager.removeContextFilter("test");
|
||||
const updatedConfig = manager.getConfig();
|
||||
expect(updatedConfig.contextFilters["test"]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
110
src/__tests__/taskMarkCleanup.test.ts
Normal file
110
src/__tests__/taskMarkCleanup.test.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { clearAllMarks } from "../components/MarkdownRenderer";
|
||||
|
||||
describe("Task Mark Cleanup", () => {
|
||||
describe("clearAllMarks function", () => {
|
||||
test("should remove priority marks", () => {
|
||||
const input = "Complete this task ! ⏫";
|
||||
const expected = "Complete this task";
|
||||
expect(clearAllMarks(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test("should remove emoji priority marks", () => {
|
||||
const input = "Important task 🔺";
|
||||
const expected = "Important task";
|
||||
expect(clearAllMarks(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test("should remove letter priority marks", () => {
|
||||
const input = "High priority task [#A]";
|
||||
const expected = "High priority task";
|
||||
expect(clearAllMarks(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test("should remove date marks", () => {
|
||||
const input = "Task with date 📅 2024-01-15";
|
||||
const expected = "Task with date";
|
||||
expect(clearAllMarks(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test("should remove multiple marks", () => {
|
||||
const input = "Complex task ! 📅 2024-01-15 ⏫ #tag";
|
||||
const expected = "Complex task";
|
||||
expect(clearAllMarks(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test("should preserve meaningful content", () => {
|
||||
const input = "Write documentation for the API";
|
||||
const expected = "Write documentation for the API";
|
||||
expect(clearAllMarks(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test("should handle empty content", () => {
|
||||
const input = "";
|
||||
const expected = "";
|
||||
expect(clearAllMarks(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test("should handle content with only marks", () => {
|
||||
const input = "! ⏫ 📅 2024-01-15";
|
||||
const expected = "";
|
||||
expect(clearAllMarks(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test("should preserve links and code", () => {
|
||||
const input = "Check [[Important Note]] and `code snippet` ! ⏫";
|
||||
const expected = "Check [[Important Note]] and `code snippet`";
|
||||
expect(clearAllMarks(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test("should handle mixed content", () => {
|
||||
const input =
|
||||
"Review [documentation](https://example.com) ! 📅 2024-01-15";
|
||||
const expected = "Review [documentation](https://example.com)";
|
||||
expect(clearAllMarks(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test("should remove tilde date prefix marks", () => {
|
||||
const input = "Complete task ~ 2024-01-15";
|
||||
const expected = "Complete task 2024-01-15";
|
||||
expect(clearAllMarks(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test("should remove target location marks", () => {
|
||||
const input = "Meeting target: office 📁";
|
||||
const expected = "Meeting office";
|
||||
expect(clearAllMarks(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test("should handle complex example from user", () => {
|
||||
const input = "今天要过去吃饭 #123-123-123 ~ 📅 2025-07-18";
|
||||
const expected = "今天要过去吃饭 #123-123-123 2025-07-18";
|
||||
expect(clearAllMarks(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Task line scenarios", () => {
|
||||
test("should handle task with priority mark in middle", () => {
|
||||
const input = "Complete this ! important task";
|
||||
const expected = "Complete this important task";
|
||||
expect(clearAllMarks(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test("should handle task with multiple priority marks", () => {
|
||||
const input = "Very ! important ⏫ task";
|
||||
const expected = "Very important task";
|
||||
expect(clearAllMarks(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test("should handle task with trailing marks", () => {
|
||||
const input = "Simple task !";
|
||||
const expected = "Simple task";
|
||||
expect(clearAllMarks(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test("should handle task with leading marks", () => {
|
||||
const input = "! Important task";
|
||||
const expected = "Important task";
|
||||
expect(clearAllMarks(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -290,13 +290,7 @@ export interface QuickCaptureSettings {
|
|||
// Minimal mode settings
|
||||
enableMinimalMode: boolean;
|
||||
minimalModeSettings: {
|
||||
showDateButton: boolean;
|
||||
showPriorityButton: boolean;
|
||||
showLocationButton: boolean;
|
||||
showTagButton: boolean;
|
||||
defaultLocation: "fixed" | "daily-note";
|
||||
autoAddTaskPrefix: boolean; // 自动添加 - [ ] 前缀
|
||||
suggestTrigger: string; // 触发字符,默认 "/"
|
||||
suggestTrigger: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -816,12 +810,6 @@ export const DEFAULT_SETTINGS: TaskProgressBarSettings = {
|
|||
},
|
||||
enableMinimalMode: false,
|
||||
minimalModeSettings: {
|
||||
showDateButton: true,
|
||||
showPriorityButton: true,
|
||||
showLocationButton: true,
|
||||
showTagButton: true,
|
||||
defaultLocation: "fixed",
|
||||
autoAddTaskPrefix: true,
|
||||
suggestTrigger: "/",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
moment,
|
||||
EditorPosition,
|
||||
Menu,
|
||||
Scope,
|
||||
setIcon,
|
||||
} from "obsidian";
|
||||
import {
|
||||
createEmbeddableMarkdownEditor,
|
||||
|
|
@ -18,6 +18,7 @@ import { t } from "../translations/helper";
|
|||
import { MinimalQuickCaptureSuggest } from "./MinimalQuickCaptureSuggest";
|
||||
import { DatePickerPopover } from "./date-picker/DatePickerPopover";
|
||||
import { TagSuggest } from "./AutoComplete";
|
||||
import { SuggestManager, UniversalEditorSuggest } from "./suggest";
|
||||
|
||||
interface TaskMetadata {
|
||||
startDate?: Date;
|
||||
|
|
@ -43,19 +44,24 @@ export class MinimalQuickCaptureModal extends Modal {
|
|||
private locationButton: HTMLButtonElement | null = null;
|
||||
private tagButton: HTMLButtonElement | null = null;
|
||||
|
||||
// Suggest instance
|
||||
// Suggest instances
|
||||
private minimalSuggest: MinimalQuickCaptureSuggest;
|
||||
private suggestManager: SuggestManager;
|
||||
private universalSuggest: UniversalEditorSuggest | null = null;
|
||||
|
||||
constructor(app: App, plugin: TaskProgressBarPlugin) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.minimalSuggest = plugin.minimalQuickCaptureSuggest;
|
||||
|
||||
// Initialize suggest manager
|
||||
this.suggestManager = new SuggestManager(app, plugin);
|
||||
|
||||
// Initialize default metadata with fallback
|
||||
const minimalSettings =
|
||||
this.plugin.settings.quickCapture.minimalModeSettings;
|
||||
this.taskMetadata.location =
|
||||
minimalSettings?.defaultLocation || "fixed";
|
||||
this.plugin.settings.quickCapture.targetType || "fixed";
|
||||
this.taskMetadata.targetFile = this.getTargetFile();
|
||||
}
|
||||
|
||||
|
|
@ -67,6 +73,9 @@ export class MinimalQuickCaptureModal extends Modal {
|
|||
// Store modal instance reference for suggest system
|
||||
(this.modalEl as any).__minimalQuickCaptureModal = this;
|
||||
|
||||
// Start managing suggests with high priority
|
||||
this.suggestManager.startManaging();
|
||||
|
||||
// Set up the suggest system
|
||||
if (this.minimalSuggest) {
|
||||
this.minimalSuggest.setMinimalMode(true);
|
||||
|
|
@ -75,11 +84,28 @@ export class MinimalQuickCaptureModal extends Modal {
|
|||
// Create the interface
|
||||
this.createMinimalInterface(contentEl);
|
||||
|
||||
// Register the suggest - EditorSuggest should be registered at plugin level, not here
|
||||
// We'll register it in the plugin's onload method instead
|
||||
// Enable universal suggest for minimal modal after editor is created
|
||||
setTimeout(() => {
|
||||
if (this.markdownEditor?.editor?.editor) {
|
||||
this.universalSuggest =
|
||||
this.suggestManager.enableForMinimalModal(
|
||||
this.markdownEditor.editor.editor
|
||||
);
|
||||
this.universalSuggest.enable();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
// Clean up universal suggest
|
||||
if (this.universalSuggest) {
|
||||
this.universalSuggest.disable();
|
||||
this.universalSuggest = null;
|
||||
}
|
||||
|
||||
// Stop managing suggests and restore original order
|
||||
this.suggestManager.stopManaging();
|
||||
|
||||
// Clean up suggest
|
||||
if (this.minimalSuggest) {
|
||||
this.minimalSuggest.setMinimalMode(false);
|
||||
|
|
@ -144,9 +170,6 @@ export class MinimalQuickCaptureModal extends Modal {
|
|||
|
||||
onChange: (update) => {
|
||||
this.capturedContent = this.markdownEditor?.value || "";
|
||||
|
||||
// Check if we should show the suggestion menu
|
||||
this.handleSuggestTrigger();
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
@ -163,70 +186,51 @@ export class MinimalQuickCaptureModal extends Modal {
|
|||
cls: "quick-actions-left",
|
||||
});
|
||||
|
||||
// Date button
|
||||
if (settings.showDateButton !== false) {
|
||||
this.dateButton = leftContainer.createEl("button", {
|
||||
cls: "quick-action-button",
|
||||
attr: { "aria-label": t("Set date") },
|
||||
});
|
||||
this.dateButton.innerHTML = "📅";
|
||||
this.dateButton.addEventListener("click", () =>
|
||||
this.showDatePicker()
|
||||
);
|
||||
this.updateButtonState(
|
||||
this.dateButton,
|
||||
!!this.taskMetadata.dueDate
|
||||
);
|
||||
}
|
||||
this.dateButton = leftContainer.createEl("button", {
|
||||
cls: ["quick-action-button", "clickable-icon"],
|
||||
attr: { "aria-label": t("Set date") },
|
||||
});
|
||||
setIcon(this.dateButton, "calendar");
|
||||
this.dateButton.addEventListener("click", () => this.showDatePicker());
|
||||
this.updateButtonState(this.dateButton, !!this.taskMetadata.dueDate);
|
||||
|
||||
// Priority button
|
||||
if (settings.showPriorityButton !== false) {
|
||||
this.priorityButton = leftContainer.createEl("button", {
|
||||
cls: "quick-action-button",
|
||||
attr: { "aria-label": t("Set priority") },
|
||||
});
|
||||
this.priorityButton.innerHTML = "!";
|
||||
this.priorityButton.addEventListener("click", () =>
|
||||
this.showPriorityMenu()
|
||||
);
|
||||
this.updateButtonState(
|
||||
this.priorityButton,
|
||||
!!this.taskMetadata.priority
|
||||
);
|
||||
}
|
||||
this.priorityButton = leftContainer.createEl("button", {
|
||||
cls: ["quick-action-button", "clickable-icon"],
|
||||
attr: { "aria-label": t("Set priority") },
|
||||
});
|
||||
setIcon(this.priorityButton, "zap");
|
||||
this.priorityButton.addEventListener("click", () =>
|
||||
this.showPriorityMenu()
|
||||
);
|
||||
this.updateButtonState(
|
||||
this.priorityButton,
|
||||
!!this.taskMetadata.priority
|
||||
);
|
||||
|
||||
// Location button
|
||||
if (settings.showLocationButton !== false) {
|
||||
this.locationButton = leftContainer.createEl("button", {
|
||||
cls: "quick-action-button",
|
||||
attr: { "aria-label": t("Set location") },
|
||||
});
|
||||
this.locationButton.innerHTML = "📁";
|
||||
this.locationButton.addEventListener("click", () =>
|
||||
this.showLocationMenu()
|
||||
);
|
||||
this.updateButtonState(
|
||||
this.locationButton,
|
||||
this.taskMetadata.location !==
|
||||
(settings.defaultLocation || "fixed")
|
||||
);
|
||||
}
|
||||
this.locationButton = leftContainer.createEl("button", {
|
||||
cls: ["quick-action-button", "clickable-icon"],
|
||||
attr: { "aria-label": t("Set location") },
|
||||
});
|
||||
setIcon(this.locationButton, "folder");
|
||||
this.locationButton.addEventListener("click", () =>
|
||||
this.showLocationMenu()
|
||||
);
|
||||
this.updateButtonState(
|
||||
this.locationButton,
|
||||
this.taskMetadata.location !==
|
||||
(this.plugin.settings.quickCapture.targetType || "fixed")
|
||||
);
|
||||
|
||||
// Tag button
|
||||
if (settings.showTagButton !== false) {
|
||||
this.tagButton = leftContainer.createEl("button", {
|
||||
cls: "quick-action-button",
|
||||
attr: { "aria-label": t("Add tags") },
|
||||
});
|
||||
this.tagButton.innerHTML = "#";
|
||||
this.tagButton.addEventListener("click", () =>
|
||||
this.showTagSelector()
|
||||
);
|
||||
this.updateButtonState(
|
||||
this.tagButton,
|
||||
!!(this.taskMetadata.tags && this.taskMetadata.tags.length > 0)
|
||||
);
|
||||
}
|
||||
this.tagButton = leftContainer.createEl("button", {
|
||||
cls: ["quick-action-button", "clickable-icon"],
|
||||
attr: { "aria-label": t("Add tags") },
|
||||
});
|
||||
setIcon(this.tagButton, "tag");
|
||||
this.tagButton.addEventListener("click", () => {});
|
||||
this.updateButtonState(
|
||||
this.tagButton,
|
||||
!!(this.taskMetadata.tags && this.taskMetadata.tags.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
private createMainButtons(container: HTMLElement) {
|
||||
|
|
@ -251,104 +255,15 @@ export class MinimalQuickCaptureModal extends Modal {
|
|||
}
|
||||
|
||||
/**
|
||||
* Handle suggestion trigger check
|
||||
* Show menu at specified coordinates
|
||||
*/
|
||||
private handleSuggestTrigger(): void {
|
||||
if (!this.markdownEditor || !this.minimalSuggest) return;
|
||||
|
||||
// Get current cursor position
|
||||
const cursor = (this.markdownEditor.editor.editor as any).getCursor();
|
||||
|
||||
// Check if we should show the suggestion menu
|
||||
if (
|
||||
this.minimalSuggest.shouldShowMenu(
|
||||
this.markdownEditor.editor.editor as any,
|
||||
cursor
|
||||
)
|
||||
) {
|
||||
// Add a small delay to ensure the character is fully processed
|
||||
setTimeout(() => {
|
||||
this.minimalSuggest.showMenuAtCursor(
|
||||
this.markdownEditor!.editor.editor as any,
|
||||
cursor
|
||||
);
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show menu with proper scope handling to prevent enter key from propagating
|
||||
*/
|
||||
private showMenuWithScope(menu: Menu, x: number, y: number): void {
|
||||
// Create a new scope for the menu
|
||||
const menuScope = new Scope();
|
||||
|
||||
// Override the Enter key to prevent it from propagating to the editor
|
||||
menuScope.register([], "Enter", (evt: KeyboardEvent) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
return false;
|
||||
});
|
||||
|
||||
// Also register Escape key to properly close menu
|
||||
menuScope.register([], "Escape", (evt: KeyboardEvent) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
menu.hide();
|
||||
return false;
|
||||
});
|
||||
|
||||
// Push the scope when menu is shown
|
||||
this.app.keymap.pushScope(menuScope);
|
||||
|
||||
// Show the menu
|
||||
private showMenuAtCoords(menu: Menu, x: number, y: number): void {
|
||||
menu.showAtMouseEvent(
|
||||
new MouseEvent("click", {
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
})
|
||||
);
|
||||
|
||||
// Pop the scope when menu is hidden
|
||||
// We need to hook into the menu's onHide event
|
||||
const originalOnHide = menu.onHide;
|
||||
menu.onHide = () => {
|
||||
this.app.keymap.popScope(menuScope);
|
||||
if (originalOnHide) {
|
||||
originalOnHide.call(menu);
|
||||
}
|
||||
};
|
||||
|
||||
// Add a stronger event listener directly to the menu DOM element
|
||||
// This is a backup method to ensure Enter key doesn't propagate
|
||||
const menuEl = (menu as any).dom;
|
||||
if (menuEl) {
|
||||
const handleKeydown = (evt: KeyboardEvent) => {
|
||||
if (evt.key === "Enter") {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
evt.stopImmediatePropagation();
|
||||
|
||||
// Find the selected item and trigger its click
|
||||
const selectedItem = menuEl.querySelector('.menu-item.selected');
|
||||
if (selectedItem) {
|
||||
(selectedItem as HTMLElement).click();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
menuEl.addEventListener('keydown', handleKeydown, true);
|
||||
|
||||
// Clean up the event listener when menu is hidden
|
||||
const originalHide = menu.onHide;
|
||||
menu.onHide = () => {
|
||||
menuEl.removeEventListener('keydown', handleKeydown, true);
|
||||
this.app.keymap.popScope(menuScope);
|
||||
if (originalHide) {
|
||||
originalHide.call(menu);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Methods called by MinimalQuickCaptureSuggest
|
||||
|
|
@ -381,7 +296,7 @@ export class MinimalQuickCaptureModal extends Modal {
|
|||
if (cursor && this.markdownEditor) {
|
||||
this.replaceAtCursor(
|
||||
cursor,
|
||||
`📅 ${this.formatDate(quickDate.date)}`
|
||||
this.formatDate(quickDate.date)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -400,9 +315,9 @@ export class MinimalQuickCaptureModal extends Modal {
|
|||
|
||||
// Show menu at cursor position if provided, otherwise at button
|
||||
if (coords) {
|
||||
this.showMenuWithScope(menu, coords.left, coords.top);
|
||||
this.showMenuAtCoords(menu, coords.left, coords.top);
|
||||
} else if (this.dateButton) {
|
||||
this.showMenuWithScope(
|
||||
this.showMenuAtCoords(
|
||||
menu,
|
||||
this.dateButton.offsetLeft,
|
||||
this.dateButton.offsetTop
|
||||
|
|
@ -442,9 +357,9 @@ export class MinimalQuickCaptureModal extends Modal {
|
|||
|
||||
// Show menu at cursor position if provided, otherwise at button
|
||||
if (coords) {
|
||||
this.showMenuWithScope(menu, coords.left, coords.top);
|
||||
this.showMenuAtCoords(menu, coords.left, coords.top);
|
||||
} else if (this.priorityButton) {
|
||||
this.showMenuWithScope(
|
||||
this.showMenuAtCoords(
|
||||
menu,
|
||||
this.priorityButton.offsetLeft,
|
||||
this.priorityButton.offsetTop
|
||||
|
|
@ -469,13 +384,13 @@ export class MinimalQuickCaptureModal extends Modal {
|
|||
this.updateButtonState(
|
||||
this.locationButton!,
|
||||
this.taskMetadata.location !==
|
||||
(this.plugin.settings.quickCapture.minimalModeSettings
|
||||
?.defaultLocation || "fixed")
|
||||
(this.plugin.settings.quickCapture.targetType ||
|
||||
"fixed")
|
||||
);
|
||||
|
||||
// If called from suggest, replace the 📁 with file icon
|
||||
// If called from suggest, replace the 📁 with file text
|
||||
if (cursor && this.markdownEditor) {
|
||||
this.replaceAtCursor(cursor, "📄");
|
||||
this.replaceAtCursor(cursor, t("Fixed location"));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -489,22 +404,22 @@ export class MinimalQuickCaptureModal extends Modal {
|
|||
this.updateButtonState(
|
||||
this.locationButton!,
|
||||
this.taskMetadata.location !==
|
||||
(this.plugin.settings.quickCapture.minimalModeSettings
|
||||
?.defaultLocation || "fixed")
|
||||
(this.plugin.settings.quickCapture?.targetType ||
|
||||
"fixed")
|
||||
);
|
||||
|
||||
// If called from suggest, replace the 📁 with calendar icon
|
||||
// If called from suggest, replace the 📁 with daily note text
|
||||
if (cursor && this.markdownEditor) {
|
||||
this.replaceAtCursor(cursor, "📅");
|
||||
this.replaceAtCursor(cursor, t("Daily note"));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Show menu at cursor position if provided, otherwise at button
|
||||
if (coords) {
|
||||
this.showMenuWithScope(menu, coords.left, coords.top);
|
||||
this.showMenuAtCoords(menu, coords.left, coords.top);
|
||||
} else if (this.locationButton) {
|
||||
this.showMenuWithScope(
|
||||
this.showMenuAtCoords(
|
||||
menu,
|
||||
this.locationButton.offsetLeft,
|
||||
this.locationButton.offsetTop
|
||||
|
|
@ -512,57 +427,7 @@ export class MinimalQuickCaptureModal extends Modal {
|
|||
}
|
||||
}
|
||||
|
||||
public showTagSelectorAtCursor(cursorCoords: any, cursor: EditorPosition) {
|
||||
this.showTagSelector(cursor, cursorCoords);
|
||||
}
|
||||
|
||||
public showTagSelector(cursor?: EditorPosition, coords?: any) {
|
||||
// Create a temporary input for tag selection
|
||||
const tagInput = document.createElement("input");
|
||||
tagInput.type = "text";
|
||||
tagInput.placeholder = t("Enter tags (comma separated)");
|
||||
tagInput.className = "quick-capture-tag-input";
|
||||
|
||||
// Position the input at cursor position if provided
|
||||
if (coords) {
|
||||
tagInput.style.position = "absolute";
|
||||
tagInput.style.left = `${coords.left}px`;
|
||||
tagInput.style.top = `${coords.top}px`;
|
||||
tagInput.style.zIndex = "1000";
|
||||
}
|
||||
|
||||
// Add to modal temporarily
|
||||
this.contentEl.appendChild(tagInput);
|
||||
|
||||
// Setup tag suggest
|
||||
new TagSuggest(this.app, tagInput, this.plugin);
|
||||
|
||||
// Focus and handle completion
|
||||
tagInput.focus();
|
||||
tagInput.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
const tags = tagInput.value
|
||||
.split(",")
|
||||
.map((t) => t.trim())
|
||||
.filter((t) => t);
|
||||
this.taskMetadata.tags = tags;
|
||||
this.updateButtonState(this.tagButton!, tags.length > 0);
|
||||
|
||||
// If called from suggest, replace the # with tags
|
||||
if (cursor && this.markdownEditor) {
|
||||
this.replaceAtCursor(
|
||||
cursor,
|
||||
tags.map((t) => `#${t}`).join(" ")
|
||||
);
|
||||
}
|
||||
|
||||
// Remove temp input
|
||||
this.contentEl.removeChild(tagInput);
|
||||
} else if (e.key === "Escape") {
|
||||
this.contentEl.removeChild(tagInput);
|
||||
}
|
||||
});
|
||||
}
|
||||
public showTagSelectorAtCursor(cursorCoords: any, cursor: EditorPosition) {}
|
||||
|
||||
private replaceAtCursor(cursor: EditorPosition, replacement: string) {
|
||||
if (!this.markdownEditor) return;
|
||||
|
|
@ -601,23 +466,51 @@ export class MinimalQuickCaptureModal extends Modal {
|
|||
private processMinimalContent(content: string): string {
|
||||
if (!content.trim()) return "";
|
||||
|
||||
const settings =
|
||||
this.plugin.settings.quickCapture.minimalModeSettings || {};
|
||||
const lines = content.split("\n");
|
||||
const processedLines = lines.map((line) => {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed && !trimmed.startsWith("- [")) {
|
||||
// Clean any temporary marks from user input before creating task
|
||||
const cleanedContent = this.cleanTemporaryMarks(trimmed);
|
||||
return `- [ ] ${cleanedContent}`;
|
||||
}
|
||||
return line;
|
||||
});
|
||||
return processedLines.join("\n");
|
||||
}
|
||||
|
||||
// Auto-add task prefix if enabled
|
||||
if (settings.autoAddTaskPrefix !== false) {
|
||||
const lines = content.split("\n");
|
||||
const processedLines = lines.map((line) => {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed && !trimmed.startsWith("- [")) {
|
||||
return `- [ ] ${trimmed}`;
|
||||
}
|
||||
return line;
|
||||
});
|
||||
return processedLines.join("\n");
|
||||
}
|
||||
/**
|
||||
* Clean temporary marks from user input that might conflict with formal metadata
|
||||
*/
|
||||
private cleanTemporaryMarks(content: string): string {
|
||||
let cleaned = content;
|
||||
|
||||
return content;
|
||||
// Remove standalone exclamation marks that users might type for priority
|
||||
cleaned = cleaned.replace(/\s*!\s*/g, " ");
|
||||
|
||||
// Remove standalone tilde marks that users might type for date
|
||||
cleaned = cleaned.replace(/\s*~\s*/g, " ");
|
||||
|
||||
// Remove standalone priority symbols that users might type
|
||||
cleaned = cleaned.replace(/\s*[🔺⏫🔼🔽⏬️]\s*/g, " ");
|
||||
|
||||
// Remove standalone date symbols that users might type
|
||||
cleaned = cleaned.replace(/\s*[📅🛫⏳✅➕❌]\s*/g, " ");
|
||||
|
||||
// Remove location/folder symbols that users might type
|
||||
cleaned = cleaned.replace(/\s*[📁🏠🏢🏪🏫🏬🏭🏯🏰]\s*/g, " ");
|
||||
|
||||
// Remove other metadata symbols that users might type
|
||||
cleaned = cleaned.replace(/\s*[🆔⛔🏁🔁]\s*/g, " ");
|
||||
|
||||
// Remove target/location prefix patterns (like @location, target:)
|
||||
cleaned = cleaned.replace(/\s*@\w*\s*/g, " ");
|
||||
cleaned = cleaned.replace(/\s*target:\s*/gi, " ");
|
||||
|
||||
// Clean up multiple spaces and trim
|
||||
cleaned = cleaned.replace(/\s+/g, " ").trim();
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
private addMetadataToContent(content: string): string {
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ import {
|
|||
EditorSuggestContext,
|
||||
EditorSuggestTriggerInfo,
|
||||
TFile,
|
||||
Menu,
|
||||
Scope,
|
||||
setIcon,
|
||||
} from "obsidian";
|
||||
import TaskProgressBarPlugin from "../index";
|
||||
import { t } from "../translations/helper";
|
||||
import { getSuggestOptionsByTrigger } from "./suggest/SpecialCharacterSuggests";
|
||||
|
||||
interface SuggestOption {
|
||||
id: string;
|
||||
|
|
@ -18,15 +18,16 @@ interface SuggestOption {
|
|||
icon: string;
|
||||
description: string;
|
||||
replacement: string;
|
||||
trigger?: string;
|
||||
action?: (editor: Editor, cursor: EditorPosition) => void;
|
||||
}
|
||||
|
||||
export class MinimalQuickCaptureSuggest {
|
||||
app: App;
|
||||
export class MinimalQuickCaptureSuggest extends EditorSuggest<SuggestOption> {
|
||||
plugin: TaskProgressBarPlugin;
|
||||
private isMinimalMode: boolean = false;
|
||||
|
||||
constructor(app: App, plugin: TaskProgressBarPlugin) {
|
||||
this.app = app;
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
|
|
@ -39,18 +40,22 @@ export class MinimalQuickCaptureSuggest {
|
|||
}
|
||||
|
||||
/**
|
||||
* Check if we should show the suggestion menu at the current cursor position
|
||||
* Get the trigger regex for the suggestion
|
||||
*/
|
||||
shouldShowMenu(editor: Editor, cursor: EditorPosition): boolean {
|
||||
onTrigger(
|
||||
cursor: EditorPosition,
|
||||
editor: Editor,
|
||||
file: TFile
|
||||
): EditorSuggestTriggerInfo | null {
|
||||
// Only trigger in minimal mode
|
||||
if (!this.isMinimalMode) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if we're in a minimal quick capture context
|
||||
const editorEl = (editor as any).cm?.dom as HTMLElement;
|
||||
if (!editorEl || !editorEl.closest(".quick-capture-modal.minimal")) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the current line
|
||||
|
|
@ -60,150 +65,148 @@ export class MinimalQuickCaptureSuggest {
|
|||
?.suggestTrigger || "/";
|
||||
|
||||
// Check if the cursor is right after the trigger character
|
||||
return cursor.ch > 0 && line.charAt(cursor.ch - 1) === triggerChar;
|
||||
if (cursor.ch > 0 && line.charAt(cursor.ch - 1) === triggerChar) {
|
||||
return {
|
||||
start: { line: cursor.line, ch: cursor.ch - 1 },
|
||||
end: cursor,
|
||||
query: triggerChar,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the suggestion menu at the cursor position
|
||||
* Get suggestions based on the trigger
|
||||
*/
|
||||
showMenuAtCursor(editor: Editor, cursor: EditorPosition): void {
|
||||
const settings =
|
||||
this.plugin.settings.quickCapture.minimalModeSettings || {};
|
||||
|
||||
// Get cursor position in screen coordinates for menu positioning
|
||||
const cursorCoords = (editor as any).coordsAtPos?.(cursor) || { left: 0, top: 0 };
|
||||
|
||||
// Create the menu
|
||||
const menu = new Menu();
|
||||
|
||||
// Add menu items based on settings
|
||||
if (settings.showDateButton !== false) {
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(`📅 ${t("Date")}`);
|
||||
item.setIcon("calendar");
|
||||
item.onClick(() => {
|
||||
this.handleSuggestionSelection(editor, cursor, "date", "~");
|
||||
});
|
||||
});
|
||||
getSuggestions(context: EditorSuggestContext): SuggestOption[] {
|
||||
// Map old @ to new * for target location
|
||||
const triggerChar = context.query === "@" ? "*" : context.query;
|
||||
|
||||
// Get suggestions from the new system
|
||||
const suggestions = getSuggestOptionsByTrigger(
|
||||
triggerChar,
|
||||
this.plugin
|
||||
);
|
||||
|
||||
// For backward compatibility, return simple trigger suggestions if no specific ones found
|
||||
if (suggestions.length === 0) {
|
||||
return [
|
||||
{
|
||||
id: "date",
|
||||
label: t("Date"),
|
||||
icon: "calendar",
|
||||
description: t("Add date"),
|
||||
replacement: "~",
|
||||
trigger: "~",
|
||||
},
|
||||
{
|
||||
id: "priority",
|
||||
label: t("Priority"),
|
||||
icon: "zap",
|
||||
description: t("Set priority"),
|
||||
replacement: "!",
|
||||
trigger: "!",
|
||||
},
|
||||
{
|
||||
id: "target",
|
||||
label: t("Target Location"),
|
||||
icon: "folder",
|
||||
description: t("Set target location"),
|
||||
replacement: "*",
|
||||
trigger: "*",
|
||||
},
|
||||
{
|
||||
id: "tag",
|
||||
label: t("Tag"),
|
||||
icon: "tag",
|
||||
description: t("Add tags"),
|
||||
replacement: "#",
|
||||
trigger: "#",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (settings.showPriorityButton !== false) {
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(`! ${t("Priority")}`);
|
||||
item.setIcon("zap");
|
||||
item.onClick(() => {
|
||||
this.handleSuggestionSelection(editor, cursor, "priority", "!");
|
||||
});
|
||||
});
|
||||
}
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
if (settings.showLocationButton !== false) {
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(`📁 ${t("Location")}`);
|
||||
item.setIcon("folder");
|
||||
item.onClick(() => {
|
||||
this.handleSuggestionSelection(editor, cursor, "location", "📁");
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Render suggestion using Obsidian Menu DOM structure
|
||||
*/
|
||||
renderSuggestion(suggestion: SuggestOption, el: HTMLElement): void {
|
||||
el.addClass("menu-item");
|
||||
el.addClass("tappable");
|
||||
|
||||
if (settings.showTagButton !== false) {
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(`# ${t("Tag")}`);
|
||||
item.setIcon("tag");
|
||||
item.onClick(() => {
|
||||
this.handleSuggestionSelection(editor, cursor, "tag", "#");
|
||||
});
|
||||
});
|
||||
}
|
||||
// Create icon element
|
||||
const iconEl = el.createDiv("menu-item-icon");
|
||||
setIcon(iconEl, suggestion.icon);
|
||||
|
||||
// Show the menu at cursor position with proper scope handling
|
||||
this.showMenuWithScope(menu, cursorCoords.left, cursorCoords.top);
|
||||
// Create title element
|
||||
const titleEl = el.createDiv("menu-item-title");
|
||||
titleEl.textContent = suggestion.label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle suggestion selection
|
||||
*/
|
||||
private handleSuggestionSelection(
|
||||
editor: Editor,
|
||||
cursor: EditorPosition,
|
||||
suggestionId: string,
|
||||
replacement: string
|
||||
selectSuggestion(
|
||||
suggestion: SuggestOption,
|
||||
evt: MouseEvent | KeyboardEvent
|
||||
): void {
|
||||
const triggerChar =
|
||||
this.plugin.settings.quickCapture.minimalModeSettings
|
||||
?.suggestTrigger || "/";
|
||||
const editor = this.context?.editor;
|
||||
const cursor = this.context?.end;
|
||||
|
||||
if (!editor || !cursor) return;
|
||||
|
||||
// Replace the trigger character with the replacement
|
||||
const startPos = { line: cursor.line, ch: cursor.ch - 1 };
|
||||
const endPos = cursor;
|
||||
|
||||
editor.replaceRange(replacement, startPos, endPos);
|
||||
|
||||
editor.replaceRange(suggestion.replacement, startPos, endPos);
|
||||
|
||||
// Move cursor to after the replacement
|
||||
const newCursor = {
|
||||
line: cursor.line,
|
||||
ch: cursor.ch - 1 + replacement.length,
|
||||
ch: cursor.ch - 1 + suggestion.replacement.length,
|
||||
};
|
||||
editor.setCursor(newCursor);
|
||||
|
||||
// Get the modal instance and trigger the appropriate action
|
||||
// Execute custom action if provided (new system)
|
||||
if (suggestion.action) {
|
||||
suggestion.action(editor, newCursor);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to legacy modal-based actions
|
||||
const editorEl = (editor as any).cm?.dom as HTMLElement;
|
||||
const modalEl = editorEl?.closest(".quick-capture-modal.minimal");
|
||||
const modal = (modalEl as any).__minimalQuickCaptureModal;
|
||||
|
||||
this.close();
|
||||
|
||||
if (!modal) return;
|
||||
|
||||
// Get cursor position for menu positioning
|
||||
const cursorCoords = (editor as any).coordsAtPos?.(newCursor) || { left: 0, top: 0 };
|
||||
const cursorCoords = (editor as any).coordsAtPos?.(newCursor) || {
|
||||
left: 0,
|
||||
top: 0,
|
||||
};
|
||||
|
||||
// Handle different suggestion types
|
||||
switch (suggestionId) {
|
||||
// Handle different suggestion types (legacy support)
|
||||
switch (suggestion.id) {
|
||||
case "date":
|
||||
modal.showDatePickerAtCursor(cursorCoords, newCursor);
|
||||
modal.showDatePickerAtCursor?.(cursorCoords, newCursor);
|
||||
break;
|
||||
case "priority":
|
||||
modal.showPriorityMenuAtCursor(cursorCoords, newCursor);
|
||||
modal.showPriorityMenuAtCursor?.(cursorCoords, newCursor);
|
||||
break;
|
||||
case "target":
|
||||
case "location":
|
||||
modal.showLocationMenuAtCursor(cursorCoords, newCursor);
|
||||
modal.showLocationMenuAtCursor?.(cursorCoords, newCursor);
|
||||
break;
|
||||
case "tag":
|
||||
modal.showTagSelectorAtCursor(cursorCoords, newCursor);
|
||||
modal.showTagSelectorAtCursor?.(cursorCoords, newCursor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show menu with proper scope handling to prevent enter key from propagating
|
||||
*/
|
||||
private showMenuWithScope(menu: Menu, x: number, y: number): void {
|
||||
// Create a new scope for the menu
|
||||
const menuScope = new Scope();
|
||||
|
||||
// Override the Enter key to prevent it from propagating to the editor
|
||||
menuScope.register([], "Enter", (evt: KeyboardEvent) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
return false;
|
||||
});
|
||||
|
||||
// Push the scope when menu is shown
|
||||
this.app.keymap.pushScope(menuScope);
|
||||
|
||||
// Show the menu
|
||||
menu.showAtMouseEvent(new MouseEvent("click", {
|
||||
clientX: x,
|
||||
clientY: y
|
||||
}));
|
||||
|
||||
// Pop the scope when menu is hidden
|
||||
const originalOnHide = menu.onHide;
|
||||
menu.onHide = () => {
|
||||
this.app.keymap.popScope(menuScope);
|
||||
if (originalOnHide) {
|
||||
originalOnHide.call(menu);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import {
|
|||
ParsedTimeResult,
|
||||
LineParseResult,
|
||||
} from "../utils/TimeParsingService";
|
||||
import { SuggestManager, UniversalEditorSuggest } from "./suggest";
|
||||
|
||||
interface TaskMetadata {
|
||||
startDate?: Date;
|
||||
|
|
@ -106,6 +107,10 @@ export class QuickCaptureModal extends Modal {
|
|||
// Debounce timer for real-time parsing
|
||||
private parseDebounceTimer?: number;
|
||||
|
||||
// Suggest management
|
||||
private suggestManager: SuggestManager;
|
||||
private universalSuggest: UniversalEditorSuggest | null = null;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
plugin: TaskProgressBarPlugin,
|
||||
|
|
@ -115,6 +120,9 @@ export class QuickCaptureModal extends Modal {
|
|||
super(app);
|
||||
this.plugin = plugin;
|
||||
|
||||
// Initialize suggest manager
|
||||
this.suggestManager = new SuggestManager(app, plugin);
|
||||
|
||||
// Initialize target file path based on target type
|
||||
if (this.plugin.settings.quickCapture.targetType === "daily-note") {
|
||||
const dateStr = moment().format(
|
||||
|
|
@ -150,12 +158,26 @@ export class QuickCaptureModal extends Modal {
|
|||
const { contentEl } = this;
|
||||
this.modalEl.toggleClass("quick-capture-modal", true);
|
||||
|
||||
// Start managing suggests with high priority
|
||||
this.suggestManager.startManaging();
|
||||
|
||||
if (this.useFullFeaturedMode) {
|
||||
this.modalEl.toggleClass(["quick-capture-modal", "full"], true);
|
||||
this.createFullFeaturedModal(contentEl);
|
||||
} else {
|
||||
this.createSimpleModal(contentEl);
|
||||
}
|
||||
|
||||
// Enable universal suggest after editor is created
|
||||
setTimeout(() => {
|
||||
if (this.markdownEditor?.editor?.editor) {
|
||||
this.universalSuggest =
|
||||
this.suggestManager.enableForQuickCaptureModal(
|
||||
this.markdownEditor.editor.editor
|
||||
);
|
||||
this.universalSuggest.enable();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
createSimpleModal(contentEl: HTMLElement) {
|
||||
|
|
@ -636,7 +658,9 @@ export class QuickCaptureModal extends Modal {
|
|||
// Don't add metadata to sub-tasks, but still clean time expressions
|
||||
// Preserve the original indentation from the original line
|
||||
const originalIndent = indentMatch[1];
|
||||
const cleanedContent = cleanedLine.trim();
|
||||
const cleanedContent = this.cleanTemporaryMarks(
|
||||
cleanedLine.trim()
|
||||
);
|
||||
processedLines.push(originalIndent + cleanedContent);
|
||||
} else if (isTaskOrList) {
|
||||
// If it's a task, add line-specific metadata
|
||||
|
|
@ -649,10 +673,12 @@ export class QuickCaptureModal extends Modal {
|
|||
const listPrefix = cleanedLine
|
||||
.trim()
|
||||
.match(/^(-|\d+\.|\*|\+)/)?.[0];
|
||||
const restOfLine = cleanedLine
|
||||
.trim()
|
||||
.substring(listPrefix?.length || 0)
|
||||
.trim();
|
||||
const restOfLine = this.cleanTemporaryMarks(
|
||||
cleanedLine
|
||||
.trim()
|
||||
.substring(listPrefix?.length || 0)
|
||||
.trim()
|
||||
);
|
||||
|
||||
// Use the specified status or default to empty checkbox
|
||||
const statusMark = this.taskMetadata.status || " ";
|
||||
|
|
@ -665,7 +691,8 @@ export class QuickCaptureModal extends Modal {
|
|||
// Not a list item or task, convert to task and add line-specific metadata
|
||||
// Use the specified status or default to empty checkbox
|
||||
const statusMark = this.taskMetadata.status || " ";
|
||||
const taskLine = `- [${statusMark}] ${cleanedLine}`;
|
||||
const cleanedContent = this.cleanTemporaryMarks(cleanedLine);
|
||||
const taskLine = `- [${statusMark}] ${cleanedContent}`;
|
||||
processedLines.push(
|
||||
this.addLineMetadataToTask(taskLine, lineParseResult)
|
||||
);
|
||||
|
|
@ -1012,6 +1039,40 @@ export class QuickCaptureModal extends Modal {
|
|||
this.taskMetadata.manuallySet[field] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean temporary marks from user input that might conflict with formal metadata
|
||||
*/
|
||||
private cleanTemporaryMarks(content: string): string {
|
||||
let cleaned = content;
|
||||
|
||||
// Remove standalone exclamation marks that users might type for priority
|
||||
cleaned = cleaned.replace(/\s*!\s*/g, " ");
|
||||
|
||||
// Remove standalone tilde marks that users might type for date
|
||||
cleaned = cleaned.replace(/\s*~\s*/g, " ");
|
||||
|
||||
// Remove standalone priority symbols that users might type
|
||||
cleaned = cleaned.replace(/\s*[🔺⏫🔼🔽⏬️]\s*/g, " ");
|
||||
|
||||
// Remove standalone date symbols that users might type
|
||||
cleaned = cleaned.replace(/\s*[📅🛫⏳✅➕❌]\s*/g, " ");
|
||||
|
||||
// Remove location/folder symbols that users might type
|
||||
cleaned = cleaned.replace(/\s*[📁🏠🏢🏪🏫🏬🏭🏯🏰]\s*/g, " ");
|
||||
|
||||
// Remove other metadata symbols that users might type
|
||||
cleaned = cleaned.replace(/\s*[🆔⛔🏁🔁]\s*/g, " ");
|
||||
|
||||
// Remove target/location prefix patterns (like @location, target:)
|
||||
cleaned = cleaned.replace(/\s*@\w*\s*/g, " ");
|
||||
cleaned = cleaned.replace(/\s*target:\s*/gi, " ");
|
||||
|
||||
// Clean up multiple spaces and trim
|
||||
cleaned = cleaned.replace(/\s+/g, " ").trim();
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform real-time parsing with debouncing
|
||||
*/
|
||||
|
|
@ -1115,6 +1176,15 @@ export class QuickCaptureModal extends Modal {
|
|||
onClose() {
|
||||
const { contentEl } = this;
|
||||
|
||||
// Clean up universal suggest
|
||||
if (this.universalSuggest) {
|
||||
this.universalSuggest.disable();
|
||||
this.universalSuggest = null;
|
||||
}
|
||||
|
||||
// Stop managing suggests and restore original order
|
||||
this.suggestManager.stopManaging();
|
||||
|
||||
// Clear debounce timer
|
||||
if (this.parseDebounceTimer) {
|
||||
clearTimeout(this.parseDebounceTimer);
|
||||
|
|
|
|||
|
|
@ -208,119 +208,6 @@ export function renderQuickCaptureSettingsTab(
|
|||
})
|
||||
);
|
||||
|
||||
// Minimal mode settings
|
||||
new Setting(containerEl).setName(t("Minimal Mode")).setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Enable minimal mode"))
|
||||
.setDesc(t("Enable simplified single-line quick capture with inline suggestions"))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settingTab.plugin.settings.quickCapture.enableMinimalMode)
|
||||
.onChange(async (value) => {
|
||||
settingTab.plugin.settings.quickCapture.enableMinimalMode = value;
|
||||
settingTab.applySettingsUpdate();
|
||||
// Refresh the settings display to show/hide minimal mode options
|
||||
setTimeout(() => {
|
||||
settingTab.display();
|
||||
}, 100);
|
||||
})
|
||||
);
|
||||
|
||||
if (!settingTab.plugin.settings.quickCapture.enableMinimalMode) return;
|
||||
|
||||
// Auto-add task prefix
|
||||
new Setting(containerEl)
|
||||
.setName(t("Auto-add task prefix"))
|
||||
.setDesc(t("Automatically add '- [ ]' prefix to captured content"))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settingTab.plugin.settings.quickCapture.minimalModeSettings.autoAddTaskPrefix)
|
||||
.onChange(async (value) => {
|
||||
settingTab.plugin.settings.quickCapture.minimalModeSettings.autoAddTaskPrefix = value;
|
||||
settingTab.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
// Suggest trigger character
|
||||
new Setting(containerEl)
|
||||
.setName(t("Suggest trigger character"))
|
||||
.setDesc(t("Character to trigger the suggestion menu"))
|
||||
.addText((text) =>
|
||||
text
|
||||
.setValue(settingTab.plugin.settings.quickCapture.minimalModeSettings.suggestTrigger)
|
||||
.onChange(async (value) => {
|
||||
settingTab.plugin.settings.quickCapture.minimalModeSettings.suggestTrigger = value || "/";
|
||||
settingTab.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
// Default location
|
||||
new Setting(containerEl)
|
||||
.setName(t("Default location"))
|
||||
.setDesc(t("Default location for capturing tasks"))
|
||||
.addDropdown((dropdown) =>
|
||||
dropdown
|
||||
.addOption("fixed", t("Fixed file"))
|
||||
.addOption("daily-note", t("Daily note"))
|
||||
.setValue(settingTab.plugin.settings.quickCapture.minimalModeSettings.defaultLocation)
|
||||
.onChange(async (value) => {
|
||||
settingTab.plugin.settings.quickCapture.minimalModeSettings.defaultLocation = value as "fixed" | "daily-note";
|
||||
settingTab.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
// Quick action buttons
|
||||
new Setting(containerEl).setName(t("Quick Action Buttons")).setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Show date button"))
|
||||
.setDesc(t("Show date selection button in minimal mode"))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settingTab.plugin.settings.quickCapture.minimalModeSettings.showDateButton)
|
||||
.onChange(async (value) => {
|
||||
settingTab.plugin.settings.quickCapture.minimalModeSettings.showDateButton = value;
|
||||
settingTab.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Show priority button"))
|
||||
.setDesc(t("Show priority selection button in minimal mode"))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settingTab.plugin.settings.quickCapture.minimalModeSettings.showPriorityButton)
|
||||
.onChange(async (value) => {
|
||||
settingTab.plugin.settings.quickCapture.minimalModeSettings.showPriorityButton = value;
|
||||
settingTab.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Show location button"))
|
||||
.setDesc(t("Show location selection button in minimal mode"))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settingTab.plugin.settings.quickCapture.minimalModeSettings.showLocationButton)
|
||||
.onChange(async (value) => {
|
||||
settingTab.plugin.settings.quickCapture.minimalModeSettings.showLocationButton = value;
|
||||
settingTab.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Show tag button"))
|
||||
.setDesc(t("Show tag selection button in minimal mode"))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settingTab.plugin.settings.quickCapture.minimalModeSettings.showTagButton)
|
||||
.onChange(async (value) => {
|
||||
settingTab.plugin.settings.quickCapture.minimalModeSettings.showTagButton = value;
|
||||
settingTab.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Append to file"))
|
||||
.setDesc(t("How to add captured content to the target location"))
|
||||
|
|
@ -336,4 +223,55 @@ export function renderQuickCaptureSettingsTab(
|
|||
settingTab.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
// Minimal mode settings
|
||||
new Setting(containerEl).setName(t("Minimal Mode")).setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Enable minimal mode"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Enable simplified single-line quick capture with inline suggestions"
|
||||
)
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(
|
||||
settingTab.plugin.settings.quickCapture.enableMinimalMode
|
||||
)
|
||||
.onChange(async (value) => {
|
||||
settingTab.plugin.settings.quickCapture.enableMinimalMode =
|
||||
value;
|
||||
settingTab.applySettingsUpdate();
|
||||
// Refresh the settings display to show/hide minimal mode options
|
||||
setTimeout(() => {
|
||||
settingTab.display();
|
||||
}, 100);
|
||||
})
|
||||
);
|
||||
|
||||
if (!settingTab.plugin.settings.quickCapture.enableMinimalMode) return;
|
||||
|
||||
if (!settingTab.plugin.settings.quickCapture.minimalModeSettings) {
|
||||
settingTab.plugin.settings.quickCapture.minimalModeSettings = {
|
||||
suggestTrigger: "/",
|
||||
};
|
||||
}
|
||||
|
||||
// Suggest trigger character
|
||||
new Setting(containerEl)
|
||||
.setName(t("Suggest trigger character"))
|
||||
.setDesc(t("Character to trigger the suggestion menu"))
|
||||
.addText((text) =>
|
||||
text
|
||||
.setValue(
|
||||
settingTab.plugin.settings.quickCapture.minimalModeSettings
|
||||
.suggestTrigger
|
||||
)
|
||||
.onChange(async (value) => {
|
||||
settingTab.plugin.settings.quickCapture.minimalModeSettings.suggestTrigger =
|
||||
value || "/";
|
||||
settingTab.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
|||
196
src/components/suggest/README.md
Normal file
196
src/components/suggest/README.md
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
# Universal Suggest System
|
||||
|
||||
This document describes the new Universal Suggest System that provides unified, high-priority suggest functionality for special characters in the Task Progress Bar plugin.
|
||||
|
||||
## Overview
|
||||
|
||||
The Universal Suggest System replaces the previous scattered suggest implementations with a unified architecture that:
|
||||
|
||||
- Provides dynamic priority management through workspace suggest array manipulation
|
||||
- Supports special characters: `!` (priority), `~` (date), `*` (target), `#` (tags)
|
||||
- Maintains backward compatibility with existing MinimalQuickCaptureSuggest
|
||||
- Offers extensible architecture for custom suggest implementations
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **UniversalEditorSuggest**: Base class extending EditorSuggest<T>
|
||||
2. **SuggestManager**: Manages dynamic suggest registration and priority
|
||||
3. **SpecialCharacterSuggests**: Provides specific suggestions for each trigger character
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Dynamic Priority**: Suggests are inserted at the beginning of `app.workspace.editorSuggest.suggests` array
|
||||
- **Context Filtering**: Supports context-specific suggest activation
|
||||
- **Lifecycle Management**: Automatic cleanup when modals close
|
||||
- **Extensible Options**: Easy to add new trigger characters and suggestions
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Setup
|
||||
|
||||
```typescript
|
||||
import { SuggestManager, UniversalEditorSuggest } from "./components/suggest";
|
||||
|
||||
// Initialize in plugin onload
|
||||
this.globalSuggestManager = new SuggestManager(this.app, this);
|
||||
|
||||
// In modal onOpen
|
||||
this.suggestManager.startManaging();
|
||||
const suggest = this.suggestManager.enableForMinimalModal(editor);
|
||||
suggest.enable();
|
||||
|
||||
// In modal onClose
|
||||
this.suggestManager.stopManaging();
|
||||
```
|
||||
|
||||
### Custom Suggest Options
|
||||
|
||||
```typescript
|
||||
// Add custom suggest option
|
||||
const customOption = {
|
||||
id: "custom-action",
|
||||
label: "Custom Action",
|
||||
icon: "star",
|
||||
description: "Perform custom action",
|
||||
replacement: "% ",
|
||||
trigger: "%",
|
||||
action: (editor, cursor) => {
|
||||
// Custom action implementation
|
||||
console.log("Custom action triggered");
|
||||
}
|
||||
};
|
||||
|
||||
suggest.addSuggestOption(customOption);
|
||||
```
|
||||
|
||||
### Context Filtering
|
||||
|
||||
```typescript
|
||||
// Add context-specific filter
|
||||
manager.addContextFilter("my-context", (editor, file) => {
|
||||
return file.path.includes("specific-folder");
|
||||
});
|
||||
|
||||
// Create suggest with context
|
||||
const suggest = manager.createUniversalSuggest("my-context");
|
||||
```
|
||||
|
||||
## Special Characters
|
||||
|
||||
### Priority (`!`)
|
||||
- `! 🔺` - Highest priority
|
||||
- `! ⏫` - High priority
|
||||
- `! 🔼` - Medium priority
|
||||
- `! 🔽` - Low priority
|
||||
- `! ⏬` - Lowest priority
|
||||
|
||||
### Date (`~`)
|
||||
- `~ YYYY-MM-DD` - Today's date
|
||||
- `~ YYYY-MM-DD` - Tomorrow's date
|
||||
- `~ ` - Open date picker
|
||||
- `~ ⏰ ` - Scheduled date format
|
||||
|
||||
### Target (`*`)
|
||||
- `* Inbox` - Save to inbox
|
||||
- `* Daily` - Save to daily note
|
||||
- `* Current` - Save to current file
|
||||
- `* ` - Open file picker
|
||||
- `* filename` - Recent files
|
||||
|
||||
### Tags (`#`)
|
||||
- `# important` - Important tag
|
||||
- `# urgent` - Urgent tag
|
||||
- `# work` - Work tag
|
||||
- `# personal` - Personal tag
|
||||
- `# ` - Open tag picker
|
||||
- `# existing-tag` - Existing vault tags
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From MinimalQuickCaptureSuggest
|
||||
|
||||
The existing MinimalQuickCaptureSuggest continues to work with enhanced functionality:
|
||||
|
||||
```typescript
|
||||
// Old usage (still works)
|
||||
const suggest = new MinimalQuickCaptureSuggest(app, plugin);
|
||||
suggest.setMinimalMode(true);
|
||||
|
||||
// New enhanced suggestions are automatically available
|
||||
// @ is automatically mapped to * for target location
|
||||
```
|
||||
|
||||
### From Custom Suggest Implementations
|
||||
|
||||
Replace custom EditorSuggest implementations:
|
||||
|
||||
```typescript
|
||||
// Old
|
||||
class CustomSuggest extends EditorSuggest<string> {
|
||||
// Custom implementation
|
||||
}
|
||||
|
||||
// New
|
||||
const suggest = manager.createUniversalSuggest("custom", {
|
||||
triggerChars: ["@"],
|
||||
contextFilter: (editor, file) => /* custom logic */
|
||||
});
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- Suggests are created and destroyed dynamically to minimize memory usage
|
||||
- Context filters are evaluated efficiently
|
||||
- Workspace suggest array manipulation is optimized for frequent operations
|
||||
- Cleanup is automatic when modals close
|
||||
|
||||
## Testing
|
||||
|
||||
Run the test suite to verify functionality:
|
||||
|
||||
```bash
|
||||
npm test -- UniversalSuggest.test.ts
|
||||
npm test -- SuggestPerformance.test.ts
|
||||
npm test -- SuggestBackwardCompatibility.test.ts
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Suggests not appearing**: Ensure `startManaging()` is called before creating suggests
|
||||
2. **Priority not working**: Check that suggests are added with `addSuggestWithPriority()`
|
||||
3. **Context filtering**: Verify context filters return boolean values
|
||||
4. **Memory leaks**: Always call `stopManaging()` or `cleanup()` when done
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable debug logging:
|
||||
|
||||
```typescript
|
||||
const manager = new SuggestManager(app, plugin, {
|
||||
enableDynamicPriority: true,
|
||||
// Add debug logging
|
||||
});
|
||||
|
||||
// Log current suggest order
|
||||
manager.debugLogSuggestOrder();
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Support for custom trigger character patterns
|
||||
- Advanced context filtering with regex support
|
||||
- Suggest caching for improved performance
|
||||
- Integration with other plugin suggest systems
|
||||
- Visual suggest priority indicators
|
||||
|
||||
## API Reference
|
||||
|
||||
See the TypeScript definitions in the source files for complete API documentation:
|
||||
|
||||
- `UniversalEditorSuggest.ts` - Core suggest implementation
|
||||
- `SuggestManager.ts` - Suggest lifecycle management
|
||||
- `SpecialCharacterSuggests.ts` - Built-in special character handlers
|
||||
341
src/components/suggest/SpecialCharacterSuggests.ts
Normal file
341
src/components/suggest/SpecialCharacterSuggests.ts
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
import { Editor, EditorPosition, Notice } from "obsidian";
|
||||
import TaskProgressBarPlugin from "../../index";
|
||||
import { SuggestOption } from "./UniversalEditorSuggest";
|
||||
import { t } from "../../translations/helper";
|
||||
|
||||
/**
|
||||
* Priority suggest options based on existing priority system
|
||||
*/
|
||||
export function createPrioritySuggestOptions(): SuggestOption[] {
|
||||
return [
|
||||
{
|
||||
id: "priority-highest",
|
||||
label: t("Highest Priority"),
|
||||
icon: "arrow-up",
|
||||
description: t("🔺 Highest priority task"),
|
||||
replacement: "! 🔺",
|
||||
trigger: "!",
|
||||
action: (editor: Editor, cursor: EditorPosition) => {
|
||||
// Priority is already inserted, no additional action needed
|
||||
new Notice(t("Highest priority set"));
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "priority-high",
|
||||
label: t("High Priority"),
|
||||
icon: "arrow-up",
|
||||
description: t("⏫ High priority task"),
|
||||
replacement: "! ⏫",
|
||||
trigger: "!",
|
||||
action: (editor: Editor, cursor: EditorPosition) => {
|
||||
new Notice(t("High priority set"));
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "priority-medium",
|
||||
label: t("Medium Priority"),
|
||||
icon: "minus",
|
||||
description: t("🔼 Medium priority task"),
|
||||
replacement: "! 🔼",
|
||||
trigger: "!",
|
||||
action: (editor: Editor, cursor: EditorPosition) => {
|
||||
new Notice(t("Medium priority set"));
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "priority-low",
|
||||
label: t("Low Priority"),
|
||||
icon: "arrow-down",
|
||||
description: t("🔽 Low priority task"),
|
||||
replacement: "! 🔽",
|
||||
trigger: "!",
|
||||
action: (editor: Editor, cursor: EditorPosition) => {
|
||||
new Notice(t("Low priority set"));
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "priority-lowest",
|
||||
label: t("Lowest Priority"),
|
||||
icon: "arrow-down",
|
||||
description: t("⏬ Lowest priority task"),
|
||||
replacement: "! ⏬",
|
||||
trigger: "!",
|
||||
action: (editor: Editor, cursor: EditorPosition) => {
|
||||
new Notice(t("Lowest priority set"));
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Date suggest options for common date patterns
|
||||
*/
|
||||
export function createDateSuggestOptions(): SuggestOption[] {
|
||||
const today = new Date();
|
||||
const tomorrow = new Date(today);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toISOString().split("T")[0];
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
id: "date-today",
|
||||
label: t("Today"),
|
||||
icon: "calendar-days",
|
||||
description: t("Set due date to today"),
|
||||
replacement: `~ ${formatDate(today)}`,
|
||||
trigger: "~",
|
||||
action: (editor: Editor, cursor: EditorPosition) => {
|
||||
new Notice(t("Due date set to today"));
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "date-tomorrow",
|
||||
label: t("Tomorrow"),
|
||||
icon: "calendar-plus",
|
||||
description: t("Set due date to tomorrow"),
|
||||
replacement: `~ ${formatDate(tomorrow)}`,
|
||||
trigger: "~",
|
||||
action: (editor: Editor, cursor: EditorPosition) => {
|
||||
new Notice(t("Due date set to tomorrow"));
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "date-picker",
|
||||
label: t("Pick Date"),
|
||||
icon: "calendar",
|
||||
description: t("Open date picker"),
|
||||
replacement: "~ ",
|
||||
trigger: "~",
|
||||
action: (editor: Editor, cursor: EditorPosition) => {
|
||||
// This will trigger the date picker modal
|
||||
// Implementation will be added when integrating with existing date picker
|
||||
new Notice(t("Date picker opened"));
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "date-scheduled",
|
||||
label: t("Scheduled Date"),
|
||||
icon: "calendar-clock",
|
||||
description: t("Set scheduled date"),
|
||||
replacement: "~ ⏰ ",
|
||||
trigger: "~",
|
||||
action: (editor: Editor, cursor: EditorPosition) => {
|
||||
new Notice(t("Scheduled date format added"));
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Target location suggest options
|
||||
*/
|
||||
export function createTargetSuggestOptions(
|
||||
plugin: TaskProgressBarPlugin
|
||||
): SuggestOption[] {
|
||||
const options: SuggestOption[] = [
|
||||
{
|
||||
id: "target-inbox",
|
||||
label: t("Inbox"),
|
||||
icon: "inbox",
|
||||
description: t("Save to inbox"),
|
||||
replacement: "* Inbox",
|
||||
trigger: "*",
|
||||
action: (editor: Editor, cursor: EditorPosition) => {
|
||||
new Notice(t("Target set to Inbox"));
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "target-daily",
|
||||
label: t("Daily Note"),
|
||||
icon: "calendar-days",
|
||||
description: t("Save to today's daily note"),
|
||||
replacement: "* Daily",
|
||||
trigger: "*",
|
||||
action: (editor: Editor, cursor: EditorPosition) => {
|
||||
new Notice(t("Target set to Daily Note"));
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "target-current",
|
||||
label: t("Current File"),
|
||||
icon: "file-text",
|
||||
description: t("Save to current file"),
|
||||
replacement: "* Current",
|
||||
trigger: "*",
|
||||
action: (editor: Editor, cursor: EditorPosition) => {
|
||||
new Notice(t("Target set to Current File"));
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "target-picker",
|
||||
label: t("Choose File"),
|
||||
icon: "folder-open",
|
||||
description: t("Open file picker"),
|
||||
replacement: "* ",
|
||||
trigger: "*",
|
||||
action: (editor: Editor, cursor: EditorPosition) => {
|
||||
// This will trigger the file picker modal
|
||||
new Notice(t("File picker opened"));
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Add recent files if available
|
||||
const recentFiles = plugin.app.workspace.getLastOpenFiles();
|
||||
recentFiles.slice(0, 3).forEach((filePath, index) => {
|
||||
const fileName =
|
||||
filePath.split("/").pop()?.replace(".md", "") || filePath;
|
||||
options.push({
|
||||
id: `target-recent-${index}`,
|
||||
label: fileName,
|
||||
icon: "file",
|
||||
description: t("Save to recent file"),
|
||||
replacement: `* ${fileName}`,
|
||||
trigger: "*",
|
||||
action: (editor: Editor, cursor: EditorPosition) => {
|
||||
new Notice(t("Target set to") + ` ${fileName}`);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag suggest options
|
||||
*/
|
||||
export function createTagSuggestOptions(
|
||||
plugin: TaskProgressBarPlugin
|
||||
): SuggestOption[] {
|
||||
const options: SuggestOption[] = [
|
||||
{
|
||||
id: "tag-important",
|
||||
label: t("Important"),
|
||||
icon: "star",
|
||||
description: t("Mark as important"),
|
||||
replacement: "# important",
|
||||
trigger: "#",
|
||||
action: (editor: Editor, cursor: EditorPosition) => {
|
||||
new Notice(t("Tagged as important"));
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "tag-urgent",
|
||||
label: t("Urgent"),
|
||||
icon: "zap",
|
||||
description: t("Mark as urgent"),
|
||||
replacement: "# urgent",
|
||||
trigger: "#",
|
||||
action: (editor: Editor, cursor: EditorPosition) => {
|
||||
new Notice(t("Tagged as urgent"));
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "tag-work",
|
||||
label: t("Work"),
|
||||
icon: "briefcase",
|
||||
description: t("Work related task"),
|
||||
replacement: "# work",
|
||||
trigger: "#",
|
||||
action: (editor: Editor, cursor: EditorPosition) => {
|
||||
new Notice(t("Tagged as work"));
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "tag-personal",
|
||||
label: t("Personal"),
|
||||
icon: "user",
|
||||
description: t("Personal task"),
|
||||
replacement: "# personal",
|
||||
trigger: "#",
|
||||
action: (editor: Editor, cursor: EditorPosition) => {
|
||||
new Notice(t("Tagged as personal"));
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "tag-picker",
|
||||
label: t("Choose Tag"),
|
||||
icon: "tag",
|
||||
description: t("Open tag picker"),
|
||||
replacement: "# ",
|
||||
trigger: "#",
|
||||
action: (editor: Editor, cursor: EditorPosition) => {
|
||||
// This will trigger the tag picker modal
|
||||
new Notice(t("Tag picker opened"));
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Add existing tags from vault
|
||||
try {
|
||||
const allTags = plugin.app.metadataCache.getTags();
|
||||
const tagNames = Object.keys(allTags)
|
||||
.map((tag) => tag.replace("#", ""))
|
||||
.filter(
|
||||
(tag) =>
|
||||
!["important", "urgent", "work", "personal"].includes(tag)
|
||||
)
|
||||
.slice(0, 5); // Limit to 5 most common tags
|
||||
|
||||
tagNames.forEach((tagName, index) => {
|
||||
options.push({
|
||||
id: `tag-existing-${index}`,
|
||||
label: `#${tagName}`,
|
||||
icon: "tag",
|
||||
description: t("Existing tag"),
|
||||
replacement: `# ${tagName}`,
|
||||
trigger: "#",
|
||||
action: (editor: Editor, cursor: EditorPosition) => {
|
||||
new Notice(t("Tagged with") + ` #${tagName}`);
|
||||
},
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("Failed to load existing tags:", error);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create all suggest options for a given plugin instance
|
||||
*/
|
||||
export function createAllSuggestOptions(plugin: TaskProgressBarPlugin): {
|
||||
priority: SuggestOption[];
|
||||
date: SuggestOption[];
|
||||
target: SuggestOption[];
|
||||
tag: SuggestOption[];
|
||||
} {
|
||||
return {
|
||||
priority: createPrioritySuggestOptions(),
|
||||
date: createDateSuggestOptions(),
|
||||
target: createTargetSuggestOptions(plugin),
|
||||
tag: createTagSuggestOptions(plugin),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get suggest options by trigger character
|
||||
*/
|
||||
export function getSuggestOptionsByTrigger(
|
||||
trigger: string,
|
||||
plugin: TaskProgressBarPlugin
|
||||
): SuggestOption[] {
|
||||
const allOptions = createAllSuggestOptions(plugin);
|
||||
|
||||
switch (trigger) {
|
||||
case "!":
|
||||
return allOptions.priority;
|
||||
case "~":
|
||||
return allOptions.date;
|
||||
case "*":
|
||||
return allOptions.target;
|
||||
case "#":
|
||||
return allOptions.tag;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
237
src/components/suggest/SuggestManager.ts
Normal file
237
src/components/suggest/SuggestManager.ts
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
import { App, Editor, EditorSuggest, TFile } from "obsidian";
|
||||
import TaskProgressBarPlugin from "../../index";
|
||||
import { UniversalEditorSuggest, UniversalSuggestConfig } from "./UniversalEditorSuggest";
|
||||
|
||||
export interface SuggestManagerConfig {
|
||||
enableDynamicPriority: boolean;
|
||||
defaultTriggerChars: string[];
|
||||
contextFilters: {
|
||||
[key: string]: (editor: Editor, file: TFile) => boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages dynamic suggest registration and priority in workspace
|
||||
*/
|
||||
export class SuggestManager {
|
||||
private app: App;
|
||||
private plugin: TaskProgressBarPlugin;
|
||||
private config: SuggestManagerConfig;
|
||||
private activeSuggests: Map<string, EditorSuggest<any>> = new Map();
|
||||
private originalSuggestsOrder: EditorSuggest<any>[] = [];
|
||||
private isManaging: boolean = false;
|
||||
|
||||
constructor(app: App, plugin: TaskProgressBarPlugin, config?: Partial<SuggestManagerConfig>) {
|
||||
this.app = app;
|
||||
this.plugin = plugin;
|
||||
this.config = {
|
||||
enableDynamicPriority: true,
|
||||
defaultTriggerChars: ["!", "~", "*", "#"],
|
||||
contextFilters: {},
|
||||
...config,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Start managing suggests with dynamic priority
|
||||
*/
|
||||
startManaging(): void {
|
||||
if (this.isManaging) return;
|
||||
|
||||
this.isManaging = true;
|
||||
// Store original order for restoration
|
||||
this.originalSuggestsOrder = [...this.app.workspace.editorSuggest.suggests];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop managing and restore original order
|
||||
*/
|
||||
stopManaging(): void {
|
||||
if (!this.isManaging) return;
|
||||
|
||||
// Remove all our managed suggests
|
||||
this.removeAllManagedSuggests();
|
||||
|
||||
// Restore original order if needed
|
||||
if (this.originalSuggestsOrder.length > 0) {
|
||||
this.app.workspace.editorSuggest.suggests = [...this.originalSuggestsOrder];
|
||||
}
|
||||
|
||||
this.isManaging = false;
|
||||
this.originalSuggestsOrder = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a suggest with high priority (insert at beginning)
|
||||
*/
|
||||
addSuggestWithPriority(suggest: EditorSuggest<any>, id: string): void {
|
||||
if (!this.isManaging) {
|
||||
console.warn("SuggestManager: Not managing, call startManaging() first");
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove if already exists
|
||||
this.removeManagedSuggest(id);
|
||||
|
||||
// Add to our tracking
|
||||
this.activeSuggests.set(id, suggest);
|
||||
|
||||
// Insert at the beginning for high priority
|
||||
this.app.workspace.editorSuggest.suggests.unshift(suggest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a managed suggest
|
||||
*/
|
||||
removeManagedSuggest(id: string): void {
|
||||
const suggest = this.activeSuggests.get(id);
|
||||
if (!suggest) return;
|
||||
|
||||
// Remove from workspace
|
||||
const index = this.app.workspace.editorSuggest.suggests.indexOf(suggest);
|
||||
if (index !== -1) {
|
||||
this.app.workspace.editorSuggest.suggests.splice(index, 1);
|
||||
}
|
||||
|
||||
// Remove from our tracking
|
||||
this.activeSuggests.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all managed suggests
|
||||
*/
|
||||
removeAllManagedSuggests(): void {
|
||||
for (const [id] of this.activeSuggests) {
|
||||
this.removeManagedSuggest(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and add a universal suggest for specific context
|
||||
*/
|
||||
createUniversalSuggest(
|
||||
contextId: string,
|
||||
config: Partial<UniversalSuggestConfig> = {}
|
||||
): UniversalEditorSuggest {
|
||||
const suggestConfig: UniversalSuggestConfig = {
|
||||
triggerChars: this.config.defaultTriggerChars,
|
||||
contextFilter: this.config.contextFilters[contextId],
|
||||
priority: 1,
|
||||
...config,
|
||||
};
|
||||
|
||||
const suggest = new UniversalEditorSuggest(this.app, this.plugin, suggestConfig);
|
||||
|
||||
// Add with priority
|
||||
this.addSuggestWithPriority(suggest, `universal-${contextId}`);
|
||||
|
||||
return suggest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable suggests for a specific editor context
|
||||
*/
|
||||
enableForEditor(editor: Editor, contextId: string = "default"): UniversalEditorSuggest {
|
||||
const suggest = this.createUniversalSuggest(contextId, {
|
||||
contextFilter: (ed, file) => ed === editor,
|
||||
});
|
||||
|
||||
suggest.enable();
|
||||
return suggest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable suggests for a specific context
|
||||
*/
|
||||
disableForContext(contextId: string): void {
|
||||
this.removeManagedSuggest(`universal-${contextId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable suggests for minimal quick capture modal
|
||||
*/
|
||||
enableForMinimalModal(editor: Editor): UniversalEditorSuggest {
|
||||
return this.createUniversalSuggest("minimal-modal", {
|
||||
contextFilter: (ed, file) => {
|
||||
// Check if we're in a minimal quick capture context
|
||||
const editorEl = (ed as any).cm?.dom as HTMLElement;
|
||||
return editorEl?.closest(".quick-capture-modal.minimal") !== null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable suggests for regular quick capture modal
|
||||
*/
|
||||
enableForQuickCaptureModal(editor: Editor): UniversalEditorSuggest {
|
||||
return this.createUniversalSuggest("quick-capture-modal", {
|
||||
contextFilter: (ed, file) => {
|
||||
// Check if we're in a quick capture context
|
||||
const editorEl = (ed as any).cm?.dom as HTMLElement;
|
||||
return editorEl?.closest(".quick-capture-modal") !== null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom context filter
|
||||
*/
|
||||
addContextFilter(
|
||||
contextId: string,
|
||||
filter: (editor: Editor, file: TFile) => boolean
|
||||
): void {
|
||||
this.config.contextFilters[contextId] = filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a context filter
|
||||
*/
|
||||
removeContextFilter(contextId: string): void {
|
||||
delete this.config.contextFilters[contextId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active suggests
|
||||
*/
|
||||
getActiveSuggests(): Map<string, EditorSuggest<any>> {
|
||||
return new Map(this.activeSuggests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if currently managing
|
||||
*/
|
||||
isCurrentlyManaging(): boolean {
|
||||
return this.isManaging;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current configuration
|
||||
*/
|
||||
getConfig(): SuggestManagerConfig {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update configuration
|
||||
*/
|
||||
updateConfig(newConfig: Partial<SuggestManagerConfig>): void {
|
||||
this.config = { ...this.config, ...newConfig };
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug: Log current suggest order
|
||||
*/
|
||||
debugLogSuggestOrder(): void {
|
||||
console.log("Current suggest order:", this.app.workspace.editorSuggest.suggests);
|
||||
console.log("Managed suggests:", Array.from(this.activeSuggests.keys()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup method for proper disposal
|
||||
*/
|
||||
cleanup(): void {
|
||||
this.stopManaging();
|
||||
this.activeSuggests.clear();
|
||||
this.config.contextFilters = {};
|
||||
}
|
||||
}
|
||||
209
src/components/suggest/UniversalEditorSuggest.ts
Normal file
209
src/components/suggest/UniversalEditorSuggest.ts
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
import {
|
||||
App,
|
||||
Editor,
|
||||
EditorPosition,
|
||||
EditorSuggest,
|
||||
EditorSuggestContext,
|
||||
EditorSuggestTriggerInfo,
|
||||
TFile,
|
||||
setIcon,
|
||||
} from "obsidian";
|
||||
import TaskProgressBarPlugin from "../../index";
|
||||
import { t } from "../../translations/helper";
|
||||
import { getSuggestOptionsByTrigger } from "./SpecialCharacterSuggests";
|
||||
|
||||
export interface SuggestOption {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
replacement: string;
|
||||
trigger: string;
|
||||
action?: (editor: Editor, cursor: EditorPosition) => void;
|
||||
}
|
||||
|
||||
export interface UniversalSuggestConfig {
|
||||
triggerChars: string[];
|
||||
contextFilter?: (editor: Editor, file: TFile) => boolean;
|
||||
priority?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal EditorSuggest that handles multiple special characters
|
||||
* and provides dynamic priority management
|
||||
*/
|
||||
export class UniversalEditorSuggest extends EditorSuggest<SuggestOption> {
|
||||
plugin: TaskProgressBarPlugin;
|
||||
private config: UniversalSuggestConfig;
|
||||
private suggestOptions: SuggestOption[] = [];
|
||||
private isEnabled: boolean = false;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
plugin: TaskProgressBarPlugin,
|
||||
config: UniversalSuggestConfig
|
||||
) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.config = config;
|
||||
this.initializeSuggestOptions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize suggest options for all supported special characters
|
||||
*/
|
||||
private initializeSuggestOptions(): void {
|
||||
// Initialize with empty array - options will be loaded dynamically
|
||||
this.suggestOptions = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable this suggest instance
|
||||
*/
|
||||
enable(): void {
|
||||
this.isEnabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable this suggest instance
|
||||
*/
|
||||
disable(): void {
|
||||
this.isEnabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if suggestion should be triggered
|
||||
*/
|
||||
onTrigger(
|
||||
cursor: EditorPosition,
|
||||
editor: Editor,
|
||||
file: TFile
|
||||
): EditorSuggestTriggerInfo | null {
|
||||
// Only trigger if enabled
|
||||
if (!this.isEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Apply context filter if provided
|
||||
if (
|
||||
this.config.contextFilter &&
|
||||
!this.config.contextFilter(editor, file)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the current line
|
||||
const line = editor.getLine(cursor.line);
|
||||
|
||||
// Check if cursor is right after any of our trigger characters
|
||||
if (cursor.ch > 0) {
|
||||
const charBefore = line.charAt(cursor.ch - 1);
|
||||
if (this.config.triggerChars.includes(charBefore)) {
|
||||
return {
|
||||
start: { line: cursor.line, ch: cursor.ch - 1 },
|
||||
end: cursor,
|
||||
query: charBefore,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get suggestions based on the trigger character
|
||||
*/
|
||||
getSuggestions(context: EditorSuggestContext): SuggestOption[] {
|
||||
const triggerChar = context.query;
|
||||
// Get dynamic suggestions based on trigger character
|
||||
return getSuggestOptionsByTrigger(triggerChar, this.plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render suggestion in the popup
|
||||
*/
|
||||
renderSuggestion(suggestion: SuggestOption, el: HTMLElement): void {
|
||||
const container = el.createDiv({ cls: "universal-suggest-item" });
|
||||
|
||||
// Icon
|
||||
const iconEl = container.createDiv({ cls: "universal-suggest-icon" });
|
||||
setIcon(iconEl, suggestion.icon);
|
||||
|
||||
// Content
|
||||
const contentEl = container.createDiv({
|
||||
cls: "universal-suggest-content",
|
||||
});
|
||||
contentEl.createDiv({
|
||||
cls: "universal-suggest-label",
|
||||
text: suggestion.label,
|
||||
});
|
||||
contentEl.createDiv({
|
||||
cls: "universal-suggest-description",
|
||||
text: suggestion.description,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle suggestion selection
|
||||
*/
|
||||
selectSuggestion(
|
||||
suggestion: SuggestOption,
|
||||
evt: MouseEvent | KeyboardEvent
|
||||
): void {
|
||||
const editor = this.context?.editor;
|
||||
const cursor = this.context?.end;
|
||||
|
||||
if (!editor || !cursor) return;
|
||||
|
||||
// Replace the trigger character with the replacement
|
||||
const startPos = { line: cursor.line, ch: cursor.ch - 1 };
|
||||
const endPos = cursor;
|
||||
|
||||
editor.replaceRange(suggestion.replacement, startPos, endPos);
|
||||
|
||||
// Move cursor to after the replacement
|
||||
const newCursor = {
|
||||
line: cursor.line,
|
||||
ch: cursor.ch - 1 + suggestion.replacement.length,
|
||||
};
|
||||
editor.setCursor(newCursor);
|
||||
|
||||
// Execute custom action if provided
|
||||
if (suggestion.action) {
|
||||
suggestion.action(editor, newCursor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom suggest option
|
||||
*/
|
||||
addSuggestOption(option: SuggestOption): void {
|
||||
this.suggestOptions.push(option);
|
||||
if (!this.config.triggerChars.includes(option.trigger)) {
|
||||
this.config.triggerChars.push(option.trigger);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a suggest option by id
|
||||
*/
|
||||
removeSuggestOption(id: string): void {
|
||||
this.suggestOptions = this.suggestOptions.filter(
|
||||
(option) => option.id !== id
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current configuration
|
||||
*/
|
||||
getConfig(): UniversalSuggestConfig {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update configuration
|
||||
*/
|
||||
updateConfig(newConfig: Partial<UniversalSuggestConfig>): void {
|
||||
this.config = { ...this.config, ...newConfig };
|
||||
}
|
||||
}
|
||||
14
src/components/suggest/index.ts
Normal file
14
src/components/suggest/index.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export {
|
||||
UniversalEditorSuggest,
|
||||
type SuggestOption,
|
||||
type UniversalSuggestConfig,
|
||||
} from "./UniversalEditorSuggest";
|
||||
export { SuggestManager, type SuggestManagerConfig } from "./SuggestManager";
|
||||
export {
|
||||
createPrioritySuggestOptions,
|
||||
createDateSuggestOptions,
|
||||
createTargetSuggestOptions,
|
||||
createTagSuggestOptions,
|
||||
createAllSuggestOptions,
|
||||
getSuggestOptionsByTrigger,
|
||||
} from "./SpecialCharacterSuggests";
|
||||
142
src/editor-ext/taskMarkCleanup.ts
Normal file
142
src/editor-ext/taskMarkCleanup.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import { EditorView } from "@codemirror/view";
|
||||
import { Extension, Transaction } from "@codemirror/state";
|
||||
import { clearAllMarks } from "../components/MarkdownRenderer";
|
||||
|
||||
/**
|
||||
* Extension to handle cleanup of task marks when text is selected and deleted
|
||||
* This ensures that when users select text containing task metadata (like priority marks)
|
||||
* and delete it, the marks are properly cleaned up
|
||||
*/
|
||||
export function taskMarkCleanupExtension(): Extension {
|
||||
return EditorView.updateListener.of((update) => {
|
||||
// Only process transactions that have changes
|
||||
if (!update.docChanged) return;
|
||||
|
||||
// Check if this is a user deletion operation
|
||||
const tr = update.transactions[0];
|
||||
if (!tr || !isUserDeletion(tr)) return;
|
||||
|
||||
// Process each change to see if we need to clean up marks
|
||||
tr.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
|
||||
// Only handle deletions (where text was removed)
|
||||
if (fromA >= toA) return;
|
||||
|
||||
const deletedText = tr.startState.doc.sliceString(fromA, toA);
|
||||
const insertedText = inserted.toString();
|
||||
|
||||
// Check if the deleted text contains task marks
|
||||
if (containsTaskMarks(deletedText)) {
|
||||
// Get the line containing the change
|
||||
const line = update.state.doc.lineAt(fromB);
|
||||
const lineText = line.text;
|
||||
|
||||
// Check if this is a task line
|
||||
if (isTaskLine(lineText)) {
|
||||
// Clean the line of any orphaned marks
|
||||
const cleanedLine = cleanOrphanedMarks(lineText);
|
||||
|
||||
if (cleanedLine !== lineText) {
|
||||
// Apply the cleanup
|
||||
update.view.dispatch({
|
||||
changes: {
|
||||
from: line.from,
|
||||
to: line.to,
|
||||
insert: cleanedLine
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a transaction represents a user deletion operation
|
||||
*/
|
||||
function isUserDeletion(tr: Transaction): boolean {
|
||||
// Check if this is a user input event
|
||||
if (!tr.isUserEvent("input.delete") && !tr.isUserEvent("input.deleteBackward")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if there are actual deletions
|
||||
let hasDeletions = false;
|
||||
tr.changes.iterChanges((fromA, toA) => {
|
||||
if (fromA < toA) {
|
||||
hasDeletions = true;
|
||||
}
|
||||
});
|
||||
|
||||
return hasDeletions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if text contains task marks that might need cleanup
|
||||
*/
|
||||
function containsTaskMarks(text: string): boolean {
|
||||
// Check for priority marks
|
||||
const priorityRegex = /(?:🔺|⏫|🔼|🔽|⏬️|\[#[A-C]\]|!)/;
|
||||
if (priorityRegex.test(text)) return true;
|
||||
|
||||
// Check for date marks
|
||||
const dateRegex = /(?:📅|🛫|⏳|✅|➕|❌)/;
|
||||
if (dateRegex.test(text)) return true;
|
||||
|
||||
// Check for other metadata marks
|
||||
const metadataRegex = /(?:🆔|⛔|🏁|🔁|@|#)/;
|
||||
if (metadataRegex.test(text)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a line is a task line
|
||||
*/
|
||||
function isTaskLine(line: string): boolean {
|
||||
const taskRegex = /^\s*[-*+]\s*\[[^\]]*\]/;
|
||||
return taskRegex.test(line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean orphaned marks from a task line
|
||||
* This removes marks that are no longer properly associated with content
|
||||
*/
|
||||
function cleanOrphanedMarks(line: string): string {
|
||||
// First, extract the task marker part
|
||||
const taskMarkerMatch = line.match(/^(\s*[-*+]\s*\[[^\]]*\]\s*)/);
|
||||
if (!taskMarkerMatch) return line;
|
||||
|
||||
const taskMarker = taskMarkerMatch[1];
|
||||
const content = line.substring(taskMarker.length);
|
||||
|
||||
// Use the existing clearAllMarks function to clean the content
|
||||
const cleanedContent = clearAllMarks(content);
|
||||
|
||||
// If the content is now empty or just whitespace, remove orphaned marks
|
||||
if (!cleanedContent.trim()) {
|
||||
// Remove any trailing marks that are now orphaned
|
||||
const cleanedLine = taskMarker.trim();
|
||||
return cleanedLine;
|
||||
}
|
||||
|
||||
// Reconstruct the line with cleaned content
|
||||
return taskMarker + cleanedContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if marks in the line are orphaned (not properly associated with content)
|
||||
*/
|
||||
function hasOrphanedMarks(line: string): boolean {
|
||||
// Extract content after task marker
|
||||
const taskMarkerMatch = line.match(/^\s*[-*+]\s*\[[^\]]*\]\s*(.*)/);
|
||||
if (!taskMarkerMatch) return false;
|
||||
|
||||
const content = taskMarkerMatch[1];
|
||||
|
||||
// Check if there are marks but no meaningful content
|
||||
const hasMarks = containsTaskMarks(content);
|
||||
const hasContent = content.replace(/[🔺⏫🔼🔽⏬️📅🛫⏳✅➕❌🆔⛔🏁🔁@#!\[\]]/g, '').trim().length > 0;
|
||||
|
||||
return hasMarks && !hasContent;
|
||||
}
|
||||
59
src/index.ts
59
src/index.ts
|
|
@ -67,6 +67,7 @@ import { Task } from "./types/task";
|
|||
import { QuickCaptureModal } from "./components/QuickCaptureModal";
|
||||
import { MinimalQuickCaptureModal } from "./components/MinimalQuickCaptureModal";
|
||||
import { MinimalQuickCaptureSuggest } from "./components/MinimalQuickCaptureSuggest";
|
||||
import { SuggestManager } from "./components/suggest";
|
||||
import { MarkdownView } from "obsidian";
|
||||
import { Notice } from "obsidian";
|
||||
import { t } from "./translations/helper";
|
||||
|
|
@ -92,6 +93,7 @@ import { monitorTaskCompletedExtension } from "./editor-ext/monitorTaskCompleted
|
|||
import { sortTasksInDocument } from "./commands/sortTaskCommands";
|
||||
import { taskGutterExtension } from "./editor-ext/TaskGutterHandler";
|
||||
import { autoDateManagerExtension } from "./editor-ext/autoDateManager";
|
||||
import { taskMarkCleanupExtension } from "./editor-ext/taskMarkCleanup";
|
||||
import { ViewManager } from "./pages/ViewManager";
|
||||
import { IcsManager } from "./utils/ics/IcsManager";
|
||||
import { VersionManager } from "./utils/VersionManager";
|
||||
|
|
@ -184,10 +186,13 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
|
||||
// ICS manager instance
|
||||
icsManager: IcsManager;
|
||||
|
||||
|
||||
// Minimal quick capture suggest
|
||||
minimalQuickCaptureSuggest: MinimalQuickCaptureSuggest;
|
||||
|
||||
// Global suggest manager
|
||||
globalSuggestManager: SuggestManager;
|
||||
|
||||
// Version manager instance
|
||||
versionManager: VersionManager;
|
||||
|
||||
|
|
@ -218,6 +223,9 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
this.versionManager = new VersionManager(this.app, this);
|
||||
this.addChild(this.versionManager);
|
||||
|
||||
// Initialize global suggest manager
|
||||
this.globalSuggestManager = new SuggestManager(this.app, this);
|
||||
|
||||
// Initialize rebuild progress manager
|
||||
this.rebuildProgressManager = new RebuildProgressManager();
|
||||
|
||||
|
|
@ -1025,10 +1033,14 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
quickCaptureExtension(this.app, this),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
// Initialize minimal quick capture suggest
|
||||
if (this.settings.quickCapture.enableMinimalMode) {
|
||||
this.minimalQuickCaptureSuggest = new MinimalQuickCaptureSuggest(this.app, this);
|
||||
this.minimalQuickCaptureSuggest = new MinimalQuickCaptureSuggest(
|
||||
this.app,
|
||||
this
|
||||
);
|
||||
this.registerEditorSuggest(this.minimalQuickCaptureSuggest);
|
||||
}
|
||||
|
||||
// Add task filter extension
|
||||
|
|
@ -1042,9 +1054,17 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
autoDateManagerExtension(this.app, this),
|
||||
]);
|
||||
}
|
||||
|
||||
// Add task mark cleanup extension (always enabled)
|
||||
this.registerEditorExtension([taskMarkCleanupExtension()]);
|
||||
}
|
||||
|
||||
onunload() {
|
||||
// Clean up global suggest manager
|
||||
if (this.globalSuggestManager) {
|
||||
this.globalSuggestManager.cleanup();
|
||||
}
|
||||
|
||||
// Clean up task manager when plugin is unloaded
|
||||
if (this.taskManager) {
|
||||
this.taskManager.onunload();
|
||||
|
|
@ -1055,36 +1075,37 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
|
||||
async loadSettings() {
|
||||
const savedData = await this.loadData();
|
||||
this.settings = Object.assign(
|
||||
{},
|
||||
DEFAULT_SETTINGS,
|
||||
savedData
|
||||
);
|
||||
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, savedData);
|
||||
|
||||
// Migrate old inheritance settings to new structure
|
||||
this.migrateInheritanceSettings(savedData);
|
||||
}
|
||||
|
||||
private migrateInheritanceSettings(savedData: any) {
|
||||
// Check if old inheritance settings exist and new ones don't
|
||||
if (savedData?.projectConfig?.metadataConfig &&
|
||||
!savedData?.fileMetadataInheritance) {
|
||||
|
||||
if (
|
||||
savedData?.projectConfig?.metadataConfig &&
|
||||
!savedData?.fileMetadataInheritance
|
||||
) {
|
||||
const oldConfig = savedData.projectConfig.metadataConfig;
|
||||
|
||||
|
||||
// Migrate to new structure
|
||||
this.settings.fileMetadataInheritance = {
|
||||
enabled: true,
|
||||
inheritFromFrontmatter: oldConfig.inheritFromFrontmatter ?? true,
|
||||
inheritFromFrontmatterForSubtasks: oldConfig.inheritFromFrontmatterForSubtasks ?? false
|
||||
inheritFromFrontmatter:
|
||||
oldConfig.inheritFromFrontmatter ?? true,
|
||||
inheritFromFrontmatterForSubtasks:
|
||||
oldConfig.inheritFromFrontmatterForSubtasks ?? false,
|
||||
};
|
||||
|
||||
|
||||
// Remove old inheritance settings from project config
|
||||
if (this.settings.projectConfig?.metadataConfig) {
|
||||
delete (this.settings.projectConfig.metadataConfig as any).inheritFromFrontmatter;
|
||||
delete (this.settings.projectConfig.metadataConfig as any).inheritFromFrontmatterForSubtasks;
|
||||
delete (this.settings.projectConfig.metadataConfig as any)
|
||||
.inheritFromFrontmatter;
|
||||
delete (this.settings.projectConfig.metadataConfig as any)
|
||||
.inheritFromFrontmatterForSubtasks;
|
||||
}
|
||||
|
||||
|
||||
// Save the migrated settings
|
||||
this.saveSettings();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,3 +246,5 @@ settings:
|
|||
default-light: '#f1f1f1'
|
||||
default-dark: '#f1f1f1'
|
||||
*/
|
||||
|
||||
@import url("universal-suggest.css");
|
||||
|
|
|
|||
|
|
@ -38,8 +38,6 @@
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--size-4-2);
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
background-color: var(--background-secondary);
|
||||
}
|
||||
|
||||
.quick-actions-left {
|
||||
|
|
@ -52,26 +50,6 @@
|
|||
gap: var(--size-2-1);
|
||||
}
|
||||
|
||||
.quick-action-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--radius-s);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
background-color: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.quick-action-button:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
border-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.quick-action-button.active {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
|
|
@ -133,7 +111,6 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-2-1);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.suggestion-icon {
|
||||
|
|
|
|||
85
src/styles/universal-suggest.css
Normal file
85
src/styles/universal-suggest.css
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
/* Universal Suggest Styles */
|
||||
|
||||
.universal-suggest-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.1s ease;
|
||||
}
|
||||
|
||||
.universal-suggest-item:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.universal-suggest-item.is-selected {
|
||||
background-color: var(--background-modifier-active-hover);
|
||||
}
|
||||
|
||||
.universal-suggest-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 12px;
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.universal-suggest-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.universal-suggest-label {
|
||||
font-weight: 500;
|
||||
color: var(--text-normal);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.universal-suggest-description {
|
||||
font-size: 0.85em;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* Special character trigger highlighting */
|
||||
.cm-editor .cm-line .universal-suggest-trigger {
|
||||
background-color: var(--background-modifier-accent);
|
||||
color: var(--text-accent);
|
||||
border-radius: 2px;
|
||||
padding: 1px 2px;
|
||||
}
|
||||
|
||||
/* Suggest popup container */
|
||||
.suggestion-container .universal-suggest-item {
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.suggestion-container .universal-suggest-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Dark theme adjustments */
|
||||
.theme-dark .universal-suggest-item:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.theme-dark .universal-suggest-item.is-selected {
|
||||
background-color: var(--background-modifier-active-hover);
|
||||
}
|
||||
|
||||
/* High contrast mode */
|
||||
@media (prefers-contrast: high) {
|
||||
.universal-suggest-item {
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.universal-suggest-item:hover,
|
||||
.universal-suggest-item.is-selected {
|
||||
border-color: var(--text-accent);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -130,24 +130,6 @@ export class TaskGeniusIconManager extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
css =
|
||||
`
|
||||
input[type=checkbox]:checked {
|
||||
background-color: unset;
|
||||
border: none;
|
||||
}
|
||||
input[type=checkbox] {
|
||||
border: none;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
input[type=checkbox]:checked:hover {
|
||||
background-color: unset;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
` + css;
|
||||
|
||||
return css;
|
||||
}
|
||||
|
||||
|
|
@ -282,11 +264,20 @@ input[type=checkbox] {
|
|||
|
||||
if (!isSpace) {
|
||||
return `
|
||||
.${this.BODY_CLASS} [data-task="${escapedChar}"] > input[type=checkbox],
|
||||
.${this.BODY_CLASS} [data-task="${escapedChar}"] > p > input[type=checkbox],
|
||||
.${this.BODY_CLASS} [data-task="${escapedChar}"][type=checkbox] {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.${this.BODY_CLASS} [data-task="${escapedChar}"] > input[type=checkbox]:checked,
|
||||
.${this.BODY_CLASS} [data-task="${escapedChar}"] > p > input[type=checkbox]:checked,
|
||||
.${this.BODY_CLASS} [data-task="${escapedChar}"][type=checkbox]:checked {
|
||||
--checkbox-color: ${fillColor};
|
||||
--checkbox-color-hover: ${fillColor};
|
||||
|
||||
background-color: unset;
|
||||
border: none;
|
||||
}
|
||||
.${this.BODY_CLASS} [data-task="${escapedChar}"] > input[type=checkbox]:checked:after,
|
||||
.${this.BODY_CLASS} [data-task="${escapedChar}"] > p > input[type=checkbox]:checked:after,
|
||||
|
|
@ -298,12 +289,20 @@ input[type=checkbox] {
|
|||
`;
|
||||
} else {
|
||||
return `
|
||||
.${this.BODY_CLASS} [data-task="${escapedChar}"] > input[type=checkbox],
|
||||
.${this.BODY_CLASS} [data-task="${escapedChar}"] > p > input[type=checkbox],
|
||||
.${this.BODY_CLASS} [data-task="${escapedChar}"][type=checkbox] {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.${this.BODY_CLASS} [data-task="${escapedChar}"] > input[type=checkbox],
|
||||
.${this.BODY_CLASS} [data-task="${escapedChar}"] > p > input[type=checkbox],
|
||||
.${this.BODY_CLASS} [data-task="${escapedChar}"][type=checkbox] {
|
||||
--checkbox-color: ${fillColor};
|
||||
--checkbox-color-hover: ${fillColor};
|
||||
|
||||
background-color: unset;
|
||||
border: none;
|
||||
}
|
||||
.${this.BODY_CLASS} [data-task="${escapedChar}"] > input[type=checkbox]:after,
|
||||
.${this.BODY_CLASS} [data-task="${escapedChar}"] > p > input[type=checkbox]:after,
|
||||
|
|
|
|||
11130
styles.css
11130
styles.css
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue