editor honours EditorSettings: Tab/Shift+Tab indent + max_line_length ruler (T-29)
EditorController exposes the active buffer's EditorSettings (parsed from the buffer payload, refreshed on editor.settings-changed). EditorView takes over Tab to insert the configured indent (spaces or a tab) and Shift+Tab to dedent — only when a source has an opinion, otherwise Flutter's focus traversal stands. A max_line_length draws a 1px wrap-guide ruler painted behind the text. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+6
-6
@@ -18,12 +18,12 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- **The editor reads `.editorconfig`.** Opening a file resolves the
|
- **The editor honours `.editorconfig`.** Opening a file resolves the
|
||||||
workspace's `.editorconfig` rules (own INI parser + glob matcher, directory
|
workspace rules into a source-agnostic `EditorSettings` (own INI parser +
|
||||||
walk with `root = true` and nearest-wins precedence — no new dependency), and
|
glob matcher, `root`/nearest-wins precedence — no new dependency). The editor
|
||||||
saving applies `end_of_line`, `trim_trailing_whitespace`, and
|
indents with Tab/Shift+Tab and draws a `max_line_length` ruler; saving applies
|
||||||
`insert_final_newline`. The resolved indent/ruler settings ride along on the
|
`end_of_line`, `trim_trailing_whitespace`, and `insert_final_newline`. Saving
|
||||||
buffer for the editor surface to honour. (T-29)
|
the `.editorconfig` re-resolves open buffers live. (T-29)
|
||||||
- **Permission-mode control beside the Claude composer.** An icon-only,
|
- **Permission-mode control beside the Claude composer.** An icon-only,
|
||||||
per-mode-coloured button opens a menu of the safe modes (default ·
|
per-mode-coloured button opens a menu of the safe modes (default ·
|
||||||
accept-edits · plan); `bypass` shows disabled. The status-bar mode is now a
|
accept-edits · plan); `bypass` shows disabled. The status-bar mode is now a
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:clide/clide.dart';
|
import 'package:clide/clide.dart';
|
||||||
import 'package:clide/kernel/kernel.dart';
|
import 'package:clide/kernel/kernel.dart';
|
||||||
|
import 'package:clide/src/editor/editor_settings.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
/// Lightweight view of one open buffer for the tab strip — the
|
/// Lightweight view of one open buffer for the tab strip — the
|
||||||
@@ -36,6 +37,7 @@ class EditorController extends ChangeNotifier {
|
|||||||
Selection _selection = const Selection.collapsed(0);
|
Selection _selection = const Selection.collapsed(0);
|
||||||
bool _dirty = false;
|
bool _dirty = false;
|
||||||
String? _error;
|
String? _error;
|
||||||
|
EditorSettings _settings = EditorSettings.empty;
|
||||||
|
|
||||||
/// All open buffers, in daemon order, for the tab strip.
|
/// All open buffers, in daemon order, for the tab strip.
|
||||||
List<OpenBuffer> _buffers = const [];
|
List<OpenBuffer> _buffers = const [];
|
||||||
@@ -51,6 +53,9 @@ class EditorController extends ChangeNotifier {
|
|||||||
String? get error => _error;
|
String? get error => _error;
|
||||||
List<OpenBuffer> get buffers => _buffers;
|
List<OpenBuffer> get buffers => _buffers;
|
||||||
|
|
||||||
|
/// Effective editor settings for the active buffer (T-29).
|
||||||
|
EditorSettings get settings => _settings;
|
||||||
|
|
||||||
/// On first mount we don't know what's already open. Ask the daemon
|
/// On first mount we don't know what's already open. Ask the daemon
|
||||||
/// for the buffer list and the active buffer.
|
/// for the buffer list and the active buffer.
|
||||||
Future<void> hydrate() async {
|
Future<void> hydrate() async {
|
||||||
@@ -128,6 +133,7 @@ class EditorController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
_activeId = r.data['id']! as String;
|
_activeId = r.data['id']! as String;
|
||||||
_activePath = r.data['path']! as String;
|
_activePath = r.data['path']! as String;
|
||||||
|
_settings = EditorSettings.fromJson(r.data['editorSettings']);
|
||||||
_content = (r.data['content'] as String?) ?? '';
|
_content = (r.data['content'] as String?) ?? '';
|
||||||
final sel = r.data['selection'];
|
final sel = r.data['selection'];
|
||||||
_selection = sel is Map ? Selection.fromJson(sel.cast<String, Object?>()) : const Selection.collapsed(0);
|
_selection = sel is Map ? Selection.fromJson(sel.cast<String, Object?>()) : const Selection.collapsed(0);
|
||||||
@@ -210,6 +216,13 @@ class EditorController extends ChangeNotifier {
|
|||||||
_dirty = false;
|
_dirty = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
case 'editor.settings-changed':
|
||||||
|
// A source (e.g. a saved .editorconfig) re-resolved the buffer's
|
||||||
|
// settings. Refresh the active buffer's copy so indent/ruler update.
|
||||||
|
if (e.data['id'] == _activeId) {
|
||||||
|
_settings = EditorSettings.fromJson(e.data['editorSettings']);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
case 'editor.closed':
|
case 'editor.closed':
|
||||||
// A buffer left the set — refresh the tab list. If it was the
|
// A buffer left the set — refresh the tab list. If it was the
|
||||||
// active one the daemon promotes another and emits
|
// active one the daemon promotes another and emits
|
||||||
@@ -227,6 +240,7 @@ class EditorController extends ChangeNotifier {
|
|||||||
_content = '';
|
_content = '';
|
||||||
_selection = const Selection.collapsed(0);
|
_selection = const Selection.collapsed(0);
|
||||||
_dirty = false;
|
_dirty = false;
|
||||||
|
_settings = EditorSettings.empty;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -183,6 +183,17 @@ class _EditorViewState extends State<EditorView> {
|
|||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tab indents per `.editorconfig` (T-29) — but only when the config has an
|
||||||
|
// opinion, otherwise leave Flutter's default (focus traversal) alone. Skip
|
||||||
|
// in Vim command mode, where keys drive motions.
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.tab && !isCmd && !hw.isAltPressed && !_vimCommandMode) {
|
||||||
|
final unit = _controller?.settings.indentUnit;
|
||||||
|
if (unit != null) {
|
||||||
|
_indent(unit, dedent: hw.isShiftPressed);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final kernel = ClideKernel.of(context);
|
final kernel = ClideKernel.of(context);
|
||||||
final scope = kernel.keymap.scope;
|
final scope = kernel.keymap.scope;
|
||||||
final inNormal = scope['vim.normal'] == true;
|
final inNormal = scope['vim.normal'] == true;
|
||||||
@@ -221,6 +232,45 @@ class _EditorViewState extends State<EditorView> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply one indent step at the caret per the resolved settings (T-29).
|
||||||
|
/// [unit] is the text a Tab inserts (spaces or a tab); [dedent] (Shift+Tab)
|
||||||
|
/// instead strips up to one unit of leading whitespace from the caret's line.
|
||||||
|
/// Writes through [_text] so `_onTextChanged` persists it to the daemon.
|
||||||
|
void _indent(String unit, {required bool dedent}) {
|
||||||
|
final value = _text.value;
|
||||||
|
final sel = value.selection;
|
||||||
|
if (!sel.isValid) return;
|
||||||
|
final text = value.text;
|
||||||
|
|
||||||
|
if (!dedent) {
|
||||||
|
final newText = text.replaceRange(sel.start, sel.end, unit);
|
||||||
|
_text.value = TextEditingValue(
|
||||||
|
text: newText,
|
||||||
|
selection: TextSelection.collapsed(offset: sel.start + unit.length),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dedent: remove leading whitespace from the start of the caret's line.
|
||||||
|
final caret = sel.baseOffset < 0 ? text.length : sel.baseOffset;
|
||||||
|
final lineStart = text.lastIndexOf('\n', caret - 1) + 1; // 0 when on the first line
|
||||||
|
var remove = 0;
|
||||||
|
if (unit == '\t') {
|
||||||
|
if (lineStart < text.length && text[lineStart] == '\t') remove = 1;
|
||||||
|
} else {
|
||||||
|
while (remove < unit.length && lineStart + remove < text.length && text[lineStart + remove] == ' ') {
|
||||||
|
remove++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (remove == 0) return;
|
||||||
|
final newText = text.replaceRange(lineStart, lineStart + remove, '');
|
||||||
|
int shift(int off) => off > lineStart ? (off - remove).clamp(lineStart, newText.length) : off;
|
||||||
|
_text.value = TextEditingValue(
|
||||||
|
text: newText,
|
||||||
|
selection: TextSelection(baseOffset: shift(sel.baseOffset), extentOffset: shift(sel.extentOffset)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _dispatchVim(Intent intent, int count, KernelServices kernel, {required bool visual}) {
|
void _dispatchVim(Intent intent, int count, KernelServices kernel, {required bool visual}) {
|
||||||
if (intent is! InvokeCommandIntent) return;
|
if (intent is! InvokeCommandIntent) return;
|
||||||
final id = intent.commandId;
|
final id = intent.commandId;
|
||||||
@@ -274,6 +324,7 @@ class _EditorViewState extends State<EditorView> {
|
|||||||
background: tokens.panelBackground,
|
background: tokens.panelBackground,
|
||||||
foreground: tokens.globalForeground,
|
foreground: tokens.globalForeground,
|
||||||
accent: tokens.globalFocus,
|
accent: tokens.globalFocus,
|
||||||
|
rulerColumn: c.settings.maxLineLength,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -290,6 +341,7 @@ class _TextBody extends StatelessWidget {
|
|||||||
required this.background,
|
required this.background,
|
||||||
required this.foreground,
|
required this.foreground,
|
||||||
required this.accent,
|
required this.accent,
|
||||||
|
this.rulerColumn,
|
||||||
});
|
});
|
||||||
|
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
@@ -299,8 +351,32 @@ class _TextBody extends StatelessWidget {
|
|||||||
final Color foreground;
|
final Color foreground;
|
||||||
final Color accent;
|
final Color accent;
|
||||||
|
|
||||||
|
/// `max_line_length` from the resolved settings — draws a wrap-guide ruler at
|
||||||
|
/// that column (T-29). Null hides it.
|
||||||
|
final int? rulerColumn;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final style = TextStyle(
|
||||||
|
color: foreground,
|
||||||
|
fontSize: clideFontMono,
|
||||||
|
fontFamily: clideMonoFamily,
|
||||||
|
fontFamilyFallback: clideMonoFamilyFallback,
|
||||||
|
);
|
||||||
|
final editable = EditableText(
|
||||||
|
controller: controller,
|
||||||
|
focusNode: focus,
|
||||||
|
readOnly: readOnly,
|
||||||
|
style: style,
|
||||||
|
cursorColor: foreground,
|
||||||
|
backgroundCursorColor: foreground.withAlpha(0x44),
|
||||||
|
selectionColor: accent.withAlpha(0x55),
|
||||||
|
maxLines: null,
|
||||||
|
expands: true,
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
showCursor: true,
|
||||||
|
);
|
||||||
return Semantics(
|
return Semantics(
|
||||||
label: 'editor text area',
|
label: 'editor text area',
|
||||||
textField: true,
|
textField: true,
|
||||||
@@ -309,27 +385,41 @@ class _TextBody extends StatelessWidget {
|
|||||||
color: background,
|
color: background,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: EditableText(
|
// CustomPaint sizes to and paints behind the EditableText, so the
|
||||||
controller: controller,
|
// ruler shares the text's coordinate space (column 0 at the left edge).
|
||||||
focusNode: focus,
|
child: CustomPaint(
|
||||||
readOnly: readOnly,
|
painter: rulerColumn == null ? null : _RulerPainter(x: _charWidth(style) * rulerColumn!, color: foreground.withAlpha(0x22)),
|
||||||
style: TextStyle(
|
child: editable,
|
||||||
color: foreground,
|
|
||||||
fontSize: clideFontMono,
|
|
||||||
fontFamily: clideMonoFamily,
|
|
||||||
fontFamilyFallback: clideMonoFamilyFallback,
|
|
||||||
),
|
|
||||||
cursorColor: foreground,
|
|
||||||
backgroundCursorColor: foreground.withAlpha(0x44),
|
|
||||||
selectionColor: accent.withAlpha(0x55),
|
|
||||||
maxLines: null,
|
|
||||||
expands: true,
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
showCursor: true,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Advance width of one monospace glyph in [style].
|
||||||
|
static double _charWidth(TextStyle style) {
|
||||||
|
final tp = TextPainter(text: TextSpan(text: '0', style: style), textDirection: TextDirection.ltr)..layout();
|
||||||
|
return tp.width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A 1px vertical wrap-guide at [x] (content-relative), behind the text.
|
||||||
|
class _RulerPainter extends CustomPainter {
|
||||||
|
const _RulerPainter({required this.x, required this.color});
|
||||||
|
|
||||||
|
final double x;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(x, 0),
|
||||||
|
Offset(x, size.height),
|
||||||
|
Paint()
|
||||||
|
..color = color
|
||||||
|
..strokeWidth = 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(_RulerPainter old) => old.x != x || old.color != color;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -429,4 +429,56 @@ void main() {
|
|||||||
expect(c.content, 'local');
|
expect(c.content, 'local');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('editor settings (T-29)', () {
|
||||||
|
Future<void> hydrateWith(Map<String, Object?> read) async {
|
||||||
|
ipc.stub(
|
||||||
|
'editor.list',
|
||||||
|
(_) async => _ok({
|
||||||
|
'buffers': [_buf('b_1', 'lib/a.dart')]
|
||||||
|
}));
|
||||||
|
ipc.stub(
|
||||||
|
'editor.active',
|
||||||
|
(_) async => _ok({
|
||||||
|
'active': {'id': 'b_1'}
|
||||||
|
}));
|
||||||
|
ipc.stub('editor.read', (_) async => _ok(read));
|
||||||
|
await c.hydrate();
|
||||||
|
}
|
||||||
|
|
||||||
|
test('reading a buffer parses its editorSettings', () async {
|
||||||
|
await hydrateWith({
|
||||||
|
..._read('b_1', 'lib/a.dart', 'x'),
|
||||||
|
'editorSettings': {'indent_style': 'space', 'indent_size': 2, 'max_line_length': 80},
|
||||||
|
});
|
||||||
|
expect(c.settings.indentUnit, ' ');
|
||||||
|
expect(c.settings.maxLineLength, 80);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a buffer with no settings exposes empty (no opinion)', () async {
|
||||||
|
await hydrateWith(_read('b_1', 'lib/a.dart', 'x'));
|
||||||
|
expect(c.settings.isEmpty, isTrue);
|
||||||
|
expect(c.settings.indentUnit, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('editor.settings-changed refreshes the active buffer live', () async {
|
||||||
|
await hydrateWith(_read('b_1', 'lib/a.dart', 'x'));
|
||||||
|
expect(c.settings.indentSize, isNull);
|
||||||
|
|
||||||
|
emitEditor(bus, 'editor.settings-changed', {
|
||||||
|
'id': 'b_1',
|
||||||
|
'editorSettings': {'indent_size': 4},
|
||||||
|
});
|
||||||
|
await pumpEventQueue();
|
||||||
|
expect(c.settings.indentSize, 4);
|
||||||
|
|
||||||
|
// An event for some other buffer doesn't touch the active settings.
|
||||||
|
emitEditor(bus, 'editor.settings-changed', {
|
||||||
|
'id': 'b_other',
|
||||||
|
'editorSettings': {'indent_size': 8},
|
||||||
|
});
|
||||||
|
await pumpEventQueue();
|
||||||
|
expect(c.settings.indentSize, 4);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,14 +20,17 @@ IpcResponse _ok(Map<String, Object?> data) => IpcResponse.ok(id: '', data: data)
|
|||||||
|
|
||||||
Map<String, Object?> _buf(String id, String path, {bool dirty = false}) => {'id': id, 'path': path, 'dirty': dirty};
|
Map<String, Object?> _buf(String id, String path, {bool dirty = false}) => {'id': id, 'path': path, 'dirty': dirty};
|
||||||
|
|
||||||
Map<String, Object?> _read(String id, String path) => {
|
Map<String, Object?> _read(String id, String path, {Map<String, Object?>? settings}) => {
|
||||||
'id': id,
|
'id': id,
|
||||||
'path': path,
|
'path': path,
|
||||||
'content': 'content of $path',
|
'content': 'content of $path',
|
||||||
'selection': {'start': 0, 'end': 0},
|
'selection': {'start': 0, 'end': 0},
|
||||||
'dirty': false,
|
'dirty': false,
|
||||||
|
if (settings != null) 'editorSettings': settings,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Finder _ruler() => find.byWidgetPredicate((w) => w is CustomPaint && w.painter?.runtimeType.toString() == '_RulerPainter');
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('EditorView tabs', () {
|
group('EditorView tabs', () {
|
||||||
late KernelFixture f;
|
late KernelFixture f;
|
||||||
@@ -141,5 +144,104 @@ void main() {
|
|||||||
|
|
||||||
expect(saved, 'b_1');
|
expect(saved, 'b_1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void stubOne(String path, {Map<String, Object?>? settings}) {
|
||||||
|
f.ipc.stub(
|
||||||
|
'editor.list',
|
||||||
|
(_) async => _ok({
|
||||||
|
'buffers': [_buf('b_1', path)]
|
||||||
|
}));
|
||||||
|
f.ipc.stub(
|
||||||
|
'editor.active',
|
||||||
|
(_) async => _ok({
|
||||||
|
'active': {'id': 'b_1'}
|
||||||
|
}));
|
||||||
|
f.ipc.stub('editor.read', (_) async => _ok(_read('b_1', path, settings: settings)));
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('Tab indents per the resolved settings (spaces)', (tester) async {
|
||||||
|
stubOne('lib/a.dart', settings: {'indent_style': 'space', 'indent_size': 2});
|
||||||
|
f.ipc.stub('editor.set-content', (_) async => _ok(const {}));
|
||||||
|
await tester.pumpWidget(harness(f, const EditorView()));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final before = tester.widget<EditableText>(find.byType(EditableText)).controller.text;
|
||||||
|
await tester.tap(find.byType(EditableText));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final after = tester.widget<EditableText>(find.byType(EditableText)).controller.text;
|
||||||
|
expect(after.length, before.length + 2); // two spaces inserted
|
||||||
|
expect(after, contains(' ')); // adjacent pair our indent added
|
||||||
|
});
|
||||||
|
|
||||||
|
void stubReadContent(String path, String content, Map<String, Object?> settings) {
|
||||||
|
f.ipc.stub(
|
||||||
|
'editor.list',
|
||||||
|
(_) async => _ok({
|
||||||
|
'buffers': [_buf('b_1', path)]
|
||||||
|
}));
|
||||||
|
f.ipc.stub(
|
||||||
|
'editor.active',
|
||||||
|
(_) async => _ok({
|
||||||
|
'active': {'id': 'b_1'}
|
||||||
|
}));
|
||||||
|
f.ipc.stub(
|
||||||
|
'editor.read',
|
||||||
|
(_) async => _ok({
|
||||||
|
'id': 'b_1',
|
||||||
|
'path': path,
|
||||||
|
'content': content,
|
||||||
|
'selection': {'start': 0, 'end': 0},
|
||||||
|
'dirty': false,
|
||||||
|
'editorSettings': settings
|
||||||
|
}));
|
||||||
|
f.ipc.stub('editor.set-content', (_) async => _ok(const {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('Shift+Tab dedents a space-indented line', (tester) async {
|
||||||
|
stubReadContent('lib/a.dart', ' x', {'indent_style': 'space', 'indent_size': 2});
|
||||||
|
await tester.pumpWidget(harness(f, const EditorView()));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.byType(EditableText));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(tester.widget<EditableText>(find.byType(EditableText)).controller.text, ' x'); // two spaces stripped
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Shift+Tab dedents one leading tab', (tester) async {
|
||||||
|
stubReadContent('lib/a.dart', '\t\tx', {'indent_style': 'tab', 'tab_width': 4});
|
||||||
|
await tester.pumpWidget(harness(f, const EditorView()));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.byType(EditableText));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(tester.widget<EditableText>(find.byType(EditableText)).controller.text, '\tx'); // one tab stripped
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('a max_line_length renders the wrap-guide ruler', (tester) async {
|
||||||
|
stubOne('lib/a.dart', settings: {'max_line_length': 80});
|
||||||
|
await tester.pumpWidget(harness(f, const EditorView()));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(_ruler(), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('no max_line_length draws no ruler', (tester) async {
|
||||||
|
stubOne('lib/a.dart');
|
||||||
|
await tester.pumpWidget(harness(f, const EditorView()));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(_ruler(), findsNothing);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user