diff --git a/lib/components/code.dart b/lib/components/code.dart index 5bd1f5c..e72ae16 100644 --- a/lib/components/code.dart +++ b/lib/components/code.dart @@ -12,7 +12,7 @@ class CodeEditor extends StatefulWidget { } class _CodeEditorState extends State { - late TextEditingController _controller; + late _CodeTextEditingController _controller; late FocusNode _focusNode; var horizontalScrollController = ScrollController(); var verticalScrollController = ScrollController(); @@ -20,7 +20,7 @@ class _CodeEditorState extends State { @override void initState() { super.initState(); - _controller = TextEditingController(text: widget.initialValue); + _controller = _CodeTextEditingController(text: widget.initialValue); _focusNode = FocusNode() ..onKeyEvent = (node, event) { if (event.logicalKey == LogicalKeyboardKey.tab) { @@ -43,45 +43,54 @@ class _CodeEditorState extends State { @override Widget build(BuildContext context) { - return Scrollbar( - thumbVisibility: true, - controller: verticalScrollController, - notificationPredicate: (notif) => notif.metrics.axis == Axis.vertical, - child: Scrollbar( - thumbVisibility: true, - controller: horizontalScrollController, - notificationPredicate: (notif) => notif.metrics.axis == Axis.horizontal, - child: SizedBox.expand( - child: ScrollConfiguration( - behavior: _CustomScrollBehavior(), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - controller: horizontalScrollController, - child: IntrinsicWidth( - stepWidth: 100, - child: TextField( - style: TextStyle( - fontFamily: 'consolas', - fontFamilyFallback: ['Courier New', 'monospace'], + return FutureBuilder( + future: _controller.init(context.brightness), + builder: (context, value) { + if (value.connectionState == ConnectionState.waiting) { + return const SizedBox(); + } + return Scrollbar( + thumbVisibility: true, + controller: verticalScrollController, + notificationPredicate: (notif) => notif.metrics.axis == Axis.vertical, + child: Scrollbar( + thumbVisibility: true, + controller: horizontalScrollController, + notificationPredicate: (notif) => + notif.metrics.axis == Axis.horizontal, + child: SizedBox.expand( + child: ScrollConfiguration( + behavior: _CustomScrollBehavior(), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + controller: horizontalScrollController, + child: IntrinsicWidth( + stepWidth: 100, + child: TextField( + style: TextStyle( + fontFamily: 'consolas', + fontFamilyFallback: ['Courier New', 'monospace'], + ), + controller: _controller, + focusNode: _focusNode, + maxLines: null, + expands: true, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.all(8), + ), + onChanged: (value) { + widget.onChanged?.call(value); + }, + scrollController: verticalScrollController, + ), ), - controller: _controller, - focusNode: _focusNode, - maxLines: null, - expands: true, - decoration: InputDecoration( - border: InputBorder.none, - contentPadding: EdgeInsets.all(8), - ), - onChanged: (value) { - widget.onChanged?.call(value); - }, - scrollController: verticalScrollController, ), ), ), ), - ), - ), + ); + }, ); } } @@ -94,3 +103,203 @@ class _CustomScrollBehavior extends MaterialScrollBehavior { return child; } } + +class _CodeTextEditingController extends TextEditingController { + _CodeTextEditingController({super.text}); + + HighlighterTheme? _theme; + + Future init(Brightness brightness) async { + Highlighter.addLanguage('js', _jsGrammer); + _theme = await HighlighterTheme.loadForBrightness(brightness); + } + + @override + TextSpan buildTextSpan( + {required BuildContext context, + TextStyle? style, + required bool withComposing}) { + var highlighter = Highlighter( + language: 'js', + theme: _theme!, + ); + var result = highlighter.highlight(text); + return result; + } +} + +const _jsGrammer = r''' +{ + "name": "JavaScript", + "version": "1.0.0", + "fileTypes": ["js", "mjs", "cjs"], + "scopeName": "source.js", + + "foldingStartMarker": "\\{\\s*$", + "foldingStopMarker": "^\\s*\\}", + + "patterns": [ + { + "name": "meta.preprocessor.script.js", + "match": "^(#!.*)$" + }, + { + "name": "meta.import-export.js", + "begin": "\\b(import|export)\\b", + "beginCaptures": { + "0": { + "name": "keyword.control.import.js" + } + }, + "end": ";", + "endCaptures": { + "0": { + "name": "punctuation.terminator.js" + } + }, + "patterns": [ + { + "include": "#strings" + }, + { + "include": "#comments" + }, + { + "name": "keyword.control.import.js", + "match": "\\b(as|from)\\b" + } + ] + }, + { + "include": "#comments" + }, + { + "include": "#keywords" + }, + { + "include": "#constants-and-special-vars" + }, + { + "include": "#operators" + }, + { + "include": "#strings" + } + ], + + "repository": { + "comments": { + "patterns": [ + { + "name": "comment.block.js", + "begin": "/\\*", + "end": "\\*/" + }, + { + "name": "comment.line.double-slash.js", + "match": "//.*$" + } + ] + }, + "keywords": { + "patterns": [ + { + "name": "keyword.control.js", + "match": "\\b(if|else|for|while|do|switch|case|default|break|continue|return|throw|try|catch|finally)\\b" + }, + { + "name": "keyword.operator.js", + "match": "\\b(instanceof|typeof|new|delete|in|void)\\b" + }, + { + "name": "storage.type.js", + "match": "\\b(var|let|const|function|class|extends)\\b" + }, + { + "name": "keyword.declaration.js", + "match": "\\b(export|import|default)\\b" + } + ] + }, + "constants-and-special-vars": { + "patterns": [ + { + "name": "constant.language.js", + "match": "\\b(true|false|null|undefined|NaN|Infinity)\\b" + }, + { + "name": "constant.numeric.js", + "match": "\\b(0x[0-9A-Fa-f]+|[0-9]+\\.?[0-9]*(e[+-]?[0-9]+)?)\\b" + } + ] + }, + "operators": { + "patterns": [ + { + "name": "keyword.operator.assignment.js", + "match": "(=|\\+=|-=|\\*=|/=|%=|\\|=|&=|\\^=|<<=|>>=|>>>=)" + }, + { + "name": "keyword.operator.comparison.js", + "match": "(==|!=|===|!==|<|<=|>|>=)" + }, + { + "name": "keyword.operator.logical.js", + "match": "(&&|\\|\\||!)" + }, + { + "name": "keyword.operator.arithmetic.js", + "match": "(-|\\+|\\*|/|%)" + }, + { + "name": "keyword.operator.bitwise.js", + "match": "(\\||&|\\^|~|<<|>>|>>>)" + } + ] + }, + "strings": { + "patterns": [ + { + "name": "string.quoted.double.js", + "begin": "\"", + "end": "\"", + "patterns": [ + { + "include": "#string-interpolation" + } + ] + }, + { + "name": "string.quoted.single.js", + "begin": "'", + "end": "'", + "patterns": [ + { + "include": "#string-interpolation" + } + ] + }, + { + "name": "string.template.js", + "begin": "`", + "end": "`", + "patterns": [ + { + "include": "#string-interpolation" + } + ] + } + ] + }, + "string-interpolation": { + "patterns": [ + { + "name": "variable.parameter.js", + "begin": "\\$\\{", + "end": "\\}" + } + ] + } + } +} +'''; diff --git a/lib/components/components.dart b/lib/components/components.dart index 55c8d9c..cef1056 100644 --- a/lib/components/components.dart +++ b/lib/components/components.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:syntax_highlight/syntax_highlight.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/app_page_route.dart'; import 'package:venera/foundation/appdata.dart'; diff --git a/pubspec.lock b/pubspec.lock index b5b6a9c..fcac644 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -944,6 +944,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + syntax_highlight: + dependency: "direct main" + description: + name: syntax_highlight + sha256: ee33b6aa82cc722bb9b40152a792181dee222353b486c0255fde666a3e3a4997 + url: "https://pub.dev" + source: hosted + version: "0.4.0" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7fb2bbf..465794e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -71,6 +71,7 @@ dependencies: dynamic_color: ^1.7.0 shimmer: ^3.0.0 flutter_memory_info: ^0.0.1 + syntax_highlight: ^0.4.0 dev_dependencies: flutter_test: