Coverage Report

Created: 2026-03-31 11:00

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libreoffice/sc/source/ui/view/spellcheckcontext.cxx
Line
Count
Source
1
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
/*
3
 * This file is part of the LibreOffice project.
4
 *
5
 * This Source Code Form is subject to the terms of the Mozilla Public
6
 * License, v. 2.0. If a copy of the MPL was not distributed with this
7
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8
 */
9
10
#include <spellcheckcontext.hxx>
11
#include <svl/sharedstring.hxx>
12
#include <editeng/eeitem.hxx>
13
#include <editeng/langitem.hxx>
14
#include <editeng/unolingu.hxx>
15
#include <vcl/dropcache.hxx>
16
17
#include <scitems.hxx>
18
#include <document.hxx>
19
#include <cellvalue.hxx>
20
#include <editutil.hxx>
21
#include <dpobject.hxx>
22
23
#include <com/sun/star/linguistic2/XSpellChecker1.hpp>
24
25
#include <o3tl/hash_combine.hxx>
26
27
#include <unordered_map>
28
29
using namespace css;
30
31
using sc::SpellCheckContext;
32
33
class SpellCheckContext::SpellCheckCache : public CacheOwner
34
{
35
    struct CellPos
36
    {
37
        struct Hash
38
        {
39
            size_t operator() (const CellPos& rPos) const
40
0
            {
41
0
                std::size_t seed = 0;
42
0
                o3tl::hash_combine(seed, rPos.mnCol);
43
0
                o3tl::hash_combine(seed, rPos.mnRow);
44
0
                return seed;
45
0
            }
46
        };
47
48
        SCCOL mnCol;
49
        SCROW mnRow;
50
51
0
        CellPos(SCCOL nCol, SCROW nRow) : mnCol(nCol), mnRow(nRow) {}
52
53
        bool operator== (const CellPos& r) const
54
0
        {
55
0
            return mnCol == r.mnCol && mnRow == r.mnRow;
56
0
        }
57
58
    };
59
60
    struct LangSharedString
61
    {
62
        struct Hash
63
        {
64
            size_t operator() (const LangSharedString& rKey) const
65
0
            {
66
0
                std::size_t seed = 0;
67
0
                o3tl::hash_combine(seed, rKey.meLang.get());
68
0
                o3tl::hash_combine(seed, rKey.mpString);
69
0
                return seed;
70
0
            }
71
        };
72
73
        LanguageType meLang;
74
        const rtl_uString* mpString;
75
76
        LangSharedString(LanguageType eLang, const ScRefCellValue& rCell)
77
0
            : meLang(eLang)
78
0
            , mpString(rCell.getSharedString()->getData())
79
0
        {
80
0
        }
81
82
        bool operator== (const LangSharedString& r) const
83
0
        {
84
0
            return meLang == r.meLang && mpString == r.mpString;
85
0
        }
86
    };
87
88
#if defined __cpp_lib_memory_resource
89
    typedef std::pmr::unordered_map<CellPos, std::unique_ptr<MisspellRangesVec>, CellPos::Hash> CellMapType;
90
    typedef std::pmr::unordered_map<LangSharedString, std::unique_ptr<MisspellRangesVec>, LangSharedString::Hash> SharedStringMapType;
91
#else
92
    typedef std::unordered_map<CellPos, std::unique_ptr<MisspellRangesVec>, CellPos::Hash> CellMapType;
93
    typedef std::unordered_map<LangSharedString, std::unique_ptr<MisspellRangesVec>, LangSharedString::Hash> SharedStringMapType;
94
#endif
95
96
    SharedStringMapType  maStringMisspells;
97
    CellMapType          maEditTextMisspells;
98
99
    virtual OUString getCacheName() const override
100
0
    {
101
0
        return "SpellCheckCache";
102
0
    }
103
104
    virtual bool dropCaches() override
105
0
    {
106
0
        clear();
107
0
        return true;
108
0
    }
109
110
    virtual void dumpState(rtl::OStringBuffer& rState) override
111
0
    {
112
0
        rState.append("\nSpellCheckCache:\t");
113
0
        rState.append("\t string misspells: ");
114
0
        rState.append(static_cast<sal_Int32>(maStringMisspells.size()));
115
0
        rState.append("\t editeng misspells: ");
116
0
        rState.append(static_cast<sal_Int32>(maEditTextMisspells.size()));
117
0
    }
118
119
public:
120
121
    SpellCheckCache()
122
#if defined __cpp_lib_memory_resource
123
0
        : maStringMisspells(&CacheOwner::GetMemoryResource())
124
0
        , maEditTextMisspells(&CacheOwner::GetMemoryResource())
125
#endif
126
0
    {
127
0
    }
128
129
    bool query(SCCOL nCol, SCROW nRow, LanguageType eLang,
130
               const ScRefCellValue& rCell, MisspellRangesVec*& rpRanges) const
131
0
    {
132
0
        CellType eType = rCell.getType();
133
0
        if (eType == CELLTYPE_STRING)
134
0
        {
135
0
            SharedStringMapType::const_iterator it = maStringMisspells.find(LangSharedString(eLang, rCell));
136
0
            if (it == maStringMisspells.end())
137
0
                return false; // Not available
138
139
0
            rpRanges = it->second.get();
140
0
            return true;
141
0
        }
142
143
0
        if (eType == CELLTYPE_EDIT)
144
0
        {
145
0
            CellMapType::const_iterator it = maEditTextMisspells.find(CellPos(nCol, nRow));
146
0
            if (it == maEditTextMisspells.end())
147
0
                return false; // Not available
148
149
0
            rpRanges = it->second.get();
150
0
            return true;
151
0
        }
152
153
0
        rpRanges = nullptr;
154
0
        return true;
155
0
    }
156
157
    void set(SCCOL nCol, SCROW nRow, LanguageType eLang,
158
             const ScRefCellValue& rCell, std::unique_ptr<MisspellRangesVec> pRanges)
159
0
    {
160
0
        CellType eType = rCell.getType();
161
0
        if (eType == CELLTYPE_STRING)
162
0
        {
163
0
            maStringMisspells.insert_or_assign(LangSharedString(eLang, rCell), std::move(pRanges));
164
0
        }
165
0
        else if (eType == CELLTYPE_EDIT)
166
0
            maEditTextMisspells.insert_or_assign(CellPos(nCol, nRow), std::move(pRanges));
167
0
    }
168
169
    void clear()
170
0
    {
171
0
        SharedStringMapType(maStringMisspells.get_allocator()).swap(maStringMisspells);
172
0
        CellMapType(maEditTextMisspells.get_allocator()).swap(maEditTextMisspells);
173
0
    }
174
175
    void clearEditTextMap()
176
0
    {
177
0
        maEditTextMisspells.clear();
178
0
    }
179
};
180
181
struct SpellCheckContext::SpellCheckStatus
182
{
183
    bool mbModified;
184
185
0
    SpellCheckStatus() : mbModified(false) {};
186
187
    DECL_LINK( EventHdl, EditStatus&, void );
188
};
189
190
IMPL_LINK(SpellCheckContext::SpellCheckStatus, EventHdl, EditStatus&, rStatus, void)
191
0
{
192
0
    EditStatusFlags nStatus = rStatus.GetStatusWord();
193
0
    if (nStatus & EditStatusFlags::WRONGWORDCHANGED)
194
0
        mbModified = true;
195
0
}
196
197
struct SpellCheckContext::SpellCheckResult
198
{
199
    SCCOL mnCol;
200
    SCROW mnRow;
201
    MisspellRangeResult maRanges;
202
203
0
    SpellCheckResult() : mnCol(-1), mnRow(-1) {}
204
205
    void set(SCCOL nCol, SCROW nRow, const MisspellRangeResult& rMisspells)
206
0
    {
207
0
        mnCol = nCol;
208
0
        mnRow = nRow;
209
0
        maRanges = rMisspells;
210
0
    }
211
212
    const MisspellRangeResult & query(SCCOL nCol, SCROW nRow) const
213
0
    {
214
0
        assert(mnCol == nCol);
215
0
        assert(mnRow == nRow);
216
0
        (void)nCol;
217
0
        (void)nRow;
218
0
        return maRanges;
219
0
    }
220
221
    void clear()
222
0
    {
223
0
        mnCol = -1;
224
0
        mnRow = -1;
225
0
        maRanges = {};
226
0
    }
227
};
228
229
SpellCheckContext::SpellCheckContext(ScDocument& rDocument, SCTAB nTab) :
230
0
    rDoc(rDocument),
