Skip to content

Commit 0316799

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 827a1fa commit 0316799

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';
@@ -69,7 +69,7 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
6969
final InteractiveDrawerController _drawerController = InteractiveDrawerController();
7070
final ValueNotifier<int> _assistantPickerCloseTick = ValueNotifier<int>(0);
7171
final FocusNode _inputFocus = FocusNode();
72-
final TextEditingController _inputController = TextEditingController();
72+
final CodeLineEditingController _inputController = CodeLineEditingController();
7373
final ChatInputBarController _mediaController = ChatInputBarController();
7474
final ScrollController _scrollController = ScrollController();
7575
final GlobalKey _inputBarKey = GlobalKey();
@@ -184,20 +184,13 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
184184
if (!mounted) return;
185185
final trimmed = text.trim();
186186
if (trimmed.isEmpty) return;
187-
final current = _inputController.text;
188-
final selection = _inputController.selection;
189-
final start = (selection.start >= 0 && selection.start <= current.length)
190-
? selection.start
191-
: current.length;
192-
final end = (selection.end >= 0 && selection.end <= current.length && selection.end >= start)
193-
? selection.end
194-
: start;
195-
final next = current.replaceRange(start, end, trimmed);
196-
_inputController.value = _inputController.value.copyWith(
197-
text: next,
198-
selection: TextSelection.collapsed(offset: start + trimmed.length),
199-
composing: TextRange.empty,
200-
);
187+
// Use CodeLineEditingController's replaceSelection to insert text at cursor
188+
try {
189+
_inputController.replaceSelection(trimmed);
190+
} catch (_) {
191+
// TODO: Add diagnostics (and/or a graceful fallback insert) when replaceSelection fails to avoid silent drops.
192+
return;
193+
}
201194
WidgetsBinding.instance.addPostFrameCallback((_) {
202195
if (!mounted) return;
203196
_controller.forceScrollToBottomSoon(animate: false);
@@ -329,7 +322,7 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
329322
Widget w = content;
330323
if (!isAndroid) {
331324
w = w
332-
.animate(key: ValueKey('mob_body_'+(_controller.currentConversation?.id ?? 'none')))
325+
.animate(key: ValueKey('mob_body_${_controller.currentConversation?.id ?? 'none'}'))
333326
.fadeIn(duration: 200.ms, curve: Curves.easeOutCubic);
334327
w = FadeTransition(opacity: _controller.convoFade, child: w);
335328
}
@@ -475,7 +468,7 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
475468
context,
476469
dividerPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
477470
),
478-
).animate(key: ValueKey('tab_body_'+(_controller.currentConversation?.id ?? 'none')))
471+
).animate(key: ValueKey('tab_body_${_controller.currentConversation?.id ?? 'none'}'))
479472
.fadeIn(duration: 200.ms, curve: Curves.easeOutCubic),
480473
),
481474
),
@@ -554,7 +547,7 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
554547
image: DecorationImage(
555548
image: provider,
556549
fit: BoxFit.cover,
557-
colorFilter: ColorFilter.mode(Colors.black.withOpacity(0.04), BlendMode.srcATop),
550+
colorFilter: ColorFilter.mode(Colors.black.withValues(alpha: 0.04), BlendMode.srcATop),
558551
),
559552
),
560553
),
@@ -570,8 +563,8 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
570563
final top = (0.20 * maskStrength).clamp(0.0, 1.0);
571564
final bottom = (0.50 * maskStrength).clamp(0.0, 1.0);
572565
return [
573-
cs.background.withOpacity(top),
574-
cs.background.withOpacity(bottom),
566+
cs.surface.withValues(alpha: top),
567+
cs.surface.withValues(alpha: bottom),
575568
];
576569
}(),
577570
),
@@ -608,16 +601,16 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
608601
child: Stack(
609602
fit: StackFit.expand,
610603
children: [
611-
ColoredBox(color: cs.background),
604+
ColoredBox(color: cs.surface),
612605
if (bg != null) Opacity(opacity: 0.9, child: bg),
613606
DecoratedBox(
614607
decoration: BoxDecoration(
615608
gradient: LinearGradient(
616609
begin: Alignment.topCenter,
617610
end: Alignment.bottomCenter,
618611
colors: [
619-
cs.background.withOpacity(0.08),
620-
cs.background.withOpacity(0.36),
612+
cs.surface.withValues(alpha: 0.08),
613+
cs.surface.withValues(alpha: 0.36),
621614
],
622615
),
623616
),
@@ -713,15 +706,20 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
713706
context.read<SettingsProvider>().setThinkingBudget(assistant.thinkingBudget);
714707
}
715708
await _openReasoningSettings();
709+
if (!context.mounted) return;
716710
final chosen = context.read<SettingsProvider>().thinkingBudget;
717711
await context.read<AssistantProvider>().updateAssistant(
718712
assistant.copyWith(thinkingBudget: chosen),
719713
);
720714
}
721715
},
722716
onSend: (text) {
717+
final trimmed = text.text.trim();
718+
if (trimmed.isEmpty && text.imagePaths.isEmpty && text.documents.isEmpty) {
719+
return;
720+
}
723721
_controller.sendMessage(text);
724-
_inputController.clear();
722+
_inputController.value = const CodeLineEditingValue.empty(); // Clear + reset selection/composing
725723
if (PlatformUtils.isMobile) {
726724
_controller.dismissKeyboard();
727725
} else {
@@ -800,14 +798,14 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
800798
if (_controller.isDragHovering)
801799
IgnorePointer(
802800
child: Container(
803-
color: Colors.black.withOpacity(0.12),
801+
color: Colors.black.withValues(alpha: 0.12),
804802
child: Center(
805803
child: Container(
806804
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
807805
decoration: BoxDecoration(
808-
color: Theme.of(context).colorScheme.surface.withOpacity(0.95),
806+
color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.95),
809807
borderRadius: BorderRadius.circular(12),
810-
border: Border.all(color: Theme.of(context).colorScheme.primary.withOpacity(0.4), width: 2),
808+
border: Border.all(color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.4), width: 2),
811809
),
812810
child: Text(
813811
AppLocalizations.of(context)!.homePageDropToUpload,
@@ -847,6 +845,7 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
847845
final assistantId = context.read<AssistantProvider>().currentAssistantId;
848846
final provider = context.read<InstructionInjectionProvider>();
849847
await provider.initialize();
848+
if (!mounted) return;
850849
final items = provider.items;
851850
if (items.isEmpty) return;
852851

0 commit comments

Comments
 (0)