read .editorconfig and apply it on open + save (T-29)

New lib/src/editor/editorconfig.dart: a dependency-free INI parser, an
EditorConfig-flavour glob matcher (*, **, ?, [seq], {a,b}, {m..n}), and
directory-walk resolution honouring root=true with nearest-file / last-section
precedence. EditorRegistry resolves the config when a buffer opens (exposed on
the buffer JSON for the UI) and applies end_of_line / trim_trailing_whitespace /
insert_final_newline on save, reconciling the in-memory buffer when the text
changes. 100% line coverage on the new file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 08:48:47 +02:00
co-authored by Claude Opus 4.8
parent 48974d2a29
commit 55c66601c3
8 changed files with 804 additions and 3 deletions
+34 -3
View File
@@ -12,6 +12,7 @@ import 'dart:io';
import '../ipc/envelope.dart';
import '../panes/event_sink.dart';
import 'buffer.dart';
import 'editorconfig.dart';
class EditorRegistry {
EditorRegistry({
@@ -52,7 +53,12 @@ class EditorRegistry {
}
final id = 'b_${_nextId++}';
final buf = EditorBuffer(id: id, path: path, content: content);
final buf = EditorBuffer(
id: id,
path: path,
content: content,
editorConfig: resolveEditorConfig(workspaceRoot, path),
);
_buffers[id] = buf;
_pathToId[path] = id;
@@ -144,12 +150,37 @@ class EditorRegistry {
});
}
/// Persist [id] to disk. Clears the dirty flag on success.
/// Persist [id] to disk. Applies the buffer's `.editorconfig` save fixes
/// (EOL, trailing-whitespace, final-newline) first, and — when those changed
/// the text — reconciles the in-memory buffer + UI so disk and buffer agree.
/// Clears the dirty flag on success.
Future<bool> save(String id) async {
final buf = _buffers[id];
if (buf == null) return false;
final normalized = applyEditorConfigOnSave(buf.content, buf.editorConfig);
final changed = normalized != buf.content;
final absolute = _absolutePathOf(buf.path);
await File(absolute).writeAsString(buf.content);
await File(absolute).writeAsString(normalized);
if (changed) {
buf.content = normalized;
buf.selection = Selection(
start: buf.selection.start.clamp(0, normalized.length),
end: buf.selection.end.clamp(0, normalized.length),
);
// Re-broadcast so the UI reloads the normalized text (the editor.edited
// handler re-reads the buffer); emitted before editor.saved clears dirty.
buf.dirty = false;
_emit('editor.edited', {
'id': id,
'kind': 'replace',
'length': normalized.length,
'selection': buf.selection.toJson(),
});
}
buf.dirty = false;
_emit('editor.saved', {'id': id, 'path': buf.path});
return true;