diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies index ec0b402..3a2dc0d 100644 --- a/.flutter-plugins-dependencies +++ b/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"highlights_plugin","path":"/Users/tkadziolka/.pub-cache/hosted/pub.dev/highlights_plugin-0.2.0/","native_build":true,"dependencies":[]}],"android":[{"name":"highlights_plugin","path":"/Users/tkadziolka/.pub-cache/hosted/pub.dev/highlights_plugin-0.2.0/","native_build":true,"dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"highlights_plugin","dependencies":[]}],"date_created":"2024-11-18 21:40:21.617016","version":"3.24.0","swift_package_manager_enabled":false} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"highlights_plugin","path":"/Users/tkadziolka/.pub-cache/hosted/pub.dev/highlights_plugin-0.2.0/","native_build":true,"dependencies":[]}],"android":[{"name":"highlights_plugin","path":"/Users/tkadziolka/.pub-cache/hosted/pub.dev/highlights_plugin-0.2.0/","native_build":true,"dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"highlights_plugin","dependencies":[]}],"date_created":"2024-11-22 20:35:46.447909","version":"3.24.0","swift_package_manager_enabled":false} \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index fb51380..7a2c216 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -45,6 +45,7 @@ class _MyAppState extends State { selectAll: true, share: true, ), + enableLineNumbers: true, ), ), const SizedBox(height: 24), @@ -56,6 +57,14 @@ class _MyAppState extends State { code: codeSnippet, controller: controller, showCursor: true, + showLineNumbers: true, + maxLines: 5, + decoration: const InputDecoration( + contentPadding: EdgeInsets.zero, + border: OutlineInputBorder( + gapPadding: 0, + ), + ), ), ), ], diff --git a/example/pubspec.lock b/example/pubspec.lock index fb15d61..e168564 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -110,7 +110,7 @@ packages: path: ".." relative: true source: path - version: "0.1.0" + version: "0.2.0" leak_tracker: dependency: transitive description: diff --git a/lib/src/presentation/code_edit_text.dart b/lib/src/presentation/code_edit_text.dart index 2374db7..d63e3e6 100644 --- a/lib/src/presentation/code_edit_text.dart +++ b/lib/src/presentation/code_edit_text.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:kode_view/src/presentation/line_numbers_wrapper.dart'; +import 'package:kode_view/src/presentation/styles/text_styles.dart'; import 'package:kode_view/src/presentation/syntax_highlighting_controller.dart'; import 'package:kode_view/src/presentation/text_selection_options.dart'; @@ -6,30 +8,39 @@ class CodeEditText extends StatefulWidget { const CodeEditText({ required this.code, this.controller, - this.maxLines, - this.options, - this.showCursor, - this.onTap, this.language, this.theme, - this.textStyle, + this.textStyle = const TextStyles.code(), + this.maxLines, this.decoration, + this.showLineNumbers = false, + this.lineNumberColor, + this.lineNumberBackgroundColor, + this.showCursor, + this.options, + this.onTap, this.debug = false, super.key, }); final String code; + + final SyntaxHighlightingController? controller; + final String? language; + final String? theme; + final TextStyle textStyle; final int? maxLines; + + final InputDecoration? decoration; + final bool showLineNumbers; + final Color? lineNumberColor; + final Color? lineNumberBackgroundColor; + final bool? showCursor; final TextSelectionOptions? options; - final InputDecoration? decoration; final GestureTapCallback? onTap; - final SyntaxHighlightingController? controller; - final bool debug; - final String? language; - final String? theme; - final TextStyle? textStyle; + final bool debug; @override State createState() => _CodeEditTextState(); @@ -37,18 +48,28 @@ class CodeEditText extends StatefulWidget { class _CodeEditTextState extends State { late SyntaxHighlightingController _controller; + final ScrollController _lineNumbersScrollController = ScrollController(); + final ScrollController _textFieldScrollController = ScrollController(); + final GlobalKey _textFieldKey = GlobalKey(); + double _textFieldHeight = 0; @override void initState() { super.initState(); _controller = widget.controller ?? - SyntaxHighlightingController(text: widget.code, debug: widget.debug, - )..addListener(() { + SyntaxHighlightingController( + text: widget.code, + debug: widget.debug, + ) + ..addListener(() { _controller.updateSyntaxHighlighting( code: _controller.text, language: widget.language, theme: widget.theme, - textStyle: widget.textStyle, + textStyle: widget.textStyle.copyWith( + height: 1.5, + fontSize: 10.0, + ), ); }); @@ -56,8 +77,26 @@ class _CodeEditTextState extends State { code: _controller.text, language: widget.language, theme: widget.theme, - textStyle: widget.textStyle, + textStyle: widget.textStyle.copyWith( + height: 1.5, + fontSize: 10.0, + ), ); + + _textFieldScrollController.addListener(() { + if (_textFieldScrollController.offset != + _lineNumbersScrollController.offset) { + _lineNumbersScrollController.jumpTo(_textFieldScrollController.offset); + } + }); + } + + void _getTextFieldHeight() { + final RenderBox renderBox = + _textFieldKey.currentContext?.findRenderObject() as RenderBox; + setState(() { + _textFieldHeight = renderBox.size.height; + }); } @override @@ -65,20 +104,42 @@ class _CodeEditTextState extends State { return ValueListenableBuilder( valueListenable: _controller.textSpansNotifier, builder: (context, __, _) { - return TextField( - controller: _controller, - style: widget.textStyle, - onTap: widget.onTap, - minLines: 1, - maxLines: widget.maxLines, - contextMenuBuilder: widget.options != null - ? (context, editableTextState) => - widget.options!.toolbarOptions(context, editableTextState) - : null, - enableInteractiveSelection: widget.options != null, - showCursor: widget.showCursor ?? true, - scrollPhysics: const ClampingScrollPhysics(), - decoration: widget.decoration, + WidgetsBinding.instance.addPostFrameCallback((_) { + _getTextFieldHeight(); + }); + return LineNumbersWrapper( + height: _textFieldHeight, + showLineNumbers: widget.showLineNumbers, + scrollController: _lineNumbersScrollController, + linesCount: _controller.text.split('\n').length, + fontSize: 10.0, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + physics: const ClampingScrollPhysics(), + child: IntrinsicWidth( + child: TextField( + scrollPadding: EdgeInsets.zero, + key: _textFieldKey, + scrollController: _textFieldScrollController, + controller: _controller, + style: widget.textStyle.copyWith( + height: 1.5, + fontSize: 10.0, + ), + onTap: widget.onTap, + minLines: 1, + maxLines: widget.maxLines, + contextMenuBuilder: widget.options != null + ? (context, editableTextState) => widget.options! + .toolbarOptions(context, editableTextState) + : null, + enableInteractiveSelection: widget.options != null, + showCursor: widget.showCursor ?? true, + scrollPhysics: const ClampingScrollPhysics(), + decoration: widget.decoration, + ), + ), + ), ); }, ); diff --git a/lib/src/presentation/code_text_view.dart b/lib/src/presentation/code_text_view.dart index fad3674..e77c72c 100644 --- a/lib/src/presentation/code_text_view.dart +++ b/lib/src/presentation/code_text_view.dart @@ -2,12 +2,13 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:highlights_plugin/highlights_plugin.dart'; +import 'package:kode_view/src/presentation/line_numbers_wrapper.dart'; import 'package:kode_view/src/presentation/styles/text_styles.dart'; import 'package:kode_view/src/presentation/text_selection_options.dart'; import 'package:kode_view/src/utils/extensions/collection_extensions.dart'; import 'package:kode_view/src/utils/extensions/text_extensions.dart'; -class CodeTextView extends StatelessWidget { +class CodeTextView extends StatefulWidget { const CodeTextView({ required this.code, this.maxLines, @@ -16,20 +17,23 @@ class CodeTextView extends StatelessWidget { this.onTap, this.language, this.theme, - this.textStyle, + this.textStyle = const TextStyles.code(), + this.enableLineNumbers = false, super.key, }); final splitter = const LineSplitter(); + final String code; final int? maxLines; final bool? showCursor; final TextSelectionOptions? options; final GestureTapCallback? onTap; + final bool enableLineNumbers; final String? language; final String? theme; - final TextStyle? textStyle; + final TextStyle textStyle; const CodeTextView.preview({ required this.code, @@ -38,31 +42,62 @@ class CodeTextView extends StatelessWidget { this.showCursor, this.language, this.theme, - this.textStyle, + this.textStyle = const TextStyles.code(), + this.enableLineNumbers = false, super.key, }) : maxLines = 5; + @override + State createState() => _CodeTextViewState(); +} + +class _CodeTextViewState extends State { + final GlobalKey selectableTextkey = GlobalKey(); + double height = 0; + + void _getTextFieldHeight() { + final RenderBox renderBox = + selectableTextkey.currentContext?.findRenderObject() as RenderBox; + setState(() { + height = renderBox.size.height; + }); + } + @override Widget build(BuildContext context) { - final maxLinesOrAll = maxLines ?? splitter.convert(code).length; + final maxLinesOrAll = + widget.maxLines ?? widget.splitter.convert(widget.code).length; + final ScrollController controller = ScrollController(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _getTextFieldHeight(); + }); return FutureBuilder( initialData: const [], future: _highlights(maxLinesOrAll), builder: (_, value) { - return SelectableText.rich( - TextSpan(children: value.requireData), - style: textStyle ?? TextStyles.code(code).style!, - minLines: 1, - maxLines: maxLinesOrAll, - onTap: () {}, - contextMenuBuilder: options != null - ? (context, editableTextState) => - options!.toolbarOptions(context, editableTextState) - : null, - enableInteractiveSelection: options != null, - showCursor: showCursor ?? false, - scrollPhysics: const ClampingScrollPhysics(), + return LineNumbersWrapper( + scrollController: controller, + showLineNumbers: widget.enableLineNumbers, + height: height, + linesCount: maxLinesOrAll, + fontSize: widget.textStyle.fontSize, + child: SelectableText.rich( + TextSpan(children: value.requireData), + key: selectableTextkey, + style: widget.textStyle, + minLines: 1, + maxLines: maxLinesOrAll, + onTap: () {}, + contextMenuBuilder: widget.options != null + ? (context, editableTextState) => + widget.options!.toolbarOptions(context, editableTextState) + : null, + enableInteractiveSelection: widget.options != null, + showCursor: widget.showCursor ?? false, + scrollPhysics: const ClampingScrollPhysics(), + ), ); }, ); @@ -70,14 +105,14 @@ class CodeTextView extends StatelessWidget { Future> _highlights(int maxLinesOrAll) async { final highlights = await HighlightsPlugin().getHighlights( - code, - language, - theme, + widget.code, + widget.language, + widget.theme, [], ); return highlights.toSpans( - code.lines(maxLinesOrAll), - textStyle ?? TextStyles.code(code).style!, + widget.code.lines(maxLinesOrAll), + widget.textStyle, ); } } diff --git a/lib/src/presentation/line_numbers_wrapper.dart b/lib/src/presentation/line_numbers_wrapper.dart new file mode 100644 index 0000000..f2666b4 --- /dev/null +++ b/lib/src/presentation/line_numbers_wrapper.dart @@ -0,0 +1,70 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:kode_view/src/presentation/styles/text_styles.dart'; +import 'package:kode_view/src/utils/extensions/text_extensions.dart'; + +class LineNumbersWrapper extends StatelessWidget { + const LineNumbersWrapper({ + required this.scrollController, + required this.showLineNumbers, + required this.linesCount, + required this.height, + required this.child, + this.fontSize, + this.lineNumberBackgroundColor, + this.lineNumberColor, + super.key, + }); + + final ScrollController scrollController; + final bool showLineNumbers; + final int linesCount; + final double height; + final Widget child; + final Color? lineNumberBackgroundColor; + final Color? lineNumberColor; + final double? fontSize; + + @override + Widget build(BuildContext context) { + final textStyle = TextStyles.lineNumber( + color: lineNumberColor, + fontSize: fontSize, + ); + + final maxLineWidth = (linesCount).toString().getTextWidth(textStyle); + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showLineNumbers) ...[ + Container( + constraints: BoxConstraints( + maxWidth: maxLineWidth, + maxHeight: height, + ), + color: lineNumberBackgroundColor, + child: ListView.builder( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + controller: scrollController, + itemCount: linesCount, + itemBuilder: (context, index) { + return Text( + '${index + 1}', + textAlign: TextAlign.right, + style: textStyle, + ); + }, + ), + ), + const SizedBox(width: 4), + ], + Expanded( + child: child, + ), + ], + ); + } +} diff --git a/lib/src/presentation/styles/text_styles.dart b/lib/src/presentation/styles/text_styles.dart index ee4d9be..ff4abef 100644 --- a/lib/src/presentation/styles/text_styles.dart +++ b/lib/src/presentation/styles/text_styles.dart @@ -1,93 +1,16 @@ import 'package:flutter/material.dart'; -import 'package:kode_view/src/presentation/styles/color_styles.dart'; -class TextStyles extends Text { - final String text; - - const TextStyles.title(this.text, {Key? key}) - : super( - text, - key: key, - style: const TextStyle(fontSize: 16), - ); - - const TextStyles.code(this.text, {Key? key}) - : super( - text, - key: key, - style: const TextStyle( - fontSize: 16, - fontStyle: FontStyle.normal, - ), - ); - - TextStyles.regular(this.text, {Key? key, Color? color}) - : super( - text, - key: key, - style: TextStyle(color: color), - ); - - TextStyles.bold(this.text, {Key? key, Color? color}) - : super( - text, - key: key, - style: TextStyle( - color: color, - fontWeight: FontWeight.w600, - ), - ); - - const TextStyles.secondary(this.text, {Key? key}) +class TextStyles extends TextStyle { + const TextStyles.code() : super( - text, - key: key, - style: const TextStyle(color: Colors.grey), + fontSize: 16, + fontStyle: FontStyle.normal, ); - - const TextStyles.secondaryBold(this.text, {Key? key}) - : super( - text, - key: key, - style: const TextStyle( - color: Colors.grey, - fontWeight: FontWeight.bold, - ), - ); - - const TextStyles.label(this.text, {Key? key}) - : super( - text, - key: key, - style: const TextStyle(fontSize: 12, color: Colors.grey), - ); - - const TextStyles.helper(this.text, {Key? key}) - : super( - text, - key: key, - style: const TextStyle(fontSize: 10, color: Colors.grey), - ); - - TextStyles.appLogo(this.text, {Key? key}) - : super( - text, - key: key, - style: TextStyle( - fontFamily: 'Kanit', - fontSize: 24.0, - color: ColorStyles.accent(), - ), - ); - - const TextStyles.appBarLogo(this.text, {Key? key}) + const TextStyles.lineNumber({Color? color, double? fontSize}) : super( - text, - key: key, - style: const TextStyle( - fontFamily: 'Kanit', - fontSize: 18.0, - color: Colors.black, - ), + color: Colors.black54, + fontSize: fontSize ?? 16, + fontStyle: FontStyle.normal, + height: 1.5, ); } diff --git a/lib/src/presentation/syntax_highlighting_controller.dart b/lib/src/presentation/syntax_highlighting_controller.dart index c5b3b99..8f927f4 100644 --- a/lib/src/presentation/syntax_highlighting_controller.dart +++ b/lib/src/presentation/syntax_highlighting_controller.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:highlights_plugin/highlights_plugin.dart'; -import 'package:kode_view/src/presentation/styles/text_styles.dart'; import 'package:kode_view/src/utils/extensions/collection_extensions.dart'; class SyntaxHighlightingController extends TextEditingController { @@ -17,9 +16,9 @@ class SyntaxHighlightingController extends TextEditingController { Future updateSyntaxHighlighting({ required String code, + required TextStyle textStyle, String? theme, String? language, - TextStyle? textStyle, }) async { try { final highlightedText = await _highlights( @@ -50,7 +49,7 @@ class SyntaxHighlightingController extends TextEditingController { required String code, required String? language, required String? theme, - required TextStyle? textStyle, + required TextStyle textStyle, }) async { final highlights = await HighlightsPlugin().getHighlights( code, @@ -60,7 +59,7 @@ class SyntaxHighlightingController extends TextEditingController { ); return highlights.toSpans( code, - textStyle ?? TextStyles.code(code).style!, + textStyle, ); } } diff --git a/lib/src/utils/extensions/collection_extensions.dart b/lib/src/utils/extensions/collection_extensions.dart index b1e821b..85b0dd8 100644 --- a/lib/src/utils/extensions/collection_extensions.dart +++ b/lib/src/utils/extensions/collection_extensions.dart @@ -2,7 +2,6 @@ import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:highlights_plugin/model/code_highlight.dart'; -import 'package:kode_view/src/presentation/styles/text_styles.dart'; import 'package:kode_view/src/utils/extensions/text_extensions.dart'; class TokenSpan with EquatableMixin { @@ -64,8 +63,7 @@ extension SyntaxSpanExtension on List { ); if (foundToken != null) { - style = - TextStyles.code(text).style!.copyWith(color: foundToken.color); + style = baseStyle.copyWith(color: foundToken.color); } return TextSpan(text: phrase, style: style); diff --git a/lib/src/utils/extensions/text_extensions.dart b/lib/src/utils/extensions/text_extensions.dart index f26b8c8..3098d6b 100644 --- a/lib/src/utils/extensions/text_extensions.dart +++ b/lib/src/utils/extensions/text_extensions.dart @@ -1,5 +1,7 @@ import 'dart:convert'; +import 'package:flutter/material.dart'; + extension TextExtensions on String { String get route => '/$this'; @@ -46,4 +48,15 @@ extension TextExtensions on String { final split = const LineSplitter().convert(this).take(count); return split.join('\n'); } + + double getTextWidth(TextStyle? style) { + final textPainter = TextPainter( + text: TextSpan( + text: this, + style: style, + ), + textDirection: TextDirection.ltr, + )..layout(); + return textPainter.size.width; + } }