Skip to content

Commit e06900d

Browse files
committed
Add document editor commands to command palette
1 parent 6e878e0 commit e06900d

4 files changed

Lines changed: 177 additions & 19 deletions

File tree

App.tsx

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ import UpdateNotification from './components/UpdateNotification';
2222
import CreateFromTemplateModal from './components/CreateFromTemplateModal';
2323
import DocumentHistoryView from './components/PromptHistoryView';
2424
import FolderOverview, { type FolderOverviewMetrics, type FolderSearchResult, type RecentDocumentSummary, type DocTypeCount, type LanguageCount } from './components/FolderOverview';
25-
import { PlusIcon, FolderPlusIcon, TrashIcon, GearIcon, InfoIcon, TerminalIcon, DocumentDuplicateIcon, PencilIcon, CopyIcon, CommandIcon, CodeIcon, FolderDownIcon, FormatIcon, SparklesIcon, SaveIcon, CheckIcon, DatabaseIcon, ExpandAllIcon, CollapseAllIcon, ArrowUpIcon, ArrowDownIcon, LockClosedIcon, LockOpenIcon, SearchIcon } from './components/Icons';
25+
import { PlusIcon, FolderPlusIcon, TrashIcon, GearIcon, InfoIcon, TerminalIcon, DocumentDuplicateIcon, PencilIcon, CopyIcon, CommandIcon, CodeIcon, FolderDownIcon, FormatIcon, SparklesIcon, SaveIcon, CheckIcon, DatabaseIcon, ExpandAllIcon, CollapseAllIcon, ArrowUpIcon, ArrowDownIcon, LockClosedIcon, LockOpenIcon, SearchIcon, RefreshIcon, HistoryIcon, UndoIcon, LayoutVerticalIcon } from './components/Icons';
2626
import AboutModal from './components/AboutModal';
2727
import Header from './components/Header';
2828
import CustomTitleBar from './components/CustomTitleBar';
2929
import ConfirmModal from './components/ConfirmModal';
3030
import FatalError from './components/FatalError';
3131
import ContextMenu, { MenuItem } from './components/ContextMenu';
3232
import NewCodeFileModal from './components/NewCodeFileModal';
33-
import type { DocumentOrFolder, Command, LogMessage, DiscoveredLLMModel, DiscoveredLLMService, Settings, DocumentTemplate, ViewMode, DocType, DraggedNodeTransfer, UpdateAvailableInfo, PreviewMetadata } from './types';
33+
import type { DocumentOrFolder, Command, LogMessage, DiscoveredLLMModel, DiscoveredLLMService, Settings, DocumentTemplate, ViewMode, DocType, DraggedNodeTransfer, UpdateAvailableInfo, PreviewMetadata, DocumentCommandTriggers } from './types';
3434
import { IconProvider } from './contexts/IconContext';
3535
import { storageService } from './services/storageService';
3636
import { exportDocumentToFile } from './services/documentExportService';
@@ -222,6 +222,17 @@ export const MainApp: React.FC = () => {
222222
const [contextMenu, setContextMenu] = useState<{ isOpen: boolean; position: { x: number, y: number }, items: MenuItem[] }>({ isOpen: false, position: { x: 0, y: 0 }, items: [] });
223223
const [isDraggingFile, setIsDraggingFile] = useState(false);
224224
const [formatTrigger, setFormatTrigger] = useState(0);
225+
const [documentCommandTriggers, setDocumentCommandTriggers] = useState<DocumentCommandTriggers>({
226+
addEmojiToTitle: 0,
227+
regenerateTitle: 0,
228+
openLanguageSelector: 0,
229+
cycleViewMode: 0,
230+
toggleInlineDiff: 0,
231+
cancelChanges: 0,
232+
manualSave: 0,
233+
copyContent: 0,
234+
refineWithAI: 0,
235+
});
225236
const [bodySearchMatches, setBodySearchMatches] = useState<Map<string, string>>(new Map());
226237
const [folderSearchTerm, setFolderSearchTerm] = useState('');
227238
const [folderBodySearchMatches, setFolderBodySearchMatches] = useState<Map<string, string>>(new Map());
@@ -497,6 +508,24 @@ export const MainApp: React.FC = () => {
497508
return activeNode?.type === 'document' ? activeNode : null;
498509
}, [activeNode]);
499510

