route editor config through a source-agnostic EditorSettings model (T-29)

Introduce EditorSettings as the effective, source-agnostic editor configuration
the editor and save path obey. .editorconfig is demoted to one *source*
(editorconfig.dart now returns EditorSettings); editor_settings_resolver.dart is
the single composition seam where future sources — a settings panel, a clide
settings file — layer in via merge() without the editor changing.

The registry resolves settings on buffer load and, when a .editorconfig is saved
in-app, re-resolves every open buffer and emits editor.settings-changed (a hook
in save, not a filesystem watcher — the realistic case, cheaply). Buffer JSON
carries editorSettings. 100% line coverage on the new model + resolver.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 09:15:49 +02:00
co-authored by Claude Opus 4.8
parent 55c66601c3
commit d4b39e430a
10 changed files with 438 additions and 277 deletions
+28 -7
View File
@@ -12,7 +12,7 @@ import 'dart:io';
import '../ipc/envelope.dart';
import '../panes/event_sink.dart';
import 'buffer.dart';
import 'editorconfig.dart';
import 'editor_settings_resolver.dart';
class EditorRegistry {
EditorRegistry({
@@ -57,7 +57,7 @@ class EditorRegistry {
id: id,
path: path,
content: content,
editorConfig: resolveEditorConfig(workspaceRoot, path),
settings: resolveEditorSettings(workspaceRoot, path),
);
_buffers[id] = buf;
_pathToId[path] = id;
@@ -150,15 +150,16 @@ class EditorRegistry {
});
}
/// 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.
/// Persist [id] to disk. Applies the buffer's on-save settings (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. Saving a `.editorconfig` re-resolves the
/// settings of every open buffer (its rules just changed).
Future<bool> save(String id) async {
final buf = _buffers[id];
if (buf == null) return false;
final normalized = applyEditorConfigOnSave(buf.content, buf.editorConfig);
final normalized = buf.settings.applyOnSave(buf.content);
final changed = normalized != buf.content;
final absolute = _absolutePathOf(buf.path);
@@ -183,9 +184,29 @@ class EditorRegistry {
buf.dirty = false;
_emit('editor.saved', {'id': id, 'path': buf.path});
if (_isEditorConfigPath(buf.path)) _reresolveSettings();
return true;
}
/// Recompute every open buffer's effective settings from its sources and tell
/// the UI about the ones that changed. Called when a `.editorconfig` is saved
/// in-app (the file's rules changed under the open buffers).
void _reresolveSettings() {
for (final buf in _buffers.values) {
final next = resolveEditorSettings(workspaceRoot, buf.path);
if (next.toJson().toString() == buf.settings.toJson().toString()) continue;
buf.settings = next;
_emit('editor.settings-changed', {
'id': buf.id,
'path': buf.path,
'editorSettings': next.toJson(),
});
}
}
bool _isEditorConfigPath(String path) => path == '.editorconfig' || path.endsWith('/.editorconfig');
/// Close a buffer. Idempotent.
void close(String id) {
final buf = _buffers.remove(id);