Skip to content

Commit 14bdae0

Browse files
RyeMuttclaude
andcommitted
Add build-time-selectable macOS NSSpellChecker engine
Add USE_NSSPELLCHECKER (DARWIN-only, default OFF) which compiles llspellcheckengine_mac.mm instead of the Hunspell engine and links AppKit + Foundation. The engine maps the viewer's dictionary names to NSSpellChecker language codes (via the shared LLSpellCheckEngine::matchLanguage helper) and answers checkWord/getSuggestions via the shared system checker. The SL glossary and user custom/ignore words are handled by LLSpellChecker's accepted-word set, so they never touch the user's global system dictionary. include(Hunspell) is skipped when this engine is selected, so a macOS-native build no longer requires Hunspell. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a6d9b27 commit 14bdae0

4 files changed

Lines changed: 231 additions & 6 deletions

File tree

indra/CMakeLists.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ else ()
7676
option(USE_SDL_WINDOW "Build with SDL based window management and input (experimental on Windows)" OFF)
7777
endif ()
7878

79+
# Spell checking backend
80+
cmake_dependent_option(USE_NSSPELLCHECKER "Use the native macOS NSSpellChecker instead of Hunspell" OFF "DARWIN" OFF)
81+
7982
# Configure crash reporting
8083
option(RELEASE_CRASH_REPORTING "Enable use of crash reporting in release builds" OFF)
8184
option(NON_RELEASE_CRASH_REPORTING "Enable use of crash reporting in developer builds" OFF)
@@ -267,14 +270,17 @@ include(GLIB)
267270
include(GLM)
268271
include(Harfbuzz)
269272
include(Havok)
270-
include(Hunspell)
273+
if (NOT USE_NSSPELLCHECKER)
274+
include(Hunspell) # only the default (Hunspell) spell-check engine needs it
275+
endif ()
271276
include(JPEG)
272277
include(LLKDU)
273278
include(LLPhysicsExtensions)
274279
include(Mesa)
275280
include(Meshoptimizer)
276281
include(Mikktspace)
277282
include(NDOF)
283+
include(NSSpellChecker)
278284
include(NVAPI)
279285
include(OpenAL)
280286
include(OpenGL)

indra/cmake/NSSpellChecker.cmake

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# -*- cmake -*-
2+
3+
include_guard()
4+
5+
if (USE_NSSPELLCHECKER)
6+
# Link target for the macOS NSSpellChecker engine (llspellcheckengine_mac.mm). No compile
7+
# definitions are needed: the engine is selected by which source CMake compiles, and the shared
8+
# llspellcheck.h is platform-clean (it only forward-declares the abstract LLSpellCheckEngine).
9+
add_library(ll::nsspellchecker INTERFACE IMPORTED)
10+
target_link_libraries(ll::nsspellchecker INTERFACE "-framework AppKit" "-framework Foundation")
11+
endif ()

indra/llui/CMakeLists.txt

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,15 @@ target_sources(llui
234234
llxyvector.h
235235
)
236236

237-
# Spell checking: the backend-agnostic LLSpellChecker core plus the Hunspell engine.
238-
target_sources(llui PRIVATE llspellcheck.cpp llspellcheckengine_hunspell.cpp)
237+
# Spell checking: the backend-agnostic LLSpellChecker core (always compiled) plus exactly one
238+
# platform engine selected by build option — native macOS NSSpellChecker or Hunspell (default).
239+
target_sources(llui PRIVATE llspellcheck.cpp)
240+
if (USE_NSSPELLCHECKER)
241+
target_sources(llui PRIVATE llspellcheckengine_mac.mm)
242+
set_source_files_properties(llspellcheckengine_mac.mm PROPERTIES SKIP_PRECOMPILE_HEADERS TRUE)
243+
else ()
244+
target_sources(llui PRIVATE llspellcheckengine_hunspell.cpp)
245+
endif ()
239246