511+
const triggerDocumentCommand = useCallback((key: keyof DocumentCommandTriggers, logMessage: string, unavailableMessage: string) => {
512+
if (!activeDocument) {
513+
addLog('WARNING', unavailableMessage);
514+
return;
515+
}
516+
addLog('INFO', logMessage);
517+
if (view !== 'editor') {
518+
setView('editor');
519+
}
520+
if (documentView !== 'editor') {
521+
setDocumentView('editor');
522+
}
523+
setDocumentCommandTriggers(prev => ({
524+
...prev,
525+
[key]: prev[key] + 1,
526+
}));
527+
}, [activeDocument, addLog, view, documentView]);
528+
500529
useEffect(() => {
501530
if (!activeDocument) {
502531
setPreviewMetadata(null);
@@ -2666,13 +2695,33 @@ export const MainApp: React.FC = () => {
26662695
{ id: 'document-tree-save-to-file', name: 'Save Document to File', action: handleSaveSelectionToFile, category: 'Document Tree', icon: SaveIcon, keywords: 'save export download file tree document' },
26672696
{ id: 'format-document', name: 'Format Document', action: handleFormatDocument, category: 'Editor', icon: FormatIcon, shortcut: ['Control', 'Shift', 'F'], keywords: 'beautify pretty print clean code' },
26682697
{ id: 'toggle-document-lock', name: activeDocument?.locked ? 'Unlock Active Document' : 'Lock Active Document', action: () => { void handleToggleActiveDocumentLock(); }, category: 'Editor', icon: activeDocument?.locked ? LockOpenIcon : LockClosedIcon, keywords: 'lock unlock read-only protect document' },
2698+
{ id: 'document-add-ai-emoji', name: 'Add AI Emoji Prefix to Title', action: () => triggerDocumentCommand('addEmojiToTitle', 'Command: Add AI-generated emoji prefix to the document title.', 'Add AI emoji prefix is only available when a document is open.'), category: 'Editor', icon: SparklesIcon, keywords: 'emoji ai title prefix' },
2699+
{ id: 'document-regenerate-title', name: 'Regenerate Title with AI', action: () => triggerDocumentCommand('regenerateTitle', 'Command: Regenerate document title with AI.', 'Title regeneration is only available when a document is open.'), category: 'Editor', icon: RefreshIcon, keywords: 'title rename ai regenerate' },
2700+
{ id: 'document-open-language-selector', name: 'Open Document Language Selector', action: () => triggerDocumentCommand('openLanguageSelector', 'Command: Open document language selector.', 'Language selection is only available when a document is open.'), category: 'Editor', icon: CommandIcon, keywords: 'language change selector locale' },
2701+
{ id: 'document-cycle-layout', name: 'Cycle Editor Layout Modes', action: () => triggerDocumentCommand('cycleViewMode', 'Command: Switch editor layout mode.', 'Layout switching is only available when a document is open.'), category: 'View', icon: LayoutVerticalIcon, keywords: 'layout switch editor preview split' },
2702+
{ id: 'document-toggle-inline-diff', name: 'Toggle Inline Diff Mode', action: () => triggerDocumentCommand('toggleInlineDiff', 'Command: Toggle inline diff mode.', 'Inline diff mode is only available when a document is open.'), category: 'View', icon: DocumentDuplicateIcon, keywords: 'diff compare changes inline' },
2703+
{ id: 'document-open-history', name: 'Open Document History Panel', action: () => {
2704+
if (!activeDocument) {
2705+
addLog('WARNING', 'Document history is only available when a document is open.');
2706+
return;
2707+
}
2708+
addLog('INFO', 'Command: Open document history panel.');
2709+
if (view !== 'editor') {
2710+
setView('editor');
2711+
}
2712+
setDocumentView('history');
2713+
}, category: 'View', icon: HistoryIcon, keywords: 'history versions timeline restore' },
2714+
{ id: 'document-cancel-unsaved', name: 'Cancel Unsaved Changes', action: () => triggerDocumentCommand('cancelChanges', 'Command: Cancel unsaved document changes.', 'Canceling unsaved changes is only available when a document is open.'), category: 'Editor', icon: UndoIcon, keywords: 'cancel undo revert discard' },
2715+
{ id: 'document-manual-save', name: 'Save Document Version', action: () => triggerDocumentCommand('manualSave', 'Command: Manually save a document version.', 'Manual saves are only available when a document is open.'), category: 'Editor', icon: SaveIcon, keywords: 'save version commit manual' },
2716+
{ id: 'document-copy-active-content', name: 'Copy Active Document Content', action: () => triggerDocumentCommand('copyContent', 'Command: Copy active document content.', 'Copying document content is only available when a document is open.'), category: 'Editor', icon: CopyIcon, keywords: 'copy clipboard content editor' },
2717+
{ id: 'document-ai-refine', name: 'Refine Document with AI', action: () => triggerDocumentCommand('refineWithAI', 'Command: Request AI refinement of the document content.', 'AI refinement is only available when a document is open.'), category: 'Editor', icon: SparklesIcon, keywords: 'ai refine improve suggestions' },
26692718
{ id: 'toggle-command-palette', name: 'Toggle Command Palette', action: handleToggleCommandPalette, category: 'View', icon: CommandIcon, shortcut: ['Control', 'Shift', 'P'], keywords: 'find action go to' },
26702719
{ id: 'toggle-editor', name: 'Switch to Editor View', action: () => { addLog('INFO', 'Command: Switch to Editor View.'); setView('editor'); }, category: 'View', icon: PencilIcon, keywords: 'main document' },
26712720
{ id: 'toggle-settings', name: 'Toggle Settings View', action: toggleSettingsView, category: 'View', icon: GearIcon, keywords: 'configure options' },
26722721
{ id: 'toggle-info', name: 'Toggle Info View', action: () => { addLog('INFO', 'Command: Toggle Info View.'); setView(v => v === 'info' ? 'editor' : 'info'); }, category: 'View', icon: InfoIcon, keywords: 'help docs readme' },
26732722
{ id: 'open-about', name: 'About DocForge', action: handleOpenAbout, category: 'Help', icon: SparklesIcon, keywords: 'about credits information' },
26742723
{ id: 'toggle-logs', name: 'Toggle Logs Panel', action: () => { addLog('INFO', 'Command: Toggle Logs Panel.'); setIsLoggerVisible(v => !v); }, category: 'View', icon: TerminalIcon, keywords: 'debug console' },
2675-
], [handleNewDocument, handleOpenNewCodeFileModal, handleNewRootFolder, handleNewSubfolder, handleDeleteSelection, handleNewTemplate, toggleSettingsView, handleDuplicateSelection, handleRenameSelection, selectedIds, addLog, handleToggleCommandPalette, handleFormatDocument, handleOpenAbout, handleNewDocumentFromClipboard, handleDocumentTreeSelectAll, handleFocusDocumentTreeSearch, handleExpandAll, handleCollapseAll, handleMoveSelectionUp, handleMoveSelectionDown, handleCopySelectionContent, handleSaveSelectionToFile, activeDocument?.locked, handleToggleActiveDocumentLock]);
2724+
], [handleNewDocument, handleOpenNewCodeFileModal, handleNewRootFolder, handleNewSubfolder, handleDeleteSelection, handleNewTemplate, toggleSettingsView, handleDuplicateSelection, handleRenameSelection, selectedIds, addLog, handleToggleCommandPalette, handleFormatDocument, handleOpenAbout, handleNewDocumentFromClipboard, handleDocumentTreeSelectAll, handleFocusDocumentTreeSearch, handleExpandAll, handleCollapseAll, handleMoveSelectionUp, handleMoveSelectionDown, handleCopySelectionContent, handleSaveSelectionToFile, activeDocument?.locked, handleToggleActiveDocumentLock, triggerDocumentCommand, view, setDocumentView]);
26762725