231
0
    mnTab(nTab),
232
0
    meLanguage(ScGlobal::GetEditDefaultLanguage())
233
0
{
234
    // defer init of engine and cache till the first query/set
235
0
}
236
237
SpellCheckContext::~SpellCheckContext()
238
0
{
239
0
}
240
241
void SpellCheckContext::dispose()
242
0
{
243
0
    mpEngine.reset();
244
0
    mpCache.reset();
245
0
}
246
247
void SpellCheckContext::setTabNo(SCTAB nTab)
248
0
{
249
0
    if (mnTab == nTab)
250
0
        return;
251
0
    mnTab = nTab;
252
0
    reset();
253
0
}
254
255
bool SpellCheckContext::isMisspelled(SCCOL nCol, SCROW nRow) const
256
0
{
257
0
    const_cast<SpellCheckContext*>(this)->ensureResults(nCol, nRow);
258
0
    return mpResult->query(nCol, nRow).mpRanges;
259
0
}
260
261
const sc::MisspellRangeResult & SpellCheckContext::getMisspellRanges(
262
    SCCOL nCol, SCROW nRow ) const
263
0
{
264
0
    const_cast<SpellCheckContext*>(this)->ensureResults(nCol, nRow);
265
0
    return mpResult->query(nCol, nRow);
266
0
}
267
268
void SpellCheckContext::setMisspellRanges(
269
    SCCOL nCol, SCROW nRow, const sc::MisspellRangeResult& rRangeResult )
270
0
{
271
0
    if (!mpEngine || !mpCache)
272
0
        reset();
273
274
0
    ScRefCellValue aCell(rDoc, ScAddress(nCol, nRow, mnTab));
275
0
    CellType eType = aCell.getType();
276
277
0
    if (eType != CELLTYPE_STRING && eType != CELLTYPE_EDIT)
278
0
        return;
279
280
0
    const MisspellRangesVec* pRanges = rRangeResult.mpRanges;
281
0
    std::unique_ptr<MisspellRangesVec> pMisspells(pRanges ? new MisspellRangesVec(*pRanges) : nullptr);
282
0
    mpCache->set(nCol, nRow, rRangeResult.meCellLang, aCell, std::move(pMisspells));
283
0
}
284
285
void SpellCheckContext::reset()
286
0
{
287
0
    meLanguage = ScGlobal::GetEditDefaultLanguage();
288
0
    resetCache();
289
0
    mpEngine.reset();
290
0
    mpStatus.reset();
291
0
}
292
293
void SpellCheckContext::resetForContentChange()
294
0
{
295
0
    resetCache(true /* bContentChangeOnly */);
296
0
}
297
298
void SpellCheckContext::ensureResults(SCCOL nCol, SCROW nRow)
299
0
{
300
0
    if (!mpEngine || !mpCache ||
301
0
        ScGlobal::GetEditDefaultLanguage() != meLanguage)
302
0
    {
303
0
        reset();
304
0
        setup();
305
0
    }
306
307
    // perhaps compute the pivot rangelist once in some pivot-table change handler ?
308
0
    if (rDoc.HasPivotTable())
309
0
    {
310
0
        if (ScDPCollection* pDPs = rDoc.GetDPCollection())
311
0
        {
312
0
            ScRangeList aPivotRanges = pDPs->GetAllTableRanges(mnTab);
313
0
            if (aPivotRanges.Contains(ScRange(ScAddress(nCol, nRow, mnTab)))) // Don't spell check within pivot tables
314
0
            {
315
0
                mpResult->set(nCol, nRow, {});
316
0
                return;
317
0
            }
318
0
        }
319
0
    }
320
321
0
    ScRefCellValue aCell(rDoc, ScAddress(nCol, nRow, mnTab));
322
0
    CellType eType = aCell.getType();
323
324
0
    if (eType != CELLTYPE_STRING && eType != CELLTYPE_EDIT)
325
0
    {
326
        // No spell-check required.
327
0
        mpResult->set(nCol, nRow, {});
328
0
        return;
329
0
    }
330
331
332
    // Cell content is either shared-string or EditTextObject
333
334
    // For spell-checking, we currently only use the primary
335
    // language; not CJK nor CTL.
336
0
    const ScPatternAttr* pPattern = rDoc.GetPattern(nCol, nRow, mnTab);
337
0
    LanguageType eCellLang = pPattern->GetItem(ATTR_FONT_LANGUAGE).GetValue();
338
339
0
    if (eCellLang == LANGUAGE_SYSTEM)
340
0
        eCellLang = meLanguage;   // never use SYSTEM for spelling
341
342
0
    if (eCellLang == LANGUAGE_NONE)
343
0
    {
344
0
        mpResult->set(nCol, nRow, {}); // No need to spell check this cell.
345
0
        return;
346
0
    }
347
348
0
    MisspellRangesVec* pCacheRanges = nullptr;
349
0
    bool bFound = mpCache->query(nCol, nRow, eCellLang, aCell, pCacheRanges);
350
0
    if (bFound)
351
0
    {
352
        // Cache hit.
353
0
        mpResult->set(nCol, nRow, MisspellRangeResult(pCacheRanges, eCellLang));
354
0
        return;
355
0
    }
356
357
    // Cache miss, the cell needs spell-check..
358
0
    if (eType == CELLTYPE_STRING)
359
0
        mpEngine->SetText(aCell.getSharedString()->getString());
360
0
    else
361
0
        mpEngine->SetText(*aCell.getEditText());
362
363
    // it has to happen after we set text
364
0
    mpEngine->SetDefaultItem(SvxLanguageItem(eCellLang, EE_CHAR_LANGUAGE));
365
366
0
    mpStatus->mbModified = false;
367
0
    mpEngine->CompleteOnlineSpelling();
368
0
    std::unique_ptr<MisspellRangesVec> pRanges;
369
0
    if (mpStatus->mbModified)
370
0
    {
371
0
        pRanges.reset(new MisspellRangesVec);
372
0
        mpEngine->GetAllMisspellRanges(*pRanges);
373
374
0
        if (pRanges->empty())
375
0
            pRanges.reset(nullptr);
376
0
    }
377
    // else : No change in status for EditStatusFlags::WRONGWORDCHANGED => no spell errors (which is the default status).
378
379
0
    mpResult->set(nCol, nRow, MisspellRangeResult(pRanges.get(), eCellLang));
380
0
    mpCache->set(nCol, nRow, eCellLang, aCell, std::move(pRanges));
381
0
}
382
383
void SpellCheckContext::resetCache(bool bContentChangeOnly)
384
0
{
385
0
    if (!mpResult)
386
0
        mpResult.reset(new SpellCheckResult());
387
0
    else
388
0
        mpResult->clear();
389
390
0
    if (!mpCache)
391
0
        mpCache.reset(new SpellCheckCache);
392
0
    else if (bContentChangeOnly)
393
0
        mpCache->clearEditTextMap();
394
0
    else
395
0
        mpCache->clear();
396
0
}
397
398
void SpellCheckContext::setup()
399
0
{
400
0
    mpEngine.reset(new ScTabEditEngine(rDoc));
401
0
    mpStatus.reset(new SpellCheckStatus());
402
403
0
    mpEngine->SetControlWord(
404
0
        mpEngine->GetControlWord() | (EEControlBits::ONLINESPELLING | EEControlBits::ALLOWBIGOBJS));
405
0
    mpEngine->SetStatusEventHdl(LINK(mpStatus.get(), SpellCheckStatus, EventHdl));
406
    //  Delimiters here like in inputhdl.cxx !!!
407
0
    mpEngine->SetWordDelimiters(
408
0
                ScEditUtil::ModifyDelimiters(mpEngine->GetWordDelimiters()));
409
410
0
    uno::Reference<linguistic2::XSpellChecker1> xXSpellChecker1(LinguMgr::GetSpellChecker());
411
0
    mpEngine->SetSpeller(xXSpellChecker1);
412
0
    mpEngine->SetDefaultLanguage(meLanguage);
413
0
}
414
415
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */