From 7be4aac74e97fd725aee604834147274baf0ce03 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Thu, 16 Apr 2026 08:01:58 +0200 Subject: [PATCH] more bug-fixes --- .../AdvancedEventLabDialog.tsx | 35 +++++- src/lib/languagetool-client.ts | 8 +- src/lib/languagetool-cm-linter.ts | 119 ++++++++++++------ src/lib/translate-client.ts | 14 +++ 4 files changed, 134 insertions(+), 42 deletions(-) diff --git a/src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx b/src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx index 63987368..1eb69f99 100644 --- a/src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx +++ b/src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx @@ -14,8 +14,9 @@ import { SelectTrigger, SelectValue } from '@/components/ui/select' +import logger from '@/lib/logger' import { isLanguageToolConfigured } from '@/lib/languagetool-client' -import { languageToolLintExtension } from '@/lib/languagetool-cm-linter' +import { languageToolLintExtension, requestAdvancedLabGrammarLint } from '@/lib/languagetool-cm-linter' import { buildLanguageToolPreferenceList } from '@/lib/languagetool-language-order' import type { AdvancedEventLabSlice } from '@/lib/advanced-event-lab-slice' import { @@ -224,6 +225,8 @@ export default function AdvancedEventLabDialog({ [i18nLanguage, i18n.language] ) const [ltLang, setLtLang] = useState(() => ltList[0] ?? 'en-US') + const ltLangRef = useRef(ltLang) + ltLangRef.current = ltLang const [translateLangs, setTranslateLangs] = useState([]) const [translateLoad, setTranslateLoad] = useState<'idle' | 'loading' | 'ready' | 'empty' | 'error'>('idle') const [translateSource, setTranslateSource] = useState('auto') @@ -235,6 +238,12 @@ export default function AdvancedEventLabDialog({ } }, [open, ltList]) + useEffect(() => { + const v = markupView.current + if (!v || !open || !isLanguageToolConfigured()) return + requestAdvancedLabGrammarLint(v) + }, [ltLang, open]) + useEffect(() => { if (!open || !isTranslateConfigured()) { setTranslateLangs([]) @@ -336,7 +345,7 @@ export default function AdvancedEventLabDialog({ }) ] if (isLanguageToolConfigured()) { - mkExtensions.push(languageToolLintExtension(ltLang, 650)) + mkExtensions.push(languageToolLintExtension(() => ltLangRef.current, 650)) } if (dark) mkExtensions.push(oneDark) @@ -407,7 +416,6 @@ export default function AdvancedEventLabDialog({ open, initial, markupMode, - ltLang, dark, destroyEditors, t, @@ -449,6 +457,11 @@ export default function AdvancedEventLabDialog({ toast.message(t('Advanced lab translation same source target')) return } + logger.info('[AdvancedLab] translate button', { + source: translateSource, + target: translateTarget, + inputChars: text.length + }) try { const out = await translatePlainText(text, translateTarget, translateSource) if (!markupView.current) return @@ -487,7 +500,13 @@ export default function AdvancedEventLabDialog({ {isLanguageToolConfigured() ? (
- { + logger.info('[AdvancedLab] grammar language changed', { from: ltLang, to: code }) + setLtLang(code) + }} + > @@ -513,6 +532,10 @@ export default function AdvancedEventLabDialog({ { + logger.info('[AdvancedLab] translation target language changed', { + from: translateTarget, + to: v + }) setTranslateTarget(v) if (translateSource !== 'auto' && v === translateSource) { setTranslateSource('auto') diff --git a/src/lib/languagetool-client.ts b/src/lib/languagetool-client.ts index c215a43e..2be6d2ae 100644 --- a/src/lib/languagetool-client.ts +++ b/src/lib/languagetool-client.ts @@ -46,7 +46,13 @@ export async function languageToolCheck( logger.warn('[LanguageTool] HTTP error', { status: res.status, errText: errText.slice(0, 200) }) throw new Error(`LanguageTool: ${res.status}`) } - return (await res.json()) as LanguageToolCheckResponse + const json = (await res.json()) as LanguageToolCheckResponse + logger.info('[AdvancedLab] LanguageTool check', { + language, + textChars: text.length, + matchCount: json.matches?.length ?? 0 + }) + return json } export function isLanguageToolConfigured(): boolean { diff --git a/src/lib/languagetool-cm-linter.ts b/src/lib/languagetool-cm-linter.ts index 2f697123..d74680fe 100644 --- a/src/lib/languagetool-cm-linter.ts +++ b/src/lib/languagetool-cm-linter.ts @@ -1,71 +1,111 @@ import { linter, type Diagnostic } from '@codemirror/lint' -import type { Extension } from '@codemirror/state' +import { Annotation, EditorSelection, type Extension } from '@codemirror/state' +import type { EditorView } from '@codemirror/view' import { languageToolCheck, type LanguageToolMatch } from '@/lib/languagetool-client' /** Local LanguageTool is slow on cold JVM; keep payloads bounded (LT has ~20–30k limits anyway). */ const MAX_CHECK_CHARS = 28_000 +/** + * Attach to a transaction so {@link languageToolLintExtension}'s `needsRefresh` schedules a new + * lint pass (e.g. grammar language changed). `forceLinting()` is a no-op once the last run finished. + */ +export const advancedLabGrammarLangRerun = Annotation.define() + +/** Request an immediate grammar re-check (after language change, etc.). */ +export function requestAdvancedLabGrammarLint(view: EditorView): void { + view.dispatch({ annotations: advancedLabGrammarLangRerun.of(true) }) +} + function matchToDiagnostic(docLen: number, m: LanguageToolMatch): Diagnostic | null { const from = Math.max(0, Math.min(m.offset, docLen)) const to = Math.max(from, Math.min(m.offset + m.length, docLen)) if (to <= from) return null const fix = m.replacements?.[0]?.value + const message = m.message + (m.rule?.id ? ` (${m.rule.id})` : '') + + if (!fix) { + return { from, to, severity: 'info', message } + } + + /** + * Do not use {@link Diagnostic.actions} for Apply: CodeMirror resolves actions via + * `findDiagnostic(..., diagnostic)` by **object identity**. Async LT refreshes replace + * diagnostics with new objects, so the tooltip's stale reference never matches and Apply + * never runs. A custom control in `renderMessage` uses stable closure `from`/`to`/`fix`. + */ return { from, to, severity: 'info', - message: m.message + (m.rule?.id ? ` (${m.rule.id})` : ''), - actions: fix - ? [ - { - name: 'Apply', - apply(view) { - view.dispatch({ changes: { from, to, insert: fix } }) - } - } - ] - : undefined + message, + renderMessage(view: EditorView) { + const wrap = document.createElement('span') + wrap.style.display = 'inline-flex' + wrap.style.alignItems = 'baseline' + wrap.style.gap = '0.4em' + + const btn = document.createElement('button') + btn.type = 'button' + btn.className = 'cm-diagnosticAction' + btn.textContent = 'Apply' + btn.addEventListener('click', (e) => { + e.preventDefault() + e.stopPropagation() + const len = view.state.doc.length + const f = Math.max(0, Math.min(from, len)) + const t = Math.max(f, Math.min(to, len)) + view.dispatch({ + changes: { from: f, to: t, insert: fix }, + selection: EditorSelection.cursor(f + fix.length) + }) + view.focus() + }) + wrap.appendChild(btn) + + const text = document.createElement('span') + text.textContent = message + wrap.appendChild(text) + + return wrap + } } } /** * Async grammar/style lint for CodeMirror using LanguageTool `/v2/check`. * Per-editor state (debounce / abort) lives in the closure so promises always settle and stale fetches are cancelled. + * `getLanguage` is read on each lint pass so the UI can change language without remounting the editor. */ -export function languageToolLintExtension(language: string, debounceMs: number): Extension { +export function languageToolLintExtension(getLanguage: () => string, debounceMs: number): Extension { let requestSeq = 0 let inFlight: AbortController | null = null - return linter((view) => { - return new Promise((resolve) => { - let settled = false - const finish = (diags: Diagnostic[]) => { - if (settled) return - settled = true - resolve(diags) - } - - const text = view.state.doc.toString() - if (text.length < 3) { - finish([]) - return - } - - const seq = ++requestSeq - inFlight?.abort() - inFlight = null + return linter( + (view) => { + return new Promise((resolve) => { + let settled = false + const finish = (diags: Diagnostic[]) => { + if (settled) return + settled = true + resolve(diags) + } - window.setTimeout(() => { - if (seq !== requestSeq) { + const text = view.state.doc.toString() + if (text.length < 3) { finish([]) return } + const seq = ++requestSeq + inFlight?.abort() + inFlight = null + const toSend = text.length > MAX_CHECK_CHARS ? text.slice(0, MAX_CHECK_CHARS) : text const ac = new AbortController() inFlight = ac - void languageToolCheck(toSend, language, ac.signal) + void languageToolCheck(toSend, getLanguage(), ac.signal) .then((res) => { if (seq !== requestSeq) { finish([]) @@ -82,7 +122,12 @@ export function languageToolLintExtension(language: string, debounceMs: number): .catch(() => { finish([]) }) - }, debounceMs) - }) - }) + }) + }, + { + delay: debounceMs, + needsRefresh: (update) => + update.transactions.some((tr) => tr.annotation(advancedLabGrammarLangRerun) === true) + } + ) } diff --git a/src/lib/translate-client.ts b/src/lib/translate-client.ts index dc7592f3..33ddc76a 100644 --- a/src/lib/translate-client.ts +++ b/src/lib/translate-client.ts @@ -119,6 +119,13 @@ export async function translatePlainText( const key = cacheKey(text, resolvedSource, resolvedTarget) const hit = memoryCache.get(key) if (hit && Date.now() - hit.at < CACHE_TTL_MS) { + logger.info('[AdvancedLab] translate', { + source: resolvedSource, + target: resolvedTarget, + inputChars: text.length, + outputChars: hit.text.length, + cacheHit: true + }) return hit.text } @@ -145,5 +152,12 @@ export async function translatePlainText( const out = data.translatedText ?? '' pruneMemory() memoryCache.set(key, { text: out, at: Date.now() }) + logger.info('[AdvancedLab] translate', { + source: resolvedSource, + target: resolvedTarget, + inputChars: text.length, + outputChars: out.length, + cacheHit: false + }) return out }