Skip to content

Commit 28c0766

Browse files
committed
refactor(chat-input)!: switch to CodeLineEditingController
Migrate the chat input stack from TextField/TextEditingController to re_editor (CodeLineEditingController + PlainTextCodeEditor) and unify shortcut handling. BREAKING CHANGE: Chat input controllers are now CodeLineEditingController. External code must migrate: - ChatInputBar.controller (TextEditingController? -> CodeLineEditingController?) - ChatInputSection.inputController (TextEditingController -> CodeLineEditingController) - HomePageController.inputController (TextEditingController -> CodeLineEditingController)
1 parent 353c41e commit 28c0766

File tree

4 files changed

+387
-511
lines changed

4 files changed

+387
-511
lines changed

lib/features/home/controllers/home_page_controller.dart

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
33
import 'package:flutter/services.dart';
44
import 'package:provider/provider.dart';
55
import 'package:image_picker/image_picker.dart';
6+
import 'package:re_editor/re_editor.dart';
67
import '../../../core/models/chat_input_data.dart';
78
import '../../../core/models/chat_message.dart';
89
import '../../../core/models/conversation.dart';
@@ -64,7 +65,7 @@ class HomePageController extends ChangeNotifier {
6465
required GlobalKey<ScaffoldState> scaffoldKey,
6566
required GlobalKey inputBarKey,
6667
required FocusNode inputFocus,
67-
required TextEditingController inputController,
68+
required CodeLineEditingController inputController,
6869
required ChatInputBarController mediaController,
6970
required ScrollController scrollController,
7071
}) : _context = context,
@@ -87,7 +88,7 @@ class HomePageController extends ChangeNotifier {
8788
final GlobalKey<ScaffoldState> _scaffoldKey;
8889
final GlobalKey _inputBarKey;
8990
final FocusNode _inputFocus;
90-
final TextEditingController _inputController;
91+
final CodeLineEditingController _inputController;
9192
final ChatInputBarController _mediaController;
9293
final ScrollController _scrollController;
9394

@@ -165,7 +166,7 @@ class HomePageController extends ChangeNotifier {
165166
GlobalKey<ScaffoldState> get scaffoldKey => _scaffoldKey;
166167
GlobalKey get inputBarKey => _inputBarKey;
167168
FocusNode get inputFocus => _inputFocus;
168-
TextEditingController get inputController => _inputController;
169+
CodeLineEditingController get inputController => _inputController;
169170
ChatInputBarController get mediaController => _mediaController;
170171
ScrollController get scrollController => _scrollController;
171172
Animation<double> get convoFade => _convoFade;
@@ -1068,22 +1069,14 @@ class HomePageController extends ChangeNotifier {
10681069

10691070
Future<void> handleQuickPhraseSelection(QuickPhrase? selected) async {
10701071
if (selected == null) return;
1071-
final text = _inputController.text;
1072-
final selection = _inputController.selection;
1073-
final start = (selection.start >= 0 && selection.start <= text.length)
1074-
? selection.start
1075-
: text.length;
1076-
final end = (selection.end >= 0 && selection.end <= text.length && selection.end >= start)
1077-
? selection.end
1078-
: start;
1079-
1080-
final newText = text.replaceRange(start, end, selected.content);
1081-
_inputController.value = _inputController.value.copyWith(
1082-
text: newText,
1083-
selection: TextSelection.collapsed(offset: start + selected.content.length),
1084-
composing: TextRange.empty,
1085-
);
1086-
notifyListeners();
1072+
// Use CodeLineEditingController's replaceSelection to insert quick phrase
1073+
try {
1074+
_inputController.replaceSelection(selected.content);
1075+
notifyListeners();
1076+
} catch (_) {
1077+
// TODO: Add diagnostics (e.g., debug log + stack trace) when replaceSelection fails to avoid silent drops.
1078+
return;
1079+
}
10871080
}
10881081

10891082
// ============================================================================

lib/features/home/pages/home_page.dart

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import 'dart:async';
22
import 'dart:io' show File;
33
import 'package:flutter/material.dart';
4-
import 'package:flutter/foundation.dart' show TargetPlatform;
54
import 'package:flutter_animate/flutter_animate.dart';
65
import 'package:desktop_drop/desktop_drop.dart';
76
import 'package:provider/provider.dart';
7+
import 'package:re_editor/re_editor.dart';
88
import '../../../l10n/app_localizations.dart';
99
import '../../../main.dart';
1010
import '../../../shared/widgets/interactive_drawer.dart';
@@ -66,7 +66,7 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
6666
final InteractiveDrawerController _drawerController = InteractiveDrawerController();
6767
final ValueNotifier<int> _assistantPickerCloseTick = ValueNotifier<int>(0);
6868
final FocusNode _inputFocus = FocusNode();
69-
final TextEditingController _inputController = TextEditingController();
69+
final CodeLineEditingController _inputController = CodeLineEditingController();
7070
final ChatInputBarController _mediaController = ChatInputBarController();
7171
final ScrollController _scrollController = ScrollController();
7272
final GlobalKey _inputBarKey = GlobalKey();
@@ -177,20 +177,13 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
177177
if (!mounted) return;
178178
final trimmed = text.trim();
179179
if (trimmed.isEmpty) return;
180-
final current = _inputController.text;
181-
final selection = _inputController.selection;
182-
final start = (selection.start >= 0 && selection.start <= current.length)
183-
? selection.start
184-
: current.length;
185-
final end = (selection.end >= 0 && selection.end <= current.length && selection.end >= start)
186-
? selection.end
187-
: start;
188-
final next = current.replaceRange(start, end, trimmed);
189-
_inputController.value = _inputController.value.copyWith(
190-
text: next,
191-
selection: TextSelection.collapsed(offset: start + trimmed.length),
192-
composing: TextRange.empty,
193-
);
180+
// Use CodeLineEditingController's replaceSelection to insert text at cursor
181+
try {
182+
_inputController.replaceSelection(trimmed);
183+
} catch (_) {
184+
// TODO: Add diagnostics (and/or a graceful fallback insert) when replaceSelection fails to avoid silent drops.
185+
return;
186+
}
194187
WidgetsBinding.instance.addPostFrameCallback((_) {
195188
if (!mounted) return;
196189
_controller.forceScrollToBottomSoon(animate: false);
@@ -322,7 +315,7 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
322315
Widget w = content;
323316
if (!isAndroid) {
324317
w = w
325-
.animate(key: ValueKey('mob_body_'+(_controller.currentConversation?.id ?? 'none')))
318+
.animate(key: ValueKey('mob_body_${_controller.currentConversation?.id ?? 'none'}'))
326319
.fadeIn(duration: 200.ms, curve: Curves.easeOutCubic);
327320
w = FadeTransition(opacity: _controller.convoFade, child: w);
328321
}
@@ -468,7 +461,7 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
468461
context,
469462
dividerPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
470463
),
471-
).animate(key: ValueKey('tab_body_'+(_controller.currentConversation?.id ?? 'none')))
464+
).animate(key: ValueKey('tab_body_${_controller.currentConversation?.id ?? 'none'}'))
472465
.fadeIn(duration: 200.ms, curve: Curves.easeOutCubic),
473466
),
474467
),
@@ -547,7 +540,7 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
547540
image: DecorationImage(
548541
image: provider,
549542
fit: BoxFit.cover,
550-
colorFilter: ColorFilter.mode(Colors.black.withOpacity(0.04), BlendMode.srcATop),
543+
colorFilter: ColorFilter.mode(Colors.black.withValues(alpha: 0.04), BlendMode.srcATop),
551544
),
552545
),
553546
),
@@ -563,8 +556,8 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
563556
final top = (0.20 * maskStrength).clamp(0.0, 1.0);
564557
final bottom = (0.50 * maskStrength).clamp(0.0, 1.0);
565558
return [
566-
cs.background.withOpacity(top),
567-
cs.background.withOpacity(bottom),
559+
cs.surface.withValues(alpha: top),
560+
cs.surface.withValues(alpha: bottom),
568561
];
569562
}(),
570563
),
@@ -601,16 +594,16 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
601594
child: Stack(
602595
fit: StackFit.expand,
603596
children: [
604-
ColoredBox(color: cs.background),
597+
ColoredBox(color: cs.surface),
605598
if (bg != null) Opacity(opacity: 0.9, child: bg),
606599
DecoratedBox(
607600
decoration: BoxDecoration(
608601
gradient: LinearGradient(
609602
begin: Alignment.topCenter,
610603
end: Alignment.bottomCenter,
611604
colors: [
612-
cs.background.withOpacity(0.08),
613-
cs.background.withOpacity(0.36),
605+
cs.surface.withValues(alpha: 0.08),
606+
cs.surface.withValues(alpha: 0.36),
614607
],
615608
),
616609
),
@@ -706,15 +699,20 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
706699
context.read<SettingsProvider>().setThinkingBudget(assistant.thinkingBudget);
707700
}
708701
await _openReasoningSettings();
702+
if (!context.mounted) return;
709703
final chosen = context.read<SettingsProvider>().thinkingBudget;
710704
await context.read<AssistantProvider>().updateAssistant(
711705
assistant.copyWith(thinkingBudget: chosen),
712706
);
713707
}
714708
},
715709
onSend: (text) {
710+
final trimmed = text.text.trim();
711+
if (trimmed.isEmpty && text.imagePaths.isEmpty && text.documents.isEmpty) {
712+
return;
713+
}
716714
_controller.sendMessage(text);
717-
_inputController.clear();
715+
_inputController.value = const CodeLineEditingValue.empty(); // Clear + reset selection/composing
718716
if (PlatformUtils.isMobile) {
719717
_controller.dismissKeyboard();
720718
} else {
@@ -792,14 +790,14 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
792790
if (_controller.isDragHovering)
793791
IgnorePointer(
794792
child: Container(
795-
color: Colors.black.withOpacity(0.12),
793+
color: Colors.black.withValues(alpha: 0.12),
796794
child: Center(
797795
child: Container(
798796
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
799797
decoration: BoxDecoration(
800-
color: Theme.of(context).colorScheme.surface.withOpacity(0.95),
798+
color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.95),
801799
borderRadius: BorderRadius.circular(12),
802-
border: Border.all(color: Theme.of(context).colorScheme.primary.withOpacity(0.4), width: 2),
800+
border: Border.all(color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.4), width: 2),
803801
),
804802
child: Text(
805803
AppLocalizations.of(context)!.homePageDropToUpload,
@@ -839,6 +837,7 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
839837
final assistantId = context.read<AssistantProvider>().currentAssistantId;
840838
final provider = context.read<InstructionInjectionProvider>();
841839
await provider.initialize();
840+
if (!mounted) return;
842841
final items = provider.items;
843842
if (items.isEmpty) return;
844843

0 commit comments

Comments
 (0)