26772726
const enrichedCommands = useMemo(() => {
26782727
return commands.map(command => {
@@ -2938,6 +2987,7 @@ export const MainApp: React.FC = () => {
29382987
onPreviewZoomAvailabilityChange={setIsPreviewZoomReady}
29392988
onPreviewMetadataChange={setPreviewMetadata}
29402989
onZoomTargetChange={handleWorkspaceZoomTargetChange}
2990+
commandTriggers={documentCommandTriggers}
29412991
/>
29422992
);
29432993
}

components/LanguageDropdown.tsx

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,23 @@ interface LanguageDropdownProps {
66
id?: string;
77
value: string;
88
onChange: (languageId: string) => void;
9+
openTrigger?: number;
910
}
1011

11-
const LanguageDropdown: React.FC<LanguageDropdownProps> = ({ id, value, onChange }) => {
12+
const LanguageDropdown = React.forwardRef<HTMLButtonElement, LanguageDropdownProps>(({ id, value, onChange, openTrigger }, forwardedRef) => {
1213
const [isOpen, setIsOpen] = useState(false);
1314
const containerRef = useRef<HTMLDivElement | null>(null);
15+
const buttonRef = useRef<HTMLButtonElement | null>(null);
16+
const previousTriggerRef = useRef(openTrigger);
17+
18+
const setButtonRef = useCallback((node: HTMLButtonElement | null) => {
19+
buttonRef.current = node;
20+
if (typeof forwardedRef === 'function') {
21+
forwardedRef(node);
22+
} else if (forwardedRef) {
23+
forwardedRef.current = node;
24+
}
25+
}, [forwardedRef]);
1426

1527
const selectedLanguage = useMemo(() => {
1628
return SUPPORTED_LANGUAGES.find((lang) => lang.id === value) ?? SUPPORTED_LANGUAGES[0];
@@ -59,11 +71,29 @@ const LanguageDropdown: React.FC<LanguageDropdownProps> = ({ id, value, onChange
5971
[close, onChange],
6072
);
6173

74+
useEffect(() => {
75+
if (openTrigger === undefined) {
76+
return;
77+
}
78+
if (previousTriggerRef.current === openTrigger) {
79+
return;
80+
}
81+
previousTriggerRef.current = openTrigger;
82+
if (!openTrigger) {
83+
return;
84+
}
85+
setIsOpen(true);
86+
requestAnimationFrame(() => {
87+
buttonRef.current?.focus();
88+
});
89+
}, [openTrigger]);
90+
6291
return (
6392
<div ref={containerRef} className="relative text-xs">
6493
<button
6594
id={id}
6695
type="button"
96+
ref={setButtonRef}
6797
onClick={toggleOpen}
6898
className="flex items-center gap-2 bg-background text-text-main text-xs rounded-md py-1 pl-2 pr-2 border border-border-color focus:outline-none focus:ring-1 focus:ring-primary"
6999
aria-haspopup="listbox"
@@ -103,6 +133,8 @@ const LanguageDropdown: React.FC<LanguageDropdownProps> = ({ id, value, onChange
103133
)}
104134
</div>
105135
);
106-
};
136+
});
137+
138+
LanguageDropdown.displayName = 'LanguageDropdown';
107139

108140
export default LanguageDropdown;

0 commit comments

Comments
 (0)