mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
Remove custom spell checking implementation in favor of native OS spell checking
- Remove custom spell checking logic from RichChatInput component
- Remove SpellCheckTooltip and SpellCheckContextMenu components
- Remove IPC spell-check and spell-suggestions handlers from main process
- Enable native browser spell checking with spellCheck={true} on textarea
- Simplify component by removing misspelled word highlighting and tooltip state
- Users now get familiar OS-native spell check with right-click context menus
This provides better UX with system dictionary integration and familiar behavior.
This commit is contained in:
committed by
Alex Hancock
parent
f809bb689c
commit
de52931e17
@@ -1,6 +1,4 @@
|
||||
import React, { useRef, useEffect, useState, useCallback, forwardRef, useImperativeHandle } from 'react';
|
||||
import SpellCheckTooltip from './SpellCheckTooltip';
|
||||
// Remove unused import - using Electron spell checking instead
|
||||
import { ActionPill } from './ActionPill';
|
||||
import MentionPill from './MentionPill';
|
||||
import { Zap, Code, FileText, Search, Play, Settings } from 'lucide-react';
|
||||
@@ -40,69 +38,6 @@ export interface RichChatInputRef {
|
||||
getBoundingClientRect: () => DOMRect;
|
||||
}
|
||||
|
||||
// Use Electron's system spell checking
|
||||
const checkSpelling = async (text: string): Promise<{ word: string; start: number; end: number; suggestions: string[] }[]> => {
|
||||
console.log('🔍 ELECTRON SPELL CHECK: Starting system spell check for text:', text);
|
||||
const misspelledWords: { word: string; start: number; end: number; suggestions: string[] }[] = [];
|
||||
|
||||
// Check if Electron API is available
|
||||
if (!window.electron?.spellCheck || !window.electron?.spellSuggestions) {
|
||||
console.warn('🔍 ELECTRON SPELL CHECK: Electron spell check API not available, falling back to no spell checking');
|
||||
return misspelledWords;
|
||||
}
|
||||
|
||||
// Split text into words while preserving positions
|
||||
const wordRegex = /\b[a-zA-Z]+\b/g;
|
||||
let match;
|
||||
|
||||
const wordChecks: Array<{word: string; start: number; end: number}> = [];
|
||||
|
||||
// First, collect all words and their positions
|
||||
while ((match = wordRegex.exec(text)) !== null) {
|
||||
const word = match[0];
|
||||
const start = match.index;
|
||||
const end = start + word.length;
|
||||
|
||||
// Skip very short words (less than 3 characters)
|
||||
if (word.length < 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
wordChecks.push({ word, start, end });
|
||||
}
|
||||
|
||||
console.log('🔍 ELECTRON SPELL CHECK: Found words to check:', wordChecks.map(w => w.word));
|
||||
|
||||
// Check each word and collect results
|
||||
for (const { word, start, end } of wordChecks) {
|
||||
try {
|
||||
const isCorrect = await window.electron.spellCheck(word);
|
||||
console.log('🔍 ELECTRON SPELL CHECK: Word:', word, 'isCorrect:', isCorrect);
|
||||
|
||||
if (!isCorrect) {
|
||||
// Get suggestions from Electron
|
||||
const suggestions = await window.electron.spellSuggestions(word);
|
||||
console.log('🔍 ELECTRON SPELL CHECK: Suggestions for', word, ':', suggestions);
|
||||
|
||||
misspelledWords.push({
|
||||
word: word,
|
||||
start: start,
|
||||
end: end,
|
||||
suggestions: suggestions || []
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('🔍 ELECTRON SPELL CHECK: Error checking word', word, ':', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort misspelled words by position
|
||||
misspelledWords.sort((a, b) => a.start - b.start);
|
||||
|
||||
console.log('🔍 ELECTRON SPELL CHECK: Final result:', misspelledWords);
|
||||
return misspelledWords;
|
||||
};
|
||||
|
||||
export const RichChatInput = forwardRef<RichChatInputRef, RichChatInputProps>(({
|
||||
value,
|
||||
onChange,
|
||||
@@ -124,7 +59,6 @@ export const RichChatInput = forwardRef<RichChatInputRef, RichChatInputProps>(({
|
||||
const displayRef = useRef<HTMLDivElement>(null);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [cursorPosition, setCursorPosition] = useState(0);
|
||||
const [misspelledWords, setMisspelledWords] = useState<{ word: string; start: number; end: number; suggestions: string[] }[]>([]);
|
||||
|
||||
// Scroll synchronization - ensure both layers stay perfectly in sync
|
||||
const handleTextareaScroll = useCallback(() => {
|
||||
@@ -237,27 +171,8 @@ export const RichChatInput = forwardRef<RichChatInputRef, RichChatInputProps>(({
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}
|
||||
}, [syncDisplayHeight]);
|
||||
}, [monitorTextareaChanges]);
|
||||
|
||||
// Spell check tooltip state
|
||||
const [tooltip, setTooltip] = useState<{
|
||||
isVisible: boolean;
|
||||
position: { x: number; y: number };
|
||||
misspelledWord: string;
|
||||
suggestions: string[];
|
||||
wordStart: number;
|
||||
wordEnd: number;
|
||||
isHoveringTooltip: boolean;
|
||||
}>({
|
||||
isVisible: false,
|
||||
position: { x: 0, y: 0 },
|
||||
misspelledWord: '',
|
||||
suggestions: [],
|
||||
wordStart: 0,
|
||||
wordEnd: 0,
|
||||
isHoveringTooltip: false,
|
||||
});
|
||||
|
||||
// Expose methods to parent component
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => hiddenTextareaRef.current?.focus(),
|
||||
@@ -276,89 +191,9 @@ export const RichChatInput = forwardRef<RichChatInputRef, RichChatInputProps>(({
|
||||
if (hiddenTextareaRef.current) {
|
||||
setCursorPosition(hiddenTextareaRef.current.selectionStart);
|
||||
}
|
||||
}, []);
|
||||
}, [updateCursorPosition]);
|
||||
|
||||
// Track the last spell checked text to avoid unnecessary re-checks
|
||||
const lastSpellCheckedTextRef = useRef<string>('');
|
||||
const spellCheckTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Spell check the content using Electron's system spell checker
|
||||
const performSpellCheck = useCallback(async (text: string, isIncremental = false) => {
|
||||
console.log('🔍 ELECTRON SPELL CHECK: Starting system spell check for text:', text, 'incremental:', isIncremental);
|
||||
|
||||
// Skip if we've already checked this exact text
|
||||
if (text === lastSpellCheckedTextRef.current) {
|
||||
console.log('🔍 ELECTRON SPELL CHECK: Skipping - text unchanged');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the Electron spell checking function
|
||||
try {
|
||||
const misspelledWords = await checkSpelling(text);
|
||||
console.log('🔍 ELECTRON SPELL CHECK: System spell check result:', misspelledWords);
|
||||
|
||||
// Critical: Check current value at time of update, not captured value
|
||||
const currentValue = hiddenTextareaRef.current?.value || '';
|
||||
|
||||
// Only update if the text hasn't changed since we started checking
|
||||
if (text === currentValue) {
|
||||
// Use functional update to avoid dependency on value in useCallback
|
||||
setMisspelledWords(misspelledWords);
|
||||
lastSpellCheckedTextRef.current = text;
|
||||
} else {
|
||||
console.log('🔍 ELECTRON SPELL CHECK: Discarding stale result - text changed during check');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('🔍 ELECTRON SPELL CHECK: Error performing spell check:', error);
|
||||
// Fallback to no spell checking on error
|
||||
const currentValue = hiddenTextareaRef.current?.value || '';
|
||||
if (text === currentValue) {
|
||||
setMisspelledWords([]);
|
||||
}
|
||||
}
|
||||
}, []); // Remove value dependency to prevent recreation
|
||||
|
||||
// Smart spell check timing - check after word completion and with shorter delays
|
||||
useEffect(() => {
|
||||
// Clear any existing timeout
|
||||
if (spellCheckTimeoutRef.current) {
|
||||
clearTimeout(spellCheckTimeoutRef.current);
|
||||
}
|
||||
|
||||
if (!value.trim()) {
|
||||
setMisspelledWords([]);
|
||||
lastSpellCheckedTextRef.current = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect if user just completed a word (typed space, punctuation, or newline)
|
||||
const lastChar = value[value.length - 1];
|
||||
const isWordBoundary = /[\s\n\.,!?;:]/.test(lastChar);
|
||||
|
||||
// Use different delays based on context
|
||||
let delay: number;
|
||||
if (isWordBoundary) {
|
||||
// Just completed a word - check quickly
|
||||
delay = 150;
|
||||
} else {
|
||||
// Still typing within a word - wait longer to avoid interrupting
|
||||
delay = 300;
|
||||
}
|
||||
|
||||
console.log('🔍 SPELL CHECK TIMING: Setting timeout with delay:', delay, 'isWordBoundary:', isWordBoundary, 'lastChar:', lastChar);
|
||||
|
||||
spellCheckTimeoutRef.current = setTimeout(() => {
|
||||
performSpellCheck(value, !isWordBoundary);
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
if (spellCheckTimeoutRef.current) {
|
||||
clearTimeout(spellCheckTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [value, performSpellCheck]);
|
||||
|
||||
// Parse and render content with action pills, mention pills, spell checking, and cursor
|
||||
// Parse and render content with action pills, mention pills, and cursor
|
||||
const renderContent = useCallback(() => {
|
||||
// Show placeholder when there's no text content (but account for whitespace-only content with newlines)
|
||||
if (!value || (value.trim() === '' && !value.includes('\n'))) {
|
||||
@@ -395,9 +230,8 @@ export const RichChatInput = forwardRef<RichChatInputRef, RichChatInputProps>(({
|
||||
|
||||
console.log('🎨 RichChatInput renderContent called with value:', value);
|
||||
console.log('🔍 Looking for action and mention patterns with regex:', { actionRegex, mentionRegex });
|
||||
console.log('📝 Misspelled words:', misspelledWords);
|
||||
|
||||
// Find all actions, mentions, and misspelled words, then sort by position
|
||||
// Find all actions and mentions, then sort by position
|
||||
const allMatches = [];
|
||||
|
||||
// Find all action matches
|
||||
@@ -428,17 +262,6 @@ export const RichChatInput = forwardRef<RichChatInputRef, RichChatInputProps>(({
|
||||
});
|
||||
}
|
||||
|
||||
// Add misspelled words
|
||||
misspelledWords.forEach(misspelled => {
|
||||
allMatches.push({
|
||||
type: 'misspelled',
|
||||
match: null,
|
||||
index: misspelled.start,
|
||||
length: misspelled.end - misspelled.start,
|
||||
content: misspelled.word
|
||||
});
|
||||
});
|
||||
|
||||
// Sort matches by position
|
||||
allMatches.sort((a, b) => a.index - b.index);
|
||||
|
||||
@@ -449,7 +272,7 @@ export const RichChatInput = forwardRef<RichChatInputRef, RichChatInputProps>(({
|
||||
let lastProcessedEnd = 0;
|
||||
|
||||
for (const matchData of allMatches) {
|
||||
// Skip overlapping matches (pills take priority over spell check)
|
||||
// Skip overlapping matches
|
||||
if (matchData.index < lastProcessedEnd) {
|
||||
continue;
|
||||
}
|
||||
@@ -541,102 +364,6 @@ export const RichChatInput = forwardRef<RichChatInputRef, RichChatInputProps>(({
|
||||
onRemove={() => handleRemoveMention(fileName)}
|
||||
/>
|
||||
);
|
||||
} else if (type === 'misspelled') {
|
||||
// Handle misspelled words with red highlighting and hover tooltip
|
||||
const misspelledData = misspelledWords.find(m => m.word === content);
|
||||
console.log('🎨 RENDERING MISSPELLED: word:', content, 'data:', misspelledData);
|
||||
console.log('🎨 RENDERING MISSPELLED: all misspelled words:', misspelledWords);
|
||||
|
||||
parts.push(
|
||||
<span
|
||||
key={`misspelled-${keyCounter++}`}
|
||||
data-misspelled="true"
|
||||
className="inline whitespace-pre-wrap cursor-pointer bg-red-50 dark:bg-red-950/30 text-red-600 dark:text-red-400 font-medium px-1 py-0.5 rounded-sm border border-red-200 dark:border-red-800 hover:bg-red-100 dark:hover:bg-red-900/40 hover:border-red-300 dark:hover:border-red-700 hover:scale-105 transition-all duration-150 relative z-50"
|
||||
style={{
|
||||
pointerEvents: 'auto', // Override parent's pointer-events: none
|
||||
userSelect: 'text', // Allow text selection for normal text editing
|
||||
}}
|
||||
title={`Click or hover for suggestions: ${content}`}
|
||||
onClick={(e) => {
|
||||
console.log('🖱️ CLICK: Clicked on misspelled word:', content);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Ensure textarea maintains focus after click
|
||||
setTimeout(() => {
|
||||
if (hiddenTextareaRef.current) {
|
||||
hiddenTextareaRef.current.focus();
|
||||
}
|
||||
}, 0);
|
||||
|
||||
if (misspelledData) {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
console.log('🖱️ CLICK: Element rect:', rect);
|
||||
|
||||
// Show tooltip on click - positioned at center of word
|
||||
const tooltipData = {
|
||||
isVisible: true,
|
||||
position: {
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top
|
||||
},
|
||||
misspelledWord: misspelledData.word,
|
||||
suggestions: misspelledData.suggestions || [],
|
||||
wordStart: misspelledData.start,
|
||||
wordEnd: misspelledData.end,
|
||||
isHoveringTooltip: false,
|
||||
};
|
||||
console.log('🖱️ CLICK: Setting tooltip data:', tooltipData);
|
||||
setTooltip(tooltipData);
|
||||
}
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
console.log('🖱️ MOUSEENTER: Mouse entered misspelled word:', content);
|
||||
|
||||
// Only show tooltip on hover if we're not actively typing
|
||||
// Check if the textarea has been recently focused/active
|
||||
const now = Date.now();
|
||||
const lastActivity = hiddenTextareaRef.current?.dataset.lastActivity;
|
||||
const timeSinceActivity = lastActivity ? now - parseInt(lastActivity) : Infinity;
|
||||
|
||||
// Only show hover tooltip if it's been more than 500ms since last typing activity
|
||||
if (timeSinceActivity > 500 && misspelledData) {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
console.log('🖱️ MOUSEENTER: Element rect:', rect);
|
||||
const tooltipData = {
|
||||
isVisible: true,
|
||||
position: {
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top
|
||||
},
|
||||
misspelledWord: misspelledData.word,
|
||||
suggestions: misspelledData.suggestions || [],
|
||||
wordStart: misspelledData.start,
|
||||
wordEnd: misspelledData.end,
|
||||
isHoveringTooltip: false,
|
||||
};
|
||||
console.log('🖱️ MOUSEENTER: Setting tooltip data:', tooltipData);
|
||||
setTooltip(tooltipData);
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
console.log('🖱️ MOUSELEAVE: Mouse left misspelled word:', content);
|
||||
|
||||
// Add a small delay before hiding to allow moving to tooltip
|
||||
setTimeout(() => {
|
||||
setTooltip(prev => {
|
||||
// Only hide if not hovering over the tooltip
|
||||
if (!prev.isHoveringTooltip) {
|
||||
return { ...prev, isVisible: false };
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}, 150);
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
currentPos += length;
|
||||
@@ -694,7 +421,7 @@ export const RichChatInput = forwardRef<RichChatInputRef, RichChatInputProps>(({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}, [value, isFocused, placeholder, cursorPosition, misspelledWords]);
|
||||
}, [value, isFocused, placeholder, cursorPosition]);
|
||||
|
||||
const handleRemoveAction = useCallback((actionLabel: string) => {
|
||||
const actionText = `[${actionLabel}]`;
|
||||
@@ -712,11 +439,6 @@ export const RichChatInput = forwardRef<RichChatInputRef, RichChatInputProps>(({
|
||||
const newValue = e.target.value;
|
||||
const newCursorPos = e.target.selectionStart;
|
||||
|
||||
// Track typing activity to prevent hover tooltips while actively typing
|
||||
if (hiddenTextareaRef.current) {
|
||||
hiddenTextareaRef.current.dataset.lastActivity = Date.now().toString();
|
||||
}
|
||||
|
||||
console.log('🔄 RichChatInput: onChange', { newValue, newCursorPos });
|
||||
onChange(newValue, newCursorPos);
|
||||
setCursorPosition(newCursorPos);
|
||||
@@ -728,9 +450,6 @@ export const RichChatInput = forwardRef<RichChatInputRef, RichChatInputProps>(({
|
||||
}, [onChange, syncDisplayHeight]);
|
||||
|
||||
const handleTextareaKeyDown = useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
// Hide tooltip on any key press
|
||||
setTooltip(prev => ({ ...prev, isVisible: false }));
|
||||
|
||||
// Update cursor position on key events
|
||||
setTimeout(updateCursorPosition, 0);
|
||||
|
||||
@@ -815,8 +534,6 @@ export const RichChatInput = forwardRef<RichChatInputRef, RichChatInputProps>(({
|
||||
|
||||
const handleTextareaBlur = useCallback(() => {
|
||||
setIsFocused(false);
|
||||
// Hide tooltip when input loses focus
|
||||
setTooltip(prev => ({ ...prev, isVisible: false }));
|
||||
onBlur?.();
|
||||
}, [onBlur]);
|
||||
|
||||
@@ -848,87 +565,9 @@ export const RichChatInput = forwardRef<RichChatInputRef, RichChatInputProps>(({
|
||||
return cleanup;
|
||||
}, [monitorTextareaChanges]);
|
||||
|
||||
// Tooltip handlers
|
||||
const handleSuggestionSelect = useCallback((suggestion: string) => {
|
||||
const newValue = value.slice(0, tooltip.wordStart) +
|
||||
suggestion +
|
||||
value.slice(tooltip.wordEnd);
|
||||
onChange(newValue);
|
||||
setTooltip(prev => ({ ...prev, isVisible: false }));
|
||||
}, [value, onChange, tooltip.wordStart, tooltip.wordEnd]);
|
||||
|
||||
const handleAddToDictionary = useCallback(() => {
|
||||
// TODO: Implement add to dictionary functionality
|
||||
console.log('Add to dictionary:', tooltip.misspelledWord);
|
||||
setTooltip(prev => ({ ...prev, isVisible: false }));
|
||||
}, [tooltip.misspelledWord]);
|
||||
|
||||
const handleIgnore = useCallback(() => {
|
||||
// TODO: Implement ignore functionality
|
||||
console.log('Ignore word:', tooltip.misspelledWord);
|
||||
setTooltip(prev => ({ ...prev, isVisible: false }));
|
||||
}, [tooltip.misspelledWord]);
|
||||
|
||||
// Container mouse leave handler
|
||||
const handleContainerMouseLeave = useCallback(() => {
|
||||
console.log('🖱️ CONTAINER MOUSE LEAVE: Hiding tooltip');
|
||||
setTooltip(prev => ({ ...prev, isVisible: false }));
|
||||
}, []);
|
||||
|
||||
// Hide tooltip when clicking outside or when component loses focus
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as Node;
|
||||
|
||||
// Don't hide if clicking on the tooltip itself or its children
|
||||
const tooltipElement = document.querySelector('[data-spell-tooltip="true"]');
|
||||
if (tooltipElement && tooltipElement.contains(target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't hide if clicking on a misspelled word
|
||||
const misspelledElement = target as Element;
|
||||
if (misspelledElement?.closest?.('[data-misspelled="true"]')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide tooltip if clicking outside the input area
|
||||
if (displayRef.current && !displayRef.current.contains(target)) {
|
||||
setTooltip(prev => ({ ...prev, isVisible: false }));
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Tooltip hover handlers
|
||||
const handleTooltipEnter = useCallback(() => {
|
||||
console.log('🖱️ TOOLTIP ENTER: Setting isHoveringTooltip to true');
|
||||
setTooltip(prev => ({
|
||||
...prev,
|
||||
isHoveringTooltip: true,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleTooltipLeave = useCallback(() => {
|
||||
console.log('🖱️ TOOLTIP LEAVE: Setting isHoveringTooltip to false and hiding tooltip');
|
||||
setTooltip(prev => ({
|
||||
...prev,
|
||||
isHoveringTooltip: false,
|
||||
isVisible: false,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative rich-text-input"
|
||||
onMouseLeave={handleContainerMouseLeave}
|
||||
>
|
||||
{/* Hidden textarea for actual input handling with spell check enabled */}
|
||||
<div className="relative rich-text-input">
|
||||
{/* Hidden textarea for actual input handling with native spell check enabled */}
|
||||
<textarea
|
||||
ref={hiddenTextareaRef}
|
||||
value={value}
|
||||
@@ -941,7 +580,7 @@ export const RichChatInput = forwardRef<RichChatInputRef, RichChatInputProps>(({
|
||||
onCompositionEnd={onCompositionEnd}
|
||||
disabled={disabled}
|
||||
data-testid={testId}
|
||||
spellCheck={false} // Disable browser spell check - we handle it ourselves
|
||||
spellCheck={true} // Enable native OS spell checking
|
||||
className="absolute inset-0 w-full resize-none overflow-y-auto"
|
||||
onScroll={handleTextareaScroll}
|
||||
style={{
|
||||
@@ -972,7 +611,7 @@ export const RichChatInput = forwardRef<RichChatInputRef, RichChatInputProps>(({
|
||||
rows={rows}
|
||||
/>
|
||||
|
||||
{/* Visual display with action pills, mention pills, spell check, and cursor */}
|
||||
{/* Visual display with action pills, mention pills, and cursor */}
|
||||
<div
|
||||
ref={displayRef}
|
||||
className={`${className} cursor-text relative overflow-y-auto rich-text-display`}
|
||||
@@ -1029,20 +668,6 @@ export const RichChatInput = forwardRef<RichChatInputRef, RichChatInputProps>(({
|
||||
}
|
||||
`
|
||||
}} />
|
||||
|
||||
{/* Spell Check Hover Tooltip */}
|
||||
{console.log('🖱️ TOOLTIP RENDER: tooltip state:', tooltip)}
|
||||
<SpellCheckTooltip
|
||||
isVisible={tooltip.isVisible}
|
||||
position={tooltip.position}
|
||||
suggestions={tooltip.suggestions}
|
||||
misspelledWord={tooltip.misspelledWord}
|
||||
onSuggestionSelect={handleSuggestionSelect}
|
||||
onAddToDictionary={handleAddToDictionary}
|
||||
onIgnore={handleIgnore}
|
||||
onMouseEnter={handleTooltipEnter}
|
||||
onMouseLeave={handleTooltipLeave}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
interface SpellCheckContextMenuProps {
|
||||
isOpen: boolean;
|
||||
position: { x: number; y: number };
|
||||
suggestions: string[];
|
||||
misspelledWord: string;
|
||||
onSuggestionSelect: (suggestion: string) => void;
|
||||
onAddToDictionary: () => void;
|
||||
onIgnore: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const SpellCheckContextMenu: React.FC<SpellCheckContextMenuProps> = ({
|
||||
isOpen,
|
||||
position,
|
||||
suggestions,
|
||||
misspelledWord,
|
||||
onSuggestionSelect,
|
||||
onAddToDictionary,
|
||||
onIgnore,
|
||||
onClose,
|
||||
}) => {
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Close menu when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
// Prevent the default context menu
|
||||
document.addEventListener('contextmenu', (e) => e.preventDefault());
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
document.removeEventListener('contextmenu', (e) => e.preventDefault());
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
// Close menu on escape key
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="fixed z-50 bg-background-default border border-borderStandard rounded-lg shadow-lg py-2 min-w-48"
|
||||
style={{
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
}}
|
||||
>
|
||||
{/* Misspelled word header */}
|
||||
<div className="px-3 py-1 text-xs text-text-muted border-b border-borderSubtle mb-1">
|
||||
Suggestions for "{misspelledWord}"
|
||||
</div>
|
||||
|
||||
{/* Suggestions */}
|
||||
{suggestions.length > 0 ? (
|
||||
suggestions.map((suggestion, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => onSuggestionSelect(suggestion)}
|
||||
className="w-full text-left px-3 py-2 text-sm text-text-default hover:bg-bgSubtle transition-colors flex items-center gap-2"
|
||||
>
|
||||
<span className="w-4 h-4 flex items-center justify-center text-xs bg-blue-500 text-white rounded">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span className="font-medium">{suggestion}</span>
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="px-3 py-2 text-sm text-text-muted italic">
|
||||
No suggestions available
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Separator */}
|
||||
<div className="border-t border-borderSubtle my-1" />
|
||||
|
||||
{/* Additional actions */}
|
||||
<button
|
||||
onClick={onAddToDictionary}
|
||||
className="w-full text-left px-3 py-2 text-sm text-text-default hover:bg-bgSubtle transition-colors"
|
||||
>
|
||||
Add "{misspelledWord}" to dictionary
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onIgnore}
|
||||
className="w-full text-left px-3 py-2 text-sm text-text-default hover:bg-bgSubtle transition-colors"
|
||||
>
|
||||
Ignore "{misspelledWord}"
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpellCheckContextMenu;
|
||||
@@ -1,263 +0,0 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
|
||||
interface SpellCheckTooltipProps {
|
||||
isVisible: boolean;
|
||||
position: { x: number; y: number };
|
||||
suggestions: string[];
|
||||
misspelledWord: string;
|
||||
onSuggestionSelect: (suggestion: string) => void;
|
||||
onAddToDictionary: () => void;
|
||||
onIgnore: () => void;
|
||||
onMouseEnter?: () => void;
|
||||
onMouseLeave?: () => void;
|
||||
}
|
||||
|
||||
export const SpellCheckTooltip: React.FC<SpellCheckTooltipProps> = ({
|
||||
isVisible,
|
||||
position,
|
||||
suggestions,
|
||||
misspelledWord,
|
||||
onSuggestionSelect,
|
||||
onAddToDictionary,
|
||||
onIgnore,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
}) => {
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [adjustedPosition, setAdjustedPosition] = useState(position);
|
||||
|
||||
console.log('🖱️ TOOLTIP COMPONENT: Rendering with props:', {
|
||||
isVisible,
|
||||
position,
|
||||
suggestions,
|
||||
misspelledWord
|
||||
});
|
||||
|
||||
// Calculate smart positioning to avoid window edges
|
||||
useEffect(() => {
|
||||
if (!isVisible) {
|
||||
setAdjustedPosition(position);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use a timeout to allow the tooltip to render first
|
||||
const timer = setTimeout(() => {
|
||||
if (!tooltipRef.current) return;
|
||||
|
||||
const tooltip = tooltipRef.current;
|
||||
const tooltipRect = tooltip.getBoundingClientRect();
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
let newX = position.x;
|
||||
let newY = position.y;
|
||||
let transform = 'translateX(-50%) translateY(-100%)'; // Default: center horizontally, above word
|
||||
|
||||
// Check horizontal boundaries
|
||||
const tooltipWidth = tooltipRect.width || 200; // Fallback width
|
||||
const halfWidth = tooltipWidth / 2;
|
||||
const margin = 10; // Margin from window edges
|
||||
|
||||
if (newX - halfWidth < margin) {
|
||||
// Too close to left edge - align to left
|
||||
newX = margin;
|
||||
transform = 'translateY(-100%)'; // Remove horizontal centering
|
||||
} else if (newX + halfWidth > windowWidth - margin) {
|
||||
// Too close to right edge - align to right
|
||||
newX = windowWidth - margin;
|
||||
transform = 'translateX(-100%) translateY(-100%)'; // Align to right edge
|
||||
}
|
||||
|
||||
// Check vertical boundaries
|
||||
const tooltipHeight = tooltipRect.height || 150; // Fallback height
|
||||
|
||||
if (newY - tooltipHeight < margin) {
|
||||
// Not enough space above - show below the word
|
||||
newY = position.y + 30; // Position below word
|
||||
if (transform.includes('translateX(-50%)')) {
|
||||
transform = 'translateX(-50%) translateY(0%)'; // Center horizontally, below word
|
||||
} else if (transform.includes('translateX(-100%)')) {
|
||||
transform = 'translateX(-100%) translateY(0%)'; // Right align, below word
|
||||
} else {
|
||||
transform = 'translateY(0%)'; // Left align, below word
|
||||
}
|
||||
}
|
||||
|
||||
setAdjustedPosition({ x: newX, y: newY });
|
||||
|
||||
// Apply the transform
|
||||
tooltip.style.transform = transform;
|
||||
|
||||
console.log('🖱️ TOOLTIP POSITIONING:', {
|
||||
original: position,
|
||||
adjusted: { x: newX, y: newY },
|
||||
transform,
|
||||
windowSize: { width: windowWidth, height: windowHeight },
|
||||
tooltipSize: { width: tooltipWidth, height: tooltipHeight }
|
||||
});
|
||||
}, 0);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [isVisible, position]);
|
||||
|
||||
// Handle keyboard navigation
|
||||
useEffect(() => {
|
||||
if (!isVisible) return;
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
setSelectedIndex(prev =>
|
||||
Math.min(prev + 1, suggestions.length - 1)
|
||||
);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
setSelectedIndex(prev => Math.max(prev - 1, 0));
|
||||
break;
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
if (suggestions[selectedIndex]) {
|
||||
onSuggestionSelect(suggestions[selectedIndex]);
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
onIgnore(); // Close tooltip on escape
|
||||
break;
|
||||
case '1':
|
||||
case '2':
|
||||
case '3':
|
||||
case '4':
|
||||
case '5':
|
||||
e.preventDefault();
|
||||
const numIndex = parseInt(e.key) - 1;
|
||||
if (suggestions[numIndex]) {
|
||||
onSuggestionSelect(suggestions[numIndex]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isVisible, suggestions, selectedIndex, onSuggestionSelect, onIgnore]);
|
||||
|
||||
// Reset selected index when suggestions change
|
||||
useEffect(() => {
|
||||
setSelectedIndex(0);
|
||||
}, [suggestions]);
|
||||
|
||||
// Auto-focus tooltip when it becomes visible
|
||||
useEffect(() => {
|
||||
if (isVisible && tooltipRef.current) {
|
||||
tooltipRef.current.focus();
|
||||
}
|
||||
}, [isVisible]);
|
||||
|
||||
if (!isVisible) {
|
||||
console.log('🖱️ TOOLTIP COMPONENT: Not visible, returning null');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('🖱️ TOOLTIP COMPONENT: Rendering visible tooltip');
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
tabIndex={-1} // Make it focusable for keyboard events
|
||||
data-spell-tooltip="true" // For click detection
|
||||
className="fixed z-50 bg-background-default border border-border-default rounded-lg shadow-xl py-2 min-w-48 max-w-64 outline-none"
|
||||
style={{
|
||||
left: adjustedPosition.x,
|
||||
top: adjustedPosition.y - 8, // Position slightly above the word
|
||||
transform: 'translateX(-50%) translateY(-100%)', // Will be overridden by smart positioning
|
||||
boxShadow: '0 10px 25px rgba(0, 0, 0, 0.15), 0 4px 6px rgba(0, 0, 0, 0.1)',
|
||||
}}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="px-3 py-1 text-xs text-text-muted border-b border-border-subtle mb-1 font-medium">
|
||||
Suggestions for "<span className="text-red-600 dark:text-red-400 font-semibold">{misspelledWord}</span>"
|
||||
</div>
|
||||
|
||||
{/* Suggestions */}
|
||||
{suggestions.length > 0 ? (
|
||||
<div className="max-h-32 overflow-y-auto">
|
||||
{suggestions.slice(0, 5).map((suggestion, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('🖱️ SUGGESTION CLICKED:', suggestion);
|
||||
onSuggestionSelect(suggestion);
|
||||
}}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
className={`w-full text-left px-3 py-2 text-sm transition-all duration-150 flex items-center gap-2 ${
|
||||
selectedIndex === index
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-900 dark:text-blue-100 border-l-2 border-blue-500'
|
||||
: 'text-text-default hover:bg-background-subtle'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`w-5 h-5 flex items-center justify-center text-xs rounded text-[10px] font-bold ${
|
||||
selectedIndex === index
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-text-muted text-white'
|
||||
}`}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
<span className="font-medium truncate">{suggestion}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-3 py-2 text-sm text-text-muted italic">
|
||||
No suggestions available
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Separator */}
|
||||
<div className="border-t border-border-subtle my-1" />
|
||||
|
||||
{/* Additional actions */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('🖱️ ADD TO DICTIONARY CLICKED');
|
||||
onAddToDictionary();
|
||||
}}
|
||||
className="w-full text-left px-3 py-1.5 text-xs text-text-muted hover:bg-background-subtle transition-colors flex items-center gap-2"
|
||||
>
|
||||
<span className="text-green-600 dark:text-green-400">+</span>
|
||||
Add to dictionary
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('🖱️ IGNORE CLICKED');
|
||||
onIgnore();
|
||||
}}
|
||||
className="w-full text-left px-3 py-1.5 text-xs text-text-muted hover:bg-background-subtle transition-colors flex items-center gap-2"
|
||||
>
|
||||
<span className="text-text-muted">×</span>
|
||||
Ignore word
|
||||
</button>
|
||||
|
||||
{/* Keyboard hints */}
|
||||
<div className="px-3 py-1 text-[10px] text-text-muted border-t border-border-subtle mt-1">
|
||||
Press 1-5 to select • ↑↓ to navigate • Enter to apply • Esc to close
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpellCheckTooltip;
|
||||
@@ -2136,298 +2136,7 @@ async function appMain() {
|
||||
}
|
||||
});
|
||||
|
||||
// Handle spell checking requests using system spell checker
|
||||
ipcMain.handle('spell-check', async (event, word: string) => {
|
||||
try {
|
||||
console.log('[Main] System spell check request for word:', word);
|
||||
|
||||
if (!word || typeof word !== 'string') {
|
||||
return true; // Assume correct for invalid input
|
||||
}
|
||||
|
||||
// Skip very short words (less than 3 characters)
|
||||
if (word.length < 3) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const cleanWord = word.trim();
|
||||
|
||||
try {
|
||||
// Use system spell checker based on platform
|
||||
if (process.platform === 'darwin') {
|
||||
// macOS: Use aspell (now that it's installed)
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const aspellProcess = spawn('aspell', ['-a'], {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: 3000
|
||||
});
|
||||
|
||||
let output = '';
|
||||
|
||||
aspellProcess.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
aspellProcess.on('close', (code) => {
|
||||
// Parse aspell output
|
||||
const lines = output.split('\n').filter(line => line.trim());
|
||||
let isCorrect = true;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('*')) {
|
||||
// Word is correct
|
||||
isCorrect = true;
|
||||
break;
|
||||
} else if (line.startsWith('&') || line.startsWith('#')) {
|
||||
// Word is misspelled
|
||||
isCorrect = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Main] macOS aspell spell check result for', word, ':', isCorrect);
|
||||
resolve(isCorrect);
|
||||
});
|
||||
|
||||
aspellProcess.on('error', (error) => {
|
||||
console.error('[Main] aspell error:', error);
|
||||
resolve(true); // Default to correct if aspell not available
|
||||
});
|
||||
|
||||
aspellProcess.stdin.write(cleanWord + '\n');
|
||||
aspellProcess.stdin.end();
|
||||
|
||||
setTimeout(() => {
|
||||
aspellProcess.kill();
|
||||
resolve(true);
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
} else if (process.platform === 'win32') {
|
||||
// Windows: Try to use hunspell or fall back to basic check
|
||||
return new Promise((resolve) => {
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
// Try hunspell first (if available)
|
||||
const hunspellProcess = spawn('hunspell', ['-d', 'en_US'], {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: 3000
|
||||
});
|
||||
|
||||
let output = '';
|
||||
|
||||
hunspellProcess.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
hunspellProcess.on('close', (code) => {
|
||||
// hunspell returns "*" for correct words, "&" for incorrect
|
||||
const isCorrect = output.includes('*') || output.trim() === '';
|
||||
console.log('[Main] Windows spell check result for', word, ':', isCorrect);
|
||||
resolve(isCorrect);
|
||||
});
|
||||
|
||||
hunspellProcess.on('error', (error) => {
|
||||
console.error('[Main] hunspell not available, defaulting to correct:', error);
|
||||
resolve(true); // Default to correct if hunspell not available
|
||||
});
|
||||
|
||||
hunspellProcess.stdin.write(cleanWord + '\n');
|
||||
hunspellProcess.stdin.end();
|
||||
|
||||
setTimeout(() => {
|
||||
hunspellProcess.kill();
|
||||
resolve(true);
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
} else {
|
||||
// Linux: Use aspell or hunspell
|
||||
return new Promise((resolve) => {
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
const aspellProcess = spawn('aspell', ['-a'], {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: 3000
|
||||
});
|
||||
|
||||
let output = '';
|
||||
|
||||
aspellProcess.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
aspellProcess.on('close', (code) => {
|
||||
// Parse aspell output
|
||||
const lines = output.split('\n').filter(line => line.trim());
|
||||
let isCorrect = true;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('*')) {
|
||||
isCorrect = true;
|
||||
break;
|
||||
} else if (line.startsWith('&') || line.startsWith('#')) {
|
||||
isCorrect = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Main] Linux spell check result for', word, ':', isCorrect);
|
||||
resolve(isCorrect);
|
||||
});
|
||||
|
||||
aspellProcess.on('error', (error) => {
|
||||
console.error('[Main] aspell error:', error);
|
||||
resolve(true); // Default to correct if aspell not available
|
||||
});
|
||||
|
||||
aspellProcess.stdin.write(cleanWord + '\n');
|
||||
aspellProcess.stdin.end();
|
||||
|
||||
setTimeout(() => {
|
||||
aspellProcess.kill();
|
||||
resolve(true);
|
||||
}, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Main] Error using system spell checker:', error);
|
||||
return true; // Default to correct on error
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in system spell-check handler:', error);
|
||||
return true; // Assume correct on error
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('spell-suggestions', async (event, word: string) => {
|
||||
try {
|
||||
console.log('[Main] System spell suggestions request for word:', word);
|
||||
|
||||
if (!word || typeof word !== 'string') {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Skip very short words
|
||||
if (word.length < 3) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const cleanWord = word.trim();
|
||||
|
||||
try {
|
||||
// Get suggestions using system spell checker based on platform
|
||||
if (process.platform === 'darwin' || process.platform === 'linux') {
|
||||
// macOS and Linux: Use aspell for suggestions
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const aspellProcess = spawn('aspell', ['-a'], {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: 3000
|
||||
});
|
||||
|
||||
let output = '';
|
||||
|
||||
aspellProcess.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
aspellProcess.on('close', (code) => {
|
||||
// Parse aspell output for suggestions
|
||||
const lines = output.split('\n').filter(line => line.trim());
|
||||
let suggestions: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('&')) {
|
||||
// Line format: & word count offset: suggestion1, suggestion2, ...
|
||||
const parts = line.split(':');
|
||||
if (parts.length > 1) {
|
||||
const suggestionsPart = parts[1].trim();
|
||||
suggestions = suggestionsPart.split(',').map(s => s.trim()).slice(0, 5); // Limit to 5 suggestions
|
||||
}
|
||||
break;
|
||||
} else if (line.startsWith('#')) {
|
||||
// No suggestions available
|
||||
suggestions = [];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Main] aspell spell suggestions for', word, ':', suggestions);
|
||||
resolve(suggestions);
|
||||
});
|
||||
|
||||
aspellProcess.on('error', (error) => {
|
||||
console.error('[Main] aspell error getting suggestions:', error);
|
||||
resolve([]); // Return empty array on error
|
||||
});
|
||||
|
||||
aspellProcess.stdin.write(cleanWord + '\n');
|
||||
aspellProcess.stdin.end();
|
||||
|
||||
setTimeout(() => {
|
||||
aspellProcess.kill();
|
||||
resolve([]);
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
} else if (process.platform === 'win32') {
|
||||
// Windows: Try to use hunspell for suggestions
|
||||
return new Promise((resolve) => {
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
const hunspellProcess = spawn('hunspell', ['-d', 'en_US', '-s'], {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: 3000
|
||||
});
|
||||
|
||||
let output = '';
|
||||
|
||||
hunspellProcess.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
hunspellProcess.on('close', (code) => {
|
||||
// Parse hunspell suggestions
|
||||
const lines = output.split('\n').filter(line => line.trim());
|
||||
const suggestions = lines.slice(0, 5); // Limit to 5 suggestions
|
||||
|
||||
console.log('[Main] hunspell spell suggestions for', word, ':', suggestions);
|
||||
resolve(suggestions);
|
||||
});
|
||||
|
||||
hunspellProcess.on('error', (error) => {
|
||||
console.error('[Main] hunspell not available for suggestions:', error);
|
||||
resolve([]); // Return empty array if hunspell not available
|
||||
});
|
||||
|
||||
hunspellProcess.stdin.write(cleanWord + '\n');
|
||||
hunspellProcess.stdin.end();
|
||||
|
||||
setTimeout(() => {
|
||||
hunspellProcess.kill();
|
||||
resolve([]);
|
||||
}, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Main] Error getting spell suggestions:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in system spell-suggestions handler:', error);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user