Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10,000 changes: 10,000 additions & 0 deletions assets/dictionary/words.txt

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions devtools_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
2 changes: 1 addition & 1 deletion example/macos/Podfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
platform :osx, '10.14'
platform :osx, '10.15'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
Expand Down
6 changes: 3 additions & 3 deletions example/macos/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
Expand Down Expand Up @@ -484,7 +484,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
Expand Down Expand Up @@ -531,7 +531,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
Expand Down
101 changes: 85 additions & 16 deletions lib/src/editor/block_component/rich_text/appflowy_rich_text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:appflowy_editor/src/service/spell_check/spell_checker.dart';
import 'package:appflowy_editor/src/editor/block_component/rich_text/spell_check_overlay.dart';

typedef TextSpanDecoratorForAttribute = InlineSpan Function(
BuildContext context,
Expand Down Expand Up @@ -145,6 +147,7 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
_buildPlaceholderText(context),
_buildRichText(context),
..._buildRichTextOverlay(context),
_buildSpellCheckOverlay(),
],
);

Expand Down Expand Up @@ -442,6 +445,16 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
[];
}

Widget _buildSpellCheckOverlay() {
if (textKey.currentContext == null) return const SizedBox.shrink();
return SpellCheckOverlay(
editorState: widget.editorState,
node: widget.node,
delegate: this,
misspelledCache: _misspelledCache,
);
}

void confirmContextEnabled() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && textKey.currentContext == null) {
Expand Down Expand Up @@ -611,29 +624,85 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
);
}
}
final textSpan = TextSpan(
text: textInsert.text,
style: textStyle,
);
textSpans.add(
textSpanDecoratorForAttribute != null
? textSpanDecoratorForAttribute!(
context,
widget.node,
offset,
textInsert,
textSpan,
widget.textSpanDecorator?.call(textSpan) ?? textSpan,
)
: textSpan,
);
// Split the insert text into word and non-word tokens so we can
// underline misspelled words and attach hover suggestion UI.
final tokenReg = RegExp(r"(\w+|[^\w]+)");
final tokens = tokenReg.allMatches(textInsert.text).map((m) => m.group(0)!).toList();
int innerIndex = 0;

for (final token in tokens) {
final isWord = RegExp(r"^\w+$").hasMatch(token);
if (isWord) {
final word = token;
final lc = word.toLowerCase();

// schedule async check for unknown words (only words >= 3 chars to avoid common short words)
// cache stored on state (to avoid repeated checks)
// IMPORTANT: Pass the original word (not lowercase) so capital letter check works!
if (!_misspelledCache.containsKey(lc) && word.length >= 3) {
_checkWord(word);
}

// Only mark as misspelled if we've checked it and confirmed it's wrong
final isMisspelled = _misspelledCache[lc] == true;

final spanStyle = isMisspelled
? textStyle.copyWith(
decoration: TextDecoration.underline,
decorationStyle: TextDecorationStyle.wavy,
decorationColor: Colors.red,
)
: textStyle;

// Always use TextSpan - never WidgetSpan to avoid transaction errors
final textSpan = TextSpan(text: word, style: spanStyle);
final span = textSpanDecoratorForAttribute != null
? textSpanDecoratorForAttribute!(
context,
widget.node,
offset + innerIndex,
textInsert,
textSpan,
widget.textSpanDecorator?.call(textSpan) ?? textSpan,
)
: textSpan;

textSpans.add(span);
} else {
// non-word token (spaces, punctuation)
final textSpan = TextSpan(text: token, style: textStyle);
textSpans.add(textSpan);
}
innerIndex += token.length;
}
offset += textInsert.length;
}
return TextSpan(
children: textSpans,
);
}

final Map<String, bool> _misspelledCache = {};

void _checkWord(String word) {
// Pass the original word (with original casing) to the spell checker
// so it can properly check for capital letters, camelCase, etc.
// Store result in cache using lowercase key for lookup
final lc = word.toLowerCase();

SpellChecker.instance.contains(word).then((exists) {
final miss = !exists;
// avoid unnecessary setState
if (_misspelledCache[lc] != miss) {
_misspelledCache[lc] = miss;
if (mounted) setState(() {});
}
}).catchError((err) {
// treat as known on error
_misspelledCache[lc] = false;
});
Comment thread
PraveenDevamane marked this conversation as resolved.
}

TextSelection? textSelectionFromEditorSelection(Selection? selection) {
if (selection == null) {
return null;
Expand Down
Loading
Loading