From cc996e990c2034f2ced138479b35d5c44ae81544 Mon Sep 17 00:00:00 2001 From: Justus <37046316+Justus-M@users.noreply.github.com> Date: Sat, 5 Aug 2023 16:09:47 +0200 Subject: [PATCH 1/6] make it testable with text field key --- lib/src/code_field/code_field.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/code_field/code_field.dart b/lib/src/code_field/code_field.dart index 6d151cd..2e6273b 100644 --- a/lib/src/code_field/code_field.dart +++ b/lib/src/code_field/code_field.dart @@ -57,7 +57,7 @@ class CodeField extends StatefulWidget { /// {@macro flutter.widgets.textField.selectionControls} final TextSelectionControls? selectionControls; - + final Key? fieldKey; final Color? background; final EdgeInsets padding; final Decoration? decoration; @@ -70,7 +70,7 @@ class CodeField extends StatefulWidget { final TextStyle? hintStyle; const CodeField({ - Key? key, + this.fieldKey, required this.controller, this.minLines, this.maxLines, @@ -97,7 +97,7 @@ class CodeField extends StatefulWidget { this.selectionControls, this.hintText, this.hintStyle, - }) : super(key: key); + }); @override State createState() => _CodeFieldState(); @@ -282,7 +282,7 @@ class _CodeFieldState extends State { ); } - final codeField = TextField( + final codeField = TextField(key: widget.fieldKey, keyboardType: widget.keyboardType, smartQuotesType: widget.smartQuotesType, focusNode: _focusNode, From 534b3f4ec813da749017e3ead25a2a073cbee21a Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 18 Sep 2025 14:13:01 +0200 Subject: [PATCH 2/6] Fix formatting in code_field.dart - Fix indentation for smartDashesType comment - Fix TextField constructor formatting --- lib/src/code_field/code_field.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/code_field/code_field.dart b/lib/src/code_field/code_field.dart index 303c941..aca4ebe 100644 --- a/lib/src/code_field/code_field.dart +++ b/lib/src/code_field/code_field.dart @@ -17,7 +17,7 @@ class CodeField extends StatefulWidget { /// {@macro flutter.widgets.textField.smartQuotesType} final SmartQuotesType? smartQuotesType; -/// {@macro flutter.widgets.textField.smartDashesType} + /// {@macro flutter.widgets.textField.smartDashesType} final SmartDashesType? smartDashesType; /// {@macro flutter.widgets.textField.keyboardType} @@ -324,7 +324,8 @@ class _CodeFieldState extends State { ); } - final codeField = TextField(key: widget.fieldKey, + final codeField = TextField( + key: widget.fieldKey, keyboardType: widget.keyboardType, smartQuotesType: widget.smartQuotesType, smartDashesType: widget.smartDashesType, From a9199f2a39412c2a2760034dc1a277e0b9f9ed9f Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 18 Sep 2025 14:31:15 +0200 Subject: [PATCH 3/6] Fix weird compilation errors --- example/lib/main.dart | 1 - lib/src/code_field/code_controller.dart | 13 ++++--------- lib/src/code_field/code_field.dart | 2 +- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 2c137c9..916c102 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -99,7 +99,6 @@ class _HomePageState extends State { TextButton.icon( style: TextButton.styleFrom( padding: EdgeInsets.symmetric(horizontal: 8.0), - primary: Colors.white, ), icon: Icon(FontAwesomeIcons.github), onPressed: () => diff --git a/lib/src/code_field/code_controller.dart b/lib/src/code_field/code_controller.dart index b749ee5..16aedd3 100644 --- a/lib/src/code_field/code_controller.dart +++ b/lib/src/code_field/code_controller.dart @@ -86,7 +86,8 @@ class CodeController extends TextEditingController { patternList.addAll(patternMap!.keys.map((e) => '($e)')); _styleList.addAll(patternMap!.values); } - _styleRegExp = RegExp(patternList.join('|'), multiLine: true, unicode: true); + _styleRegExp = + RegExp(patternList.join('|'), multiLine: true, unicode: true); } /// Sets a specific cursor position in the text @@ -305,24 +306,18 @@ class CodeController extends TextEditingController { } CodeController copyWith({ - Mode? _language, - CodeAutoComplete? autoComplete, + Mode? language, Map? patternMap, Map? stringMap, EditorParams? params, List? modifiers, - String? _languageId, - RegExp? _styleRegExp, }) { return CodeController( - _language: _language ?? this._language, - autoComplete: autoComplete ?? this.autoComplete, + language: language ?? this.language, patternMap: patternMap ?? this.patternMap, stringMap: stringMap ?? this.stringMap, params: params ?? this.params, modifiers: modifiers ?? this.modifiers, - _languageId: _languageId ?? this._languageId, - _styleRegExp: _styleRegExp ?? this._styleRegExp, ); } } diff --git a/lib/src/code_field/code_field.dart b/lib/src/code_field/code_field.dart index aca4ebe..fcc90a6 100644 --- a/lib/src/code_field/code_field.dart +++ b/lib/src/code_field/code_field.dart @@ -84,8 +84,8 @@ class CodeField extends StatefulWidget { final CodeAutoComplete? autoComplete; const CodeField({ - this.fieldKey, required this.controller, + this.fieldKey, this.minLines, this.maxLines, this.expands = false, From 8c4aab2d0be6b140df391eaf5a488dd8b1eb6e95 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 18 Sep 2025 15:20:04 +0200 Subject: [PATCH 4/6] fix(code-field): align line numbers with wrapped text lines When wrap=true, long lines create multiple visual rows but line numbers only showed logical line count. Now TextPainter measures each line's visual height and inserts blank placeholders to keep line numbers synchronized with wrapped content. - Calculate visual line count using TextPainter.layout() - Insert empty line number placeholders for wrapped visual lines - Recalculate on width changes to maintain alignment - Handle non-numeric placeholders in LineNumberController --- lib/src/code_field/code_field.dart | 57 ++++++++++++++++++- .../line_numbers/line_number_controller.dart | 19 ++++--- 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/lib/src/code_field/code_field.dart b/lib/src/code_field/code_field.dart index fcc90a6..71ae1ab 100644 --- a/lib/src/code_field/code_field.dart +++ b/lib/src/code_field/code_field.dart @@ -132,6 +132,10 @@ class _CodeFieldState extends State { FocusNode? _focusNode; String? lines; String longestLine = ''; + // Track the available width of the code column in order to compute + // visual line wrapping for line-numbers. + double _codeColumnWidth = 0; + var _currentTextStyle = const TextStyle(); @override void initState() { @@ -186,8 +190,34 @@ class _CodeFieldState extends State { final str = widget.controller.text.split('\n'); final buf = []; - for (var k = 0; k < str.length; k++) { - buf.add((k + 1).toString()); + if (widget.wrap && _codeColumnWidth > 0) { + // When wrapping is enabled we need to account for visual lines that + // are created by the soft wrap. We measure each logical line using a + // TextPainter and add blank placeholders so the scrolling offset of + // the linked controllers stays in sync. + for (var k = 0; k < str.length; k++) { + final logicalLine = str[k]; + + // Compute how many visual lines this logical line occupies. + final tp = TextPainter( + text: TextSpan(text: logicalLine, style: _currentTextStyle), + textDirection: TextDirection.ltr, + )..layout(maxWidth: _codeColumnWidth); + final visualLines = tp.computeLineMetrics().length; + + // First visual line gets the real line number. + buf.add((k + 1).toString()); + + // Remaining visual lines get an empty placeholder so that the + // total number of lines matches the code field. + for (int v = 1; v < visualLines; v++) { + buf.add(''); + } + } + } else { + for (var k = 0; k < str.length; k++) { + buf.add((k + 1).toString()); + } } _numberController?.text = buf.join('\n'); @@ -370,7 +400,28 @@ class _CodeFieldState extends State { textSelectionTheme: widget.textSelectionTheme, ), child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { + builder: (context, constraints) { + // Save the textStyle and available width so _onTextChanged can + // accurately compute the amount of visual lines when wrapping. + _currentTextStyle = textStyle; + // Subtract left padding that will be applied inside the + // SingleChildScrollView when horizontal scrolling is disabled. + var availableWidth = constraints.maxWidth; + if (widget.lineNumbers) { + availableWidth -= widget.lineNumberStyle.width; + } + + if ((availableWidth - _codeColumnWidth).abs() > 0.5) { + _codeColumnWidth = max(0, availableWidth); + // Recalculate the line numbers because wrapping may have + // changed due to a width change. + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _onTextChanged(); + } + }); + } + // Control horizontal scrolling return widget.wrap ? codeField diff --git a/lib/src/line_numbers/line_number_controller.dart b/lib/src/line_numbers/line_number_controller.dart index e5ce6bb..8ac2c30 100644 --- a/lib/src/line_numbers/line_number_controller.dart +++ b/lib/src/line_numbers/line_number_controller.dart @@ -18,14 +18,19 @@ class LineNumberController extends TextEditingController { for (int k = 0; k < list.length; k++) { final el = list[k]; - final number = int.parse(el); - var textSpan = TextSpan(text: el, style: style); - - if (lineNumberBuilder != null) { - textSpan = lineNumberBuilder!(number, style); + // Blank lines are placeholders inserted to align with wrapped + // visual lines. They should render as empty text spans to + // preserve vertical spacing. + if (el.trim().isEmpty) { + children.add(TextSpan(text: '', style: style)); + } else { + final number = int.tryParse(el) ?? 0; + var textSpan = TextSpan(text: el, style: style); + if (lineNumberBuilder != null && number != 0) { + textSpan = lineNumberBuilder!(number, style); + } + children.add(textSpan); } - - children.add(textSpan); if (k < list.length - 1) { children.add(const TextSpan(text: '\n')); } From 2450e927876e59780c8f3911e86a97d4fe4de5cb Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Wed, 24 Sep 2025 16:35:22 +0200 Subject: [PATCH 5/6] Calculate the available width from a closer LayoutBuilder Now the line wrapping and line numbers should be synced --- lib/src/code_field/code_field.dart | 50 +++++++++++++++++------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/lib/src/code_field/code_field.dart b/lib/src/code_field/code_field.dart index 71ae1ab..a1a1f2d 100644 --- a/lib/src/code_field/code_field.dart +++ b/lib/src/code_field/code_field.dart @@ -401,27 +401,6 @@ class _CodeFieldState extends State { ), child: LayoutBuilder( builder: (context, constraints) { - // Save the textStyle and available width so _onTextChanged can - // accurately compute the amount of visual lines when wrapping. - _currentTextStyle = textStyle; - // Subtract left padding that will be applied inside the - // SingleChildScrollView when horizontal scrolling is disabled. - var availableWidth = constraints.maxWidth; - if (widget.lineNumbers) { - availableWidth -= widget.lineNumberStyle.width; - } - - if ((availableWidth - _codeColumnWidth).abs() > 0.5) { - _codeColumnWidth = max(0, availableWidth); - // Recalculate the line numbers because wrapping may have - // changed due to a width change. - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - _onTextChanged(); - } - }); - } - // Control horizontal scrolling return widget.wrap ? codeField @@ -438,7 +417,34 @@ class _CodeFieldState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (widget.lineNumbers && numberCol != null) numberCol, - Expanded(child: codeCol), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + // Save the textStyle and available width so _onTextChanged can + // accurately compute the amount of visual lines when wrapping. + _currentTextStyle = textStyle; + // Subtract left padding that will be applied inside the + // SingleChildScrollView when horizontal scrolling is disabled. + var availableWidth = constraints.maxWidth; + + const caretMargin = 3.0; // 1 gap + 2 cursor + availableWidth -= caretMargin; + + if ((availableWidth - _codeColumnWidth).abs() > 0.5) { + _codeColumnWidth = max(0, availableWidth); + print('availableWidth: $availableWidth'); + // Recalculate the line numbers because wrapping may have + // changed due to a width change. + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _onTextChanged(); + } + }); + } + return codeCol; + }, + ), + ), ], ), ); From 6c32445971a5d5779eb372db4b8fdadffcfa4323 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 25 Sep 2025 14:00:37 +0200 Subject: [PATCH 6/6] Readd key parameter --- lib/src/code_field/code_field.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/code_field/code_field.dart b/lib/src/code_field/code_field.dart index a1a1f2d..21e9b6a 100644 --- a/lib/src/code_field/code_field.dart +++ b/lib/src/code_field/code_field.dart @@ -84,6 +84,7 @@ class CodeField extends StatefulWidget { final CodeAutoComplete? autoComplete; const CodeField({ + Key? key, required this.controller, this.fieldKey, this.minLines, @@ -115,7 +116,7 @@ class CodeField extends StatefulWidget { this.autoComplete, this.textInputAction, this.enableSuggestions = false, - }); + }) : super(key: key); @override State createState() => _CodeFieldState();