mirror of
https://github.com/qinglong-app/qinglong_app.git
synced 2025-10-09 16:48:19 +08:00
新增代码文件支持行号显示
This commit is contained in:
2
pub/code_field-master/lib/code_text_field.dart
Normal file
2
pub/code_field-master/lib/code_text_field.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
export 'src/code_field.dart';
|
||||
export 'src/code_controller.dart';
|
||||
273
pub/code_field-master/lib/src/code_controller.dart
Normal file
273
pub/code_field-master/lib/src/code_controller.dart
Normal file
@@ -0,0 +1,273 @@
|
||||
import 'dart:math';
|
||||
import 'package:code_text_field/src/code_modifier.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:highlight/highlight_core.dart';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
|
||||
const _MIDDLE_DOT = '·';
|
||||
|
||||
class EditorParams {
|
||||
final int tabSpaces;
|
||||
|
||||
const EditorParams({this.tabSpaces = 2});
|
||||
}
|
||||
|
||||
class CodeController extends TextEditingController {
|
||||
/// A highlight language to parse the text with
|
||||
final Mode? language;
|
||||
|
||||
/// The theme to apply to the [language] parsing result
|
||||
final Map<String, TextStyle>? theme;
|
||||
|
||||
/// A map of specific regexes to style
|
||||
final Map<String, TextStyle>? patternMap;
|
||||
|
||||
/// A map of specific keywords to style
|
||||
final Map<String, TextStyle>? stringMap;
|
||||
|
||||
/// Common editor params such as the size of a tab in spaces
|
||||
///
|
||||
/// Will be exposed to all [modifiers]
|
||||
final EditorParams params;
|
||||
|
||||
/// A list of code modifiers to dynamically update the code upon certain keystrokes
|
||||
final List<CodeModifier> modifiers;
|
||||
|
||||
/// On web, replace spaces with invisible dots “·” to fix the current issue with spaces
|
||||
///
|
||||
/// https://github.com/flutter/flutter/issues/77929
|
||||
final bool webSpaceFix;
|
||||
|
||||
/// onChange callback, called whenever the content is changed
|
||||
final void Function(String)? onChange;
|
||||
|
||||
/* Computed members */
|
||||
final String languageId = _genId();
|
||||
final styleList = <TextStyle>[];
|
||||
final modifierMap = <String, CodeModifier>{};
|
||||
RegExp? styleRegExp;
|
||||
|
||||
CodeController({
|
||||
String? text,
|
||||
this.language,
|
||||
this.theme,
|
||||
this.patternMap,
|
||||
this.stringMap,
|
||||
this.params = const EditorParams(),
|
||||
this.modifiers = const <CodeModifier>[
|
||||
const IntendModifier(),
|
||||
const CloseBlockModifier(),
|
||||
const TabModifier(),
|
||||
],
|
||||
this.webSpaceFix = true,
|
||||
this.onChange,
|
||||
}) : super(text: text) {
|
||||
// PatternMap
|
||||
if (language != null && theme == null) throw Exception("A theme must be provided for language parsing");
|
||||
// Register language
|
||||
if (language != null) {
|
||||
highlight.registerLanguage(languageId, language!);
|
||||
}
|
||||
// Create modifier map
|
||||
modifiers.forEach((el) {
|
||||
modifierMap[el.char] = el;
|
||||
});
|
||||
}
|
||||
|
||||
/// Sets a specific cursor position in the text
|
||||
void setCursor(int offset) {
|
||||
selection = TextSelection.collapsed(offset: offset);
|
||||
}
|
||||
|
||||
/// Replaces the current [selection] by [str]
|
||||
void insertStr(String str) {
|
||||
final sel = selection;
|
||||
text = text.replaceRange(selection.start, selection.end, str);
|
||||
final len = str.length;
|
||||
selection = sel.copyWith(
|
||||
baseOffset: sel.start + len,
|
||||
extentOffset: sel.start + len,
|
||||
);
|
||||
}
|
||||
|
||||
/// Remove the char just before the cursor or the selection
|
||||
void removeChar() {
|
||||
if (selection.start < 1) return;
|
||||
final sel = selection;
|
||||
text = text.replaceRange(selection.start - 1, selection.start, "");
|
||||
selection = sel.copyWith(
|
||||
baseOffset: sel.start - 1,
|
||||
extentOffset: sel.start - 1,
|
||||
);
|
||||
}
|
||||
|
||||
/// Remove the selected text
|
||||
void removeSelection() {
|
||||
final sel = selection;
|
||||
text = text.replaceRange(selection.start, selection.end, "");
|
||||
selection = sel.copyWith(
|
||||
baseOffset: sel.start,
|
||||
extentOffset: sel.start,
|
||||
);
|
||||
}
|
||||
|
||||
/// Remove the selection or last char if the selection is empty
|
||||
void backspace() {
|
||||
if (selection.start < selection.end)
|
||||
removeSelection();
|
||||
else
|
||||
removeChar();
|
||||
}
|
||||
|
||||
KeyEventResult onKey(RawKeyEvent event) {
|
||||
if (event.isKeyPressed(LogicalKeyboardKey.tab)) {
|
||||
text = text.replaceRange(selection.start, selection.end, "\t");
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
/// See webSpaceFix
|
||||
static String _spacesToMiddleDots(String str) {
|
||||
return str.replaceAll(" ", _MIDDLE_DOT);
|
||||
}
|
||||
|
||||
/// See webSpaceFix
|
||||
static String _middleDotsToSpaces(String str) {
|
||||
return str.replaceAll(_MIDDLE_DOT, " ");
|
||||
}
|
||||
|
||||
/// Get untransformed text
|
||||
/// See webSpaceFix
|
||||
String get rawText {
|
||||
if (!_webSpaceFix) return super.text;
|
||||
return _middleDotsToSpaces(super.text);
|
||||
}
|
||||
|
||||
// Private methods
|
||||
bool get _webSpaceFix => kIsWeb && webSpaceFix;
|
||||
|
||||
static String _genId() {
|
||||
const _chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
|
||||
final _rnd = Random();
|
||||
return String.fromCharCodes(
|
||||
Iterable.generate(10, (_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length))),
|
||||
);
|
||||
}
|
||||
|
||||
int? _insertedLoc(String a, String b) {
|
||||
final sel = selection;
|
||||
if (a.length + 1 != b.length || sel.start != sel.end) return null;
|
||||
return sel.start;
|
||||
}
|
||||
|
||||
@override
|
||||
set value(TextEditingValue newValue) {
|
||||
final loc = _insertedLoc(text, newValue.text);
|
||||
if (loc != null) {
|
||||
final char = newValue.text[loc];
|
||||
final modifier = modifierMap[char];
|
||||
final val = modifier?.updateString(rawText, selection, params);
|
||||
if (val != null) {
|
||||
// Update newValue
|
||||
newValue = newValue.copyWith(
|
||||
text: val.text,
|
||||
selection: val.selection,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Now fix the textfield for web
|
||||
if (_webSpaceFix) newValue = newValue.copyWith(text: _spacesToMiddleDots(newValue.text));
|
||||
if (onChange != null) onChange!(_webSpaceFix ? _middleDotsToSpaces(newValue.text) : newValue.text);
|
||||
super.value = newValue;
|
||||
}
|
||||
|
||||
TextSpan _processPatterns(String text, TextStyle? style) {
|
||||
final children = <TextSpan>[];
|
||||
text.splitMapJoin(
|
||||
styleRegExp!,
|
||||
onMatch: (Match m) {
|
||||
if (styleList.isEmpty) return '';
|
||||
int idx;
|
||||
for (idx = 1; idx < m.groupCount && idx <= styleList.length && m.group(idx) == null; idx++) {}
|
||||
children.add(TextSpan(
|
||||
text: m[0],
|
||||
style: styleList[idx - 1],
|
||||
));
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (String span) {
|
||||
children.add(TextSpan(text: span, style: style));
|
||||
return '';
|
||||
},
|
||||
);
|
||||
return TextSpan(style: style, children: children);
|
||||
}
|
||||
|
||||
TextSpan _processLanguage(String text, TextStyle? style) {
|
||||
final rawText = _webSpaceFix ? _middleDotsToSpaces(text) : text;
|
||||
final result = highlight.parse(rawText, language: languageId);
|
||||
|
||||
final nodes = result.nodes;
|
||||
|
||||
final children = <TextSpan>[];
|
||||
var currentSpans = children;
|
||||
final stack = <List<TextSpan>>[];
|
||||
|
||||
void _traverse(Node node) {
|
||||
var val = node.value;
|
||||
final nodeChildren = node.children;
|
||||
if (val != null) {
|
||||
if (_webSpaceFix) val = _spacesToMiddleDots(val);
|
||||
var child = TextSpan(text: val, style: theme?[node.className]);
|
||||
if (styleRegExp != null) child = _processPatterns(val, theme?[node.className]);
|
||||
currentSpans.add(child);
|
||||
} else if (nodeChildren != null) {
|
||||
List<TextSpan> tmp = [];
|
||||
currentSpans.add(TextSpan(
|
||||
children: tmp,
|
||||
style: theme?[node.className],
|
||||
));
|
||||
stack.add(currentSpans);
|
||||
currentSpans = tmp;
|
||||
nodeChildren.forEach((n) {
|
||||
_traverse(n);
|
||||
if (n == nodeChildren.last) {
|
||||
currentSpans = stack.isEmpty ? children : stack.removeLast();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (nodes != null) for (var node in nodes) _traverse(node);
|
||||
return TextSpan(style: style, children: children);
|
||||
}
|
||||
|
||||
@override
|
||||
TextSpan buildTextSpan({required BuildContext context, TextStyle? style, bool? withComposing}) {
|
||||
// Retrieve pattern regexp
|
||||
final patternList = <String>[];
|
||||
if (_webSpaceFix) {
|
||||
patternList.add("(" + _MIDDLE_DOT + ")");
|
||||
styleList.add(TextStyle(color: Colors.transparent));
|
||||
}
|
||||
if (stringMap != null) {
|
||||
patternList.addAll(stringMap!.keys.map((e) => r'(\b' + e + r'\b)'));
|
||||
styleList.addAll(stringMap!.values);
|
||||
}
|
||||
if (patternMap != null) {
|
||||
patternList.addAll(patternMap!.keys.map((e) => "(" + e + ")"));
|
||||
styleList.addAll(patternMap!.values);
|
||||
}
|
||||
styleRegExp = RegExp(patternList.join('|'), multiLine: true);
|
||||
|
||||
// Return parsing
|
||||
if (language != null)
|
||||
return _processLanguage(text, style);
|
||||
else if (styleRegExp != null)
|
||||
return _processPatterns(text, style);
|
||||
else
|
||||
return TextSpan(text: text, style: style);
|
||||
}
|
||||
}
|
||||
336
pub/code_field-master/lib/src/code_field.dart
Normal file
336
pub/code_field-master/lib/src/code_field.dart
Normal file
@@ -0,0 +1,336 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:linked_scroll_controller/linked_scroll_controller.dart';
|
||||
|
||||
import './code_controller.dart';
|
||||
|
||||
class LineNumberController extends TextEditingController {
|
||||
final TextSpan Function(int, TextStyle?)? lineNumberBuilder;
|
||||
|
||||
LineNumberController(this.lineNumberBuilder);
|
||||
|
||||
@override
|
||||
TextSpan buildTextSpan({required BuildContext context, TextStyle? style, bool? withComposing}) {
|
||||
final children = <TextSpan>[];
|
||||
final list = text.split("\n");
|
||||
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);
|
||||
children.add(textSpan);
|
||||
if (k < list.length - 1) children.add(TextSpan(text: "\n"));
|
||||
}
|
||||
return TextSpan(children: children, style: style);
|
||||
}
|
||||
}
|
||||
|
||||
class LineNumberStyle {
|
||||
/// Width of the line number column
|
||||
final double width;
|
||||
|
||||
/// Alignment of the numbers in the column
|
||||
final TextAlign textAlign;
|
||||
|
||||
/// Style of the numbers
|
||||
final TextStyle? textStyle;
|
||||
|
||||
/// Background of the line number column
|
||||
final Color? background;
|
||||
|
||||
/// Central horizontal margin between the numbers and the code
|
||||
final double margin;
|
||||
|
||||
const LineNumberStyle({
|
||||
this.width = 42.0,
|
||||
this.textAlign = TextAlign.right,
|
||||
this.margin = 10.0,
|
||||
this.textStyle,
|
||||
this.background,
|
||||
});
|
||||
}
|
||||
|
||||
class CodeField extends StatefulWidget {
|
||||
/// {@macro flutter.widgets.textField.minLines}
|
||||
final SmartQuotesType smartQuotesType;
|
||||
|
||||
/// {@macro flutter.widgets.textField.minLines}
|
||||
final int? minLines;
|
||||
|
||||
/// {@macro flutter.widgets.textField.maxLInes}
|
||||
final int? maxLines;
|
||||
|
||||
/// {@macro flutter.widgets.textField.expands}
|
||||
final bool expands;
|
||||
|
||||
/// Whether overflowing lines should wrap around or make the field scrollable horizontally
|
||||
final bool wrap;
|
||||
|
||||
/// A CodeController instance to apply language highlight, themeing and modifiers
|
||||
final CodeController controller;
|
||||
|
||||
/// A LineNumberStyle instance to tweak the line number column styling
|
||||
final LineNumberStyle lineNumberStyle;
|
||||
|
||||
/// {@macro flutter.widgets.textField.cursorColor}
|
||||
final Color? cursorColor;
|
||||
|
||||
/// {@macro flutter.widgets.textField.textStyle}
|
||||
final TextStyle? textStyle;
|
||||
|
||||
/// A way to replace specific line numbers by a custom TextSpan
|
||||
final TextSpan Function(int, TextStyle?)? lineNumberBuilder;
|
||||
|
||||
/// {@macro flutter.widgets.textField.enabled}
|
||||
final bool? enabled;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.onChanged}
|
||||
final void Function(String)? onChanged;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.readOnly}
|
||||
final bool readOnly;
|
||||
|
||||
final Color? background;
|
||||
final EdgeInsets padding;
|
||||
final Decoration? decoration;
|
||||
final TextSelectionThemeData? textSelectionTheme;
|
||||
final FocusNode? focusNode;
|
||||
final void Function()? onTap;
|
||||
final bool hideColumn;
|
||||
|
||||
const CodeField({
|
||||
Key? key,
|
||||
required this.controller,
|
||||
this.minLines,
|
||||
this.maxLines,
|
||||
this.expands = false,
|
||||
this.wrap = false,
|
||||
this.background,
|
||||
this.decoration,
|
||||
this.textStyle,
|
||||
this.padding = const EdgeInsets.symmetric(),
|
||||
this.lineNumberStyle = const LineNumberStyle(),
|
||||
this.enabled,
|
||||
this.hideColumn = false,
|
||||
this.onTap,
|
||||
this.readOnly = false,
|
||||
this.cursorColor,
|
||||
this.textSelectionTheme,
|
||||
this.lineNumberBuilder,
|
||||
this.focusNode,
|
||||
this.onChanged,
|
||||
this.smartQuotesType = SmartQuotesType.enabled,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
CodeFieldState createState() => CodeFieldState();
|
||||
}
|
||||
|
||||
class CodeFieldState extends State<CodeField> {
|
||||
// Add a controller
|
||||
LinkedScrollControllerGroup? _controllers;
|
||||
ScrollController? _numberScroll;
|
||||
ScrollController? _codeScroll;
|
||||
LineNumberController? _numberController;
|
||||
|
||||
StreamSubscription<bool>? _keyboardVisibilitySubscription;
|
||||
FocusNode? _focusNode;
|
||||
String? lines;
|
||||
String longestLine = "";
|
||||
|
||||
ScrollController? getCodeScroll() {
|
||||
return _codeScroll;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controllers = LinkedScrollControllerGroup();
|
||||
_numberScroll = _controllers?.addAndGet();
|
||||
_codeScroll = _controllers?.addAndGet();
|
||||
_numberController = LineNumberController(widget.lineNumberBuilder);
|
||||
widget.controller.addListener(_onTextChanged);
|
||||
_focusNode = widget.focusNode ?? FocusNode();
|
||||
_focusNode!.onKey = _onKey;
|
||||
_focusNode!.attach(context, onKey: _onKey);
|
||||
|
||||
_onTextChanged();
|
||||
}
|
||||
|
||||
ScrollController? codeScrollController() {
|
||||
return _codeScroll;
|
||||
}
|
||||
|
||||
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
|
||||
if (widget.readOnly) return KeyEventResult.ignored;
|
||||
return widget.controller.onKey(event);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(_onTextChanged);
|
||||
_numberScroll?.dispose();
|
||||
_codeScroll?.dispose();
|
||||
_numberController?.dispose();
|
||||
_keyboardVisibilitySubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void rebuild() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _onTextChanged() {
|
||||
// Rebuild line number
|
||||
final str = widget.controller.text.split("\n");
|
||||
final buf = <String>[];
|
||||
for (var k = 0; k < str.length; k++) {
|
||||
buf.add((k + 1).toString());
|
||||
}
|
||||
_numberController?.text = buf.join("\n");
|
||||
// Find longest line
|
||||
longestLine = "";
|
||||
widget.controller.text.split("\n").forEach((line) {
|
||||
if (line.length > longestLine.length) longestLine = line;
|
||||
});
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
// Wrap the codeField in a horizontal scrollView
|
||||
Widget _wrapInScrollView(Widget codeField, TextStyle textStyle, double minWidth) {
|
||||
final leftPad = widget.lineNumberStyle.margin / 2;
|
||||
final intrinsic = IntrinsicWidth(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 0.0,
|
||||
minWidth: max(minWidth - leftPad, 0.0),
|
||||
),
|
||||
child: Padding(
|
||||
child: Text(longestLine, style: textStyle),
|
||||
padding: const EdgeInsets.only(right: 16.0),
|
||||
), // Add extra padding
|
||||
),
|
||||
widget.expands ? Expanded(child: codeField) : codeField,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: EdgeInsets.only(
|
||||
left: leftPad,
|
||||
right: widget.padding.right,
|
||||
),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: intrinsic,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Default color scheme
|
||||
const ROOT_KEY = 'root';
|
||||
final defaultBg = Colors.grey.shade900;
|
||||
final defaultText = Colors.grey.shade200;
|
||||
|
||||
final theme = widget.controller.theme;
|
||||
Color? backgroundCol = widget.background ?? theme?[ROOT_KEY]?.backgroundColor ?? defaultBg;
|
||||
if (widget.decoration != null) {
|
||||
backgroundCol = null;
|
||||
}
|
||||
TextStyle textStyle = widget.textStyle ?? TextStyle();
|
||||
textStyle = textStyle.copyWith(
|
||||
color: textStyle.color ?? theme?[ROOT_KEY]?.color ?? defaultText,
|
||||
fontSize: textStyle.fontSize ?? 16.0,
|
||||
);
|
||||
TextStyle numberTextStyle = widget.lineNumberStyle.textStyle ?? TextStyle();
|
||||
final numberColor = (theme?[ROOT_KEY]?.color ?? defaultText).withOpacity(0.7);
|
||||
// Copy important attributes
|
||||
numberTextStyle = numberTextStyle.copyWith(
|
||||
color: numberTextStyle.color ?? numberColor,
|
||||
fontSize: textStyle.fontSize,
|
||||
fontFamily: textStyle.fontFamily,
|
||||
);
|
||||
final cursorColor = widget.cursorColor ?? theme?[ROOT_KEY]?.color ?? defaultText;
|
||||
|
||||
final lineNumberCol = TextField(
|
||||
smartQuotesType: widget.smartQuotesType,
|
||||
scrollPadding: widget.padding,
|
||||
style: numberTextStyle,
|
||||
controller: _numberController,
|
||||
enabled: false,
|
||||
minLines: widget.minLines,
|
||||
maxLines: widget.maxLines,
|
||||
expands: widget.expands,
|
||||
scrollController: _numberScroll,
|
||||
decoration: InputDecoration(
|
||||
disabledBorder: InputBorder.none,
|
||||
),
|
||||
textAlign: widget.lineNumberStyle.textAlign,
|
||||
);
|
||||
|
||||
final numberCol = Container(
|
||||
width: widget.lineNumberStyle.width,
|
||||
padding: EdgeInsets.only(
|
||||
left: widget.padding.left,
|
||||
right: widget.lineNumberStyle.margin / 2,
|
||||
),
|
||||
color: widget.lineNumberStyle.background,
|
||||
child: lineNumberCol,
|
||||
);
|
||||
|
||||
final codeField = TextField(
|
||||
smartQuotesType: widget.smartQuotesType,
|
||||
focusNode: _focusNode,
|
||||
onTap: widget.onTap,
|
||||
scrollPadding: widget.padding,
|
||||
style: textStyle,
|
||||
controller: widget.controller,
|
||||
minLines: widget.minLines,
|
||||
maxLines: widget.maxLines,
|
||||
expands: widget.expands,
|
||||
scrollController: _codeScroll,
|
||||
decoration: InputDecoration(
|
||||
disabledBorder: InputBorder.none,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
),
|
||||
cursorColor: cursorColor,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
enabled: widget.enabled,
|
||||
onChanged: widget.onChanged,
|
||||
readOnly: widget.readOnly,
|
||||
);
|
||||
|
||||
final codeCol = Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
textSelectionTheme: widget.textSelectionTheme,
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
// Control horizontal scrolling
|
||||
return widget.wrap ? codeField : _wrapInScrollView(codeField, textStyle, constraints.maxWidth);
|
||||
},
|
||||
),
|
||||
);
|
||||
return Container(
|
||||
decoration: widget.decoration,
|
||||
color: backgroundCol,
|
||||
child: widget.hideColumn
|
||||
? codeCol
|
||||
: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
numberCol,
|
||||
Expanded(child: codeCol),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
79
pub/code_field-master/lib/src/code_modifier.dart
Normal file
79
pub/code_field-master/lib/src/code_modifier.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
import 'dart:math';
|
||||
import 'package:code_text_field/code_text_field.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
abstract class CodeModifier {
|
||||
final String char;
|
||||
|
||||
const CodeModifier(this.char);
|
||||
|
||||
// Helper to insert [str] in [text] between [start] and [end]
|
||||
TextEditingValue replace(String text, int start, int end, String str) {
|
||||
final len = str.length;
|
||||
return TextEditingValue(
|
||||
text: text.replaceRange(start, end, str),
|
||||
selection: TextSelection(
|
||||
baseOffset: start + len,
|
||||
extentOffset: start + len,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TextEditingValue? updateString(String text, TextSelection sel, EditorParams params);
|
||||
}
|
||||
|
||||
class IntendModifier extends CodeModifier {
|
||||
final bool handleBrackets;
|
||||
|
||||
const IntendModifier({
|
||||
this.handleBrackets = true,
|
||||
}) : super('\n');
|
||||
|
||||
@override
|
||||
TextEditingValue? updateString(String text, TextSelection sel, EditorParams params) {
|
||||
var spacesCount = 0;
|
||||
var braceCount = 0;
|
||||
for (var k = min(sel.start, text.length) - 1; k >= 0; k--) {
|
||||
if (text[k] == "\n") break;
|
||||
if (text[k] == " ")
|
||||
spacesCount += 1;
|
||||
else
|
||||
spacesCount = 0;
|
||||
if (text[k] == "{")
|
||||
braceCount += 1;
|
||||
else if (text[k] == "}") braceCount -= 1;
|
||||
}
|
||||
if (braceCount > 0) spacesCount += params.tabSpaces;
|
||||
final insert = "\n" + " " * spacesCount;
|
||||
return replace(text, sel.start, sel.end, insert);
|
||||
}
|
||||
}
|
||||
|
||||
class CloseBlockModifier extends CodeModifier {
|
||||
const CloseBlockModifier() : super('}');
|
||||
|
||||
@override
|
||||
TextEditingValue? updateString(String text, TextSelection sel, EditorParams params) {
|
||||
int spaceCount = 0;
|
||||
for (var k = min(sel.start, text.length) - 1; k >= 0; k--) {
|
||||
if (text[k] == "\n") break;
|
||||
if (text[k] != " ") {
|
||||
spaceCount = 0;
|
||||
break;
|
||||
}
|
||||
spaceCount += 1;
|
||||
}
|
||||
if (spaceCount >= params.tabSpaces) return replace(text, sel.start - params.tabSpaces, sel.end, "}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class TabModifier extends CodeModifier {
|
||||
const TabModifier() : super('\t');
|
||||
|
||||
@override
|
||||
TextEditingValue? updateString(String text, TextSelection sel, EditorParams params) {
|
||||
final tmp = replace(text, sel.start, sel.end, " " * params.tabSpaces);
|
||||
return tmp;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user