240247
SET(llurlentry_TEST_DEPENDENCIES
241248
llurlmatch.cpp
@@ -262,14 +269,24 @@ target_link_libraries(llui
262269
llxml
263270
llmath
264271
llcommon
265-
ll::hunspell
266272
)
267273

274+
if (USE_NSSPELLCHECKER)
275+
target_link_libraries(llui PUBLIC ll::nsspellchecker)
276+
else ()
277+
target_link_libraries(llui PUBLIC ll::hunspell)
278+
endif ()
279+
268280
target_precompile_headers(llui REUSE_FROM llprecompiled)
269281

270282
# Add tests
271283
if(BUILD_TESTING)
272-
set(test_libs llmessage llcorehttp llxml llrender llcommon ll::hunspell)
284+
if (USE_NSSPELLCHECKER)
285+
set(spellcheck_lib ll::nsspellchecker)
286+
else ()
287+
set(spellcheck_lib ll::hunspell)
288+
endif ()
289+
set(test_libs llmessage llcorehttp llxml llrender llcommon ${spellcheck_lib})
273290

274291
SET(llui_TEST_SOURCE_FILES
275292
llurlmatch.cpp
@@ -278,7 +295,7 @@ if(BUILD_TESTING)
278295
LL_ADD_PROJECT_UNIT_TESTS(llui "${llui_TEST_SOURCE_FILES}")
279296

280297
# INTEGRATION TESTS
281-
set(test_libs llui llmessage llcorehttp llxml llrender llcommon ll::hunspell)
298+
set(test_libs llui llmessage llcorehttp llxml llrender llcommon ${spellcheck_lib})
282299
set(test_project llui)
283300
LL_ADD_INTEGRATION_TEST(llurlentry llurlentry.cpp "${test_libs}" "${test_project}")
284301
LL_ADD_INTEGRATION_TEST(llemojidictionary llemojidictionary.cpp "${test_libs}" "${test_project}")
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/**
2+
* @file llspellcheckengine_mac.mm
3+
* @brief Spell-check engine backed by the native macOS NSSpellChecker
4+
*
5+
* $LicenseInfo:firstyear=2001&license=viewerlgpl$
6+
* Second Life Viewer Source Code
7+
* Copyright (C) 2010, Linden Research, Inc.
8+
*
9+
* This library is free software; you can redistribute it and/or
10+
* modify it under the terms of the GNU Lesser General Public
11+
* License as published by the Free Software Foundation;
12+
* version 2.1 of the License only.
13+
*
14+
* This library is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17+
* Lesser General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU Lesser General Public
20+
* License along with this library; if not, write to the Free Software
21+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
22+
*
23+
* Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
24+
* $/LicenseInfo$
25+
*/
26+
27+
#include "linden_common.h"
28+
29+
#include "llspellcheckengine.h"
30+
31+
#import <AppKit/AppKit.h>
32+
33+
namespace
34+
{
35+
NSSpellChecker* nsChecker()
36+
{
37+
return [NSSpellChecker sharedSpellChecker];
38+
}
39+
40+
// std::string (UTF-8) -> NSString. Returns nil on invalid UTF-8 (callers must guard).
41+
NSString* toNS(const std::string& str)
42+
{
43+
return [NSString stringWithUTF8String:str.c_str()];
44+
}
45+
46+
std::string fromNS(NSString* str)
47+
{
48+
if (!str)
49+
{
50+
return std::string();
51+
}
52+
const char* utf8 = [str UTF8String];
53+
return (utf8) ? std::string(utf8) : std::string();
54+
}
55+
56+
class LLNSSpellEngine final : public LLSpellCheckEngine
57+
{
58+
public:
59+
bool setLanguage(const std::string& name) override;
60+
bool isActive() const override { return mEnabled; }
61+
bool checkWord(const std::string& word) const override;
62+
S32 getSuggestions(const std::string& word, std::vector<std::string>& suggestions) const override;
63+
std::set<std::string> getInstalledLanguages(const std::vector<std::string>& candidate_names) const override;
64+
private:
65+
// The spell checker's installed language codes (e.g. "en_US"), as UTF-8 strings.
66+
static std::vector<std::string> availableTags();
67+
68+
bool mEnabled = false;
69+
std::string mNSLanguage; // Resolved NSSpellChecker language code (e.g. "en_US")
70+
};
71+
72+
// static
73+
std::vector<std::string> LLNSSpellEngine::availableTags()
74+
{
75+
std::vector<std::string> tags;
76+
@autoreleasepool
77+
{
78+
for (NSString* lang in [nsChecker() availableLanguages])
79+
{
80+
tags.push_back(fromNS(lang));
81+
}
82+
}
83+
return tags;
84+
}
85+
86+
bool LLNSSpellEngine::setLanguage(const std::string& name)
87+
{
88+
llassert([NSThread isMainThread]);
89+
mEnabled = false;
90+
mNSLanguage.clear();
91+
if (name.empty())
92+
{
93+
return false;
94+
}
95+
96+
const std::string ns_lang = LLSpellCheckEngine::matchLanguage(name, availableTags(), '_');
97+
if (ns_lang.empty())
98+
{
99+
return false;
100+
}
101+
102+
@autoreleasepool
103+
{
104+
NSString* ns_lang_str = toNS(ns_lang);
105+
// setLanguage: returns NO if the language can't be matched; treat that as disabled.
106+
if ( (!ns_lang_str) || (![nsChecker() setLanguage:ns_lang_str]) )
107+
{
108+
return false;
109+
}
110+
}
111+
mNSLanguage = ns_lang;
112+
mEnabled = true;
113+
return true;
114+
}
115+
116+
bool LLNSSpellEngine::checkWord(const std::string& word) const
117+
{
118+
// NSSpellChecker is AppKit; use it only on the main thread (where text widgets lay out).
119+
llassert([NSThread isMainThread]);
120+
if (!mEnabled)
121+
{
122+
return true;
123+
}
124+
125+
@autoreleasepool
126+
{
127+
NSString* ns_word = toNS(word);
128+
if (!ns_word)
129+
{
130+
return true;
131+
}
132+
NSRange range = [nsChecker() checkSpellingOfString:ns_word
133+
startingAt:0
134+
language:toNS(mNSLanguage)
135+
wrap:NO
136+
inSpellDocumentWithTag:0
137+
wordCount:NULL];
138+
return (range.location == NSNotFound);
139+
}
140+
}
141+
142+
S32 LLNSSpellEngine::getSuggestions(const std::string& word, std::vector<std::string>& suggestions) const
143+
{
144+
llassert([NSThread isMainThread]);
145+
suggestions.clear();
146+
if (!mEnabled)
147+
{
148+
return 0;
149+
}
150+
151+
@autoreleasepool
152+
{
153+
NSString* ns_word = toNS(word);
154+
if (!ns_word)
155+
{
156+
return 0;
157+
}
158+
// The range must be expressed in NSString (UTF-16) units, not std::string bytes.
159+
NSArray* guesses = [nsChecker() guessesForWordRange:NSMakeRange(0, [ns_word length])
160+
inString:ns_word
161+
language:toNS(mNSLanguage)
162+
inSpellDocumentWithTag:0];
163+
for (NSString* guess in guesses)
164+
{
165+
suggestions.push_back(fromNS(guess));
166+
}
167+
}
168+
return static_cast<S32>(suggestions.size());
169+
}
170+
171+
std::set<std::string> LLNSSpellEngine::getInstalledLanguages(const std::vector<std::string>& candidate_names) const
172+
{
173+
llassert([NSThread isMainThread]);
174+
std::set<std::string> installed;
175+
const std::vector<std::string> available = availableTags(); // one OS query for the batch
176+
for (const std::string& name : candidate_names)
177+
{
178+
if (!LLSpellCheckEngine::matchLanguage(name, available, '_').empty())
179+
{
180+
installed.insert(name);
181+
}
182+
}
183+
return installed;
184+
}
185+
}
186+
187+
// static
188+
std::unique_ptr<LLSpellCheckEngine> LLSpellCheckEngine::create()
189+
{
190+
return std::make_unique<LLNSSpellEngine>();
191+
}

0 commit comments

Comments
 (0)