Skip to content
1 change: 0 additions & 1 deletion example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ class _HomePageState extends State<HomePage> {
TextButton.icon(
style: TextButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 8.0),
primary: Colors.white,
),
icon: Icon(FontAwesomeIcons.github),
onPressed: () =>
Expand Down
13 changes: 4 additions & 9 deletions lib/src/code_field/code_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -305,24 +306,18 @@ class CodeController extends TextEditingController {
}

CodeController copyWith({
Mode? _language,
CodeAutoComplete? autoComplete,
Mode? language,
Map<String, TextStyle>? patternMap,
Map<String, TextStyle>? stringMap,
EditorParams? params,
List<CodeModifier>? 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,
);
}
}
71 changes: 65 additions & 6 deletions lib/src/code_field/code_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -64,13 +64,13 @@ class CodeField extends StatefulWidget {

/// {@macro flutter.widgets.textField.selectionControls}
final TextSelectionControls? selectionControls;
final Key? fieldKey;

/// {@macro flutter.widgets.textField.textInputAction}
final TextInputAction? textInputAction;

/// {@macro flutter.services.TextInputConfiguration.enableSuggestions}
final bool enableSuggestions;

final Color? background;
final EdgeInsets padding;
final Decoration? decoration;
Expand All @@ -86,6 +86,7 @@ class CodeField extends StatefulWidget {
const CodeField({
Key? key,
required this.controller,
this.fieldKey,
this.minLines,
this.maxLines,
this.expands = false,
Expand Down Expand Up @@ -132,6 +133,10 @@ class _CodeFieldState extends State<CodeField> {
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() {
Expand Down Expand Up @@ -186,8 +191,34 @@ class _CodeFieldState extends State<CodeField> {
final str = widget.controller.text.split('\n');
final buf = <String>[];

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');
Expand Down Expand Up @@ -325,6 +356,7 @@ class _CodeFieldState extends State<CodeField> {
}

final codeField = TextField(
key: widget.fieldKey,
keyboardType: widget.keyboardType,
smartQuotesType: widget.smartQuotesType,
smartDashesType: widget.smartDashesType,
Expand Down Expand Up @@ -369,7 +401,7 @@ class _CodeFieldState extends State<CodeField> {
textSelectionTheme: widget.textSelectionTheme,
),
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
builder: (context, constraints) {
// Control horizontal scrolling
return widget.wrap
? codeField
Expand All @@ -386,7 +418,34 @@ class _CodeFieldState extends State<CodeField> {
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;
},
),
),
],
),
);
Expand Down
19 changes: 12 additions & 7 deletions lib/src/line_numbers/line_number_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
}
Expand Down