From a169373ab7b38db1eb1c1bb5c2ff6289c5043104 Mon Sep 17 00:00:00 2001 From: Zach Hannum Date: Sun, 1 Jan 2023 23:32:35 -0500 Subject: [PATCH] Add ability to ignore suggestions and add suggestions to languagetool dictionary (#176) --- app/renderer/components/editor/Editor.tsx | 1 + .../components/editor/TooltipView.tsx | 84 ++++++++++++++++++- .../components/editor/language-tool/api.ts | 54 ++++++++++++ app/renderer/icons/DictionaryAddIcon.tsx | 19 +++++ app/renderer/icons/Icon.tsx | 4 +- app/renderer/icons/index.ts | 1 + 6 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 app/renderer/icons/DictionaryAddIcon.tsx diff --git a/app/renderer/components/editor/Editor.tsx b/app/renderer/components/editor/Editor.tsx index 17213c7..19686fb 100755 --- a/app/renderer/components/editor/Editor.tsx +++ b/app/renderer/components/editor/Editor.tsx @@ -86,6 +86,7 @@ const Editor = () => { const { signal } = abortController; checkText(previewContent, signal) .then((result) => { + console.log(result); const effects: StateEffect[] = []; if (result.matches) { diff --git a/app/renderer/components/editor/TooltipView.tsx b/app/renderer/components/editor/TooltipView.tsx index ba88475..819eb1a 100644 --- a/app/renderer/components/editor/TooltipView.tsx +++ b/app/renderer/components/editor/TooltipView.tsx @@ -2,8 +2,14 @@ import { EditorView } from '@codemirror/view'; import { useEffect, useRef, useState } from 'react'; import { useResizeObserver } from 'renderer/hooks'; import styled from 'styled-components'; +import useStore from 'renderer/store/useStore'; +import { DictionaryAddIcon } from 'renderer/icons'; +import { addToDictionary } from './language-tool/api'; import { TooltipLocation } from './extensions/proofreadTooltipHelper'; -import { clearUnderlinesInRange } from './extensions/proofreadUnderlines'; +import { + clearUnderlinesInRange, + proofreadUnderlineField, +} from './extensions/proofreadUnderlines'; type TooltipViewProps = { tooltip: TooltipLocation | null; @@ -46,14 +52,30 @@ const TooltipMessage = styled.div` padding: 7px; `; -const TooltipSuggestion = styled.div` +type TooltipSuggestionProps = { + color?: string; +}; + +const TooltipSuggestion = styled.div` color: ${(p) => p.theme.buttonPrimaryBg}; + ${(p) => p.color && `color: ${p.color};`} padding: 7px; &:hover { background-color: rgba(0, 0, 0, 0.2); } border-radius: 7px; cursor: pointer; + display: flex; + justify-content: flex-start; + align-items: center; +`; + +const TooltipBottom = styled.div` + display: flex; + padding-top: 7px; + gap: 5px; + flex-direction: row; + justify-content: flex-start; `; const tooltipPadding = 5; @@ -69,6 +91,50 @@ export const TooltipView = ({ const [message, setMessage] = useState(''); const [suggestions, setSuggestions] = useState([]); const [tooltipLoc, setTooltipLoc] = useState(null); + const [settings] = useStore((s) => [s.settings]); + + const handleDismiss = () => { + if (editorView && tooltipLoc && tooltip) { + const clearUnderlineEffect = clearUnderlinesInRange.of({ + from: tooltip.from, + to: tooltip.to, + }); + editorView.dispatch({ + effects: [clearUnderlineEffect], + }); + } + }; + + const handleAddToDictionary = () => { + if (editorView && tooltipLoc && tooltip) { + const word = editorView.state.sliceDoc(tooltip.from, tooltip.to); + addToDictionary(word) + .then((result) => { + if (!result.added) { + console.error('Failed to add word to dictionary'); + } + return null; + }) + .catch((e) => { + console.error(e); + }); + + const underlines = editorView.state.field(proofreadUnderlineField); + + underlines.between(0, editorView.state.doc.length, (from, to) => { + const underline = editorView.state.sliceDoc(from, to); + if (underline === word) { + const clearUnderlineEffect = clearUnderlinesInRange.of({ + from, + to, + }); + editorView.dispatch({ + effects: [clearUnderlineEffect], + }); + } + }); + } + }; const calculateTooltipPos = (): { top: number; left: number } => { if (tooltipRef.current && tooltipRef.current.parentElement && editorView) { @@ -274,6 +340,20 @@ export const TooltipView = ({ {suggestion} ))} + + + Dismiss + + {tooltipLoc?.match.rule.issueType === 'misspelling' && + settings.languageToolApiKey !== '' && ( + + + + )} + ); }; diff --git a/app/renderer/components/editor/language-tool/api.ts b/app/renderer/components/editor/language-tool/api.ts index 231b025..04ff426 100644 --- a/app/renderer/components/editor/language-tool/api.ts +++ b/app/renderer/components/editor/language-tool/api.ts @@ -64,6 +64,10 @@ export interface Category { name: string; } +export interface LanguageToolDictionaryResponse { + added: boolean; +} + export const checkText = async ( text: string, abortSignal: AbortSignal @@ -131,3 +135,53 @@ export const checkText = async ( } return body; }; + +export const addToDictionary = async ( + text: string +): Promise => { + const { languageToolEndpointUrl, languageToolUsername, languageToolApiKey } = + useStore.getState().settings; + + const requestParams: { [key: string]: string } = { + word: text, + }; + + if (languageToolEndpointUrl === 'https://api.languagetoolplus.com') { + requestParams.username = languageToolUsername; + requestParams.apiKey = languageToolApiKey; + } + + let response: Response; + + try { + response = await fetch(`${languageToolEndpointUrl}/v2/words/add`, { + method: 'POST', + body: Object.keys(requestParams) + .map((key) => { + return `${encodeURIComponent(key)}=${encodeURIComponent( + requestParams[key] + )}`; + }) + .join('&'), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json', + }, + }); + } catch (e) { + return Promise.reject(e); + } + + if (!response.ok) { + return Promise.reject( + new Error(`unexpected status ${response.status}, see network tab`) + ); + } + let body: { added: boolean }; + try { + body = await response.json(); + } catch (e) { + return Promise.reject(e); + } + return body; +}; diff --git a/app/renderer/icons/DictionaryAddIcon.tsx b/app/renderer/icons/DictionaryAddIcon.tsx new file mode 100644 index 0000000..831600d --- /dev/null +++ b/app/renderer/icons/DictionaryAddIcon.tsx @@ -0,0 +1,19 @@ +import Icon from './Icon'; +import { IconProps, IconPropDefaults } from './type'; + +export const DictionaryAddIcon = (props: IconProps) => { + return ( + + + + ); +}; + +DictionaryAddIcon.defaultProps = { + ...IconPropDefaults, +}; diff --git a/app/renderer/icons/Icon.tsx b/app/renderer/icons/Icon.tsx index 25d408a..f4dd7f6 100755 --- a/app/renderer/icons/Icon.tsx +++ b/app/renderer/icons/Icon.tsx @@ -1,11 +1,11 @@ import styled from 'styled-components'; import { IconProps, IconPropDefaults } from './type'; -const IconDiv = styled.span` +const IconDiv = styled.div` height: ${(props) => props.size}; width: ${(props) => props.size}; overflow: hidden; - /* display: inline-block; */ + display: inline-block; `; const StyledSvg = styled.svg` diff --git a/app/renderer/icons/index.ts b/app/renderer/icons/index.ts index 17f5f18..4e26529 100755 --- a/app/renderer/icons/index.ts +++ b/app/renderer/icons/index.ts @@ -29,3 +29,4 @@ export { default as InfoIcon } from './InfoIcon'; export { default as CheckIcon } from './CheckIcon'; export { SpellCheckIcon } from './SpellCheckIcon'; export { AnnotationInfoIcon } from './AnnotationInfoIcon'; +export { DictionaryAddIcon } from './DictionaryAddIcon';