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:
@@ -6,7 +6,7 @@
|
||||
/// transitions.
|
||||
library;
|
||||
|
||||
import 'editorconfig.dart';
|
||||
import 'editor_settings.dart';
|
||||
|
||||
class Selection {
|
||||
const Selection({required this.start, required this.end});
|
||||
@@ -45,7 +45,7 @@ class EditorBuffer {
|
||||
required this.content,
|
||||
Selection? selection,
|
||||
this.dirty = false,
|
||||
this.editorConfig = EditorConfig.empty,
|
||||
this.settings = EditorSettings.empty,
|
||||
}) : selection = selection ?? const Selection.collapsed(0);
|
||||
|
||||
/// Stable daemon-local id (`b_1`, `b_2`, …).
|
||||
@@ -70,10 +70,11 @@ class EditorBuffer {
|
||||
/// `editor.save` (or `files.save` in future).
|
||||
bool dirty;
|
||||
|
||||
/// Resolved `.editorconfig` properties for this file (T-29). Drives the
|
||||
/// editor's indent/ruler rendering on the UI side and the save-time
|
||||
/// normalization daemon-side. [EditorConfig.empty] when nothing applies.
|
||||
EditorConfig editorConfig;
|
||||
/// Effective editor settings for this file (T-29), resolved from its sources
|
||||
/// (today: `.editorconfig`). Drives the editor's indent/ruler rendering on
|
||||
/// the UI side and the save-time normalization daemon-side.
|
||||
/// [EditorSettings.empty] when nothing applies.
|
||||
EditorSettings settings;
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'id': id,
|
||||
@@ -81,7 +82,7 @@ class EditorBuffer {
|
||||
'length': content.length,
|
||||
'selection': selection.toJson(),
|
||||
'dirty': dirty,
|
||||
'editorConfig': editorConfig.toJson(),
|
||||
'editorSettings': settings.toJson(),
|
||||
};
|
||||
|
||||
/// Full snapshot including [content] — for `editor.read` / tests /
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/// [EditorSettings] — the effective, source-agnostic editor configuration for
|
||||
/// one file (T-29).
|
||||
///
|
||||
/// The editor surface and the save path obey *this* object, never a particular
|
||||
/// source file. Today the only source is the project's `.editorconfig` (parsed
|
||||
/// in `editorconfig.dart`), but a settings panel or a clide-owned settings file
|
||||
/// can layer in later via [merge] without the editor, registry, or save path
|
||||
/// changing — that is the whole point of routing everything through one model.
|
||||
///
|
||||
/// Flutter-free (no `dart:io`, no `dart:ui`): it travels from the daemon to the
|
||||
/// UI as plain JSON and is unit-tested under `dart test`.
|
||||
library;
|
||||
|
||||
class EditorSettings {
|
||||
const EditorSettings({
|
||||
this.indentStyle,
|
||||
this.indentSize,
|
||||
this.tabWidth,
|
||||
this.endOfLine,
|
||||
this.maxLineLength,
|
||||
this.trimTrailingWhitespace,
|
||||
this.insertFinalNewline,
|
||||
});
|
||||
|
||||
/// `tab` or `space`.
|
||||
final String? indentStyle;
|
||||
|
||||
/// Columns per indent level.
|
||||
final int? indentSize;
|
||||
|
||||
/// Width of a tab character.
|
||||
final int? tabWidth;
|
||||
|
||||
/// `lf`, `crlf`, or `cr`.
|
||||
final String? endOfLine;
|
||||
|
||||
/// Ruler / wrap-guide column. Null when unset.
|
||||
final int? maxLineLength;
|
||||
|
||||
final bool? trimTrailingWhitespace;
|
||||
final bool? insertFinalNewline;
|
||||
|
||||
/// Nothing set — the editor keeps all of its built-in defaults.
|
||||
static const empty = EditorSettings();
|
||||
|
||||
bool get isEmpty =>
|
||||
indentStyle == null &&
|
||||
indentSize == null &&
|
||||
tabWidth == null &&
|
||||
endOfLine == null &&
|
||||
maxLineLength == null &&
|
||||
trimTrailingWhitespace == null &&
|
||||
insertFinalNewline == null;
|
||||
|
||||
/// The line terminator [endOfLine] names, or null when unset.
|
||||
String? get eolString => switch (endOfLine) {
|
||||
'lf' => '\n',
|
||||
'crlf' => '\r\n',
|
||||
'cr' => '\r',
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// The text one Tab press inserts, or null to keep the editor's default
|
||||
/// (Flutter's focus traversal) — the editor only takes over Tab when a source
|
||||
/// has an opinion about indentation.
|
||||
String? get indentUnit {
|
||||
if (indentStyle == 'tab') return '\t';
|
||||
if (indentStyle == 'space') return ' ' * (indentSize ?? 4);
|
||||
if (indentSize != null) return ' ' * indentSize!; // a size with no style → spaces
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Layer [other] on top: every field [other] sets overrides this one, fields
|
||||
/// it leaves null fall through. The composition order (lowest precedence
|
||||
/// first) lives in the resolver — a higher-precedence source (settings panel,
|
||||
/// clide settings file) merges over a lower one (.editorconfig).
|
||||
EditorSettings merge(EditorSettings other) => EditorSettings(
|
||||
indentStyle: other.indentStyle ?? indentStyle,
|
||||
indentSize: other.indentSize ?? indentSize,
|
||||
tabWidth: other.tabWidth ?? tabWidth,
|
||||
endOfLine: other.endOfLine ?? endOfLine,
|
||||
maxLineLength: other.maxLineLength ?? maxLineLength,
|
||||
trimTrailingWhitespace: other.trimTrailingWhitespace ?? trimTrailingWhitespace,
|
||||
insertFinalNewline: other.insertFinalNewline ?? insertFinalNewline,
|
||||
);
|
||||
|
||||
/// Only the set keys, for the IPC payload. Empty map when [isEmpty].
|
||||
Map<String, Object?> toJson() => {
|
||||
if (indentStyle != null) 'indent_style': indentStyle,
|
||||
if (indentSize != null) 'indent_size': indentSize,
|
||||
if (tabWidth != null) 'tab_width': tabWidth,
|
||||
if (endOfLine != null) 'end_of_line': endOfLine,
|
||||
if (maxLineLength != null) 'max_line_length': maxLineLength,
|
||||
if (trimTrailingWhitespace != null) 'trim_trailing_whitespace': trimTrailingWhitespace,
|
||||
if (insertFinalNewline != null) 'insert_final_newline': insertFinalNewline,
|
||||
};
|
||||
|
||||
factory EditorSettings.fromJson(Object? raw) {
|
||||
if (raw is! Map) return empty;
|
||||
return EditorSettings(
|
||||
indentStyle: raw['indent_style'] as String?,
|
||||
indentSize: (raw['indent_size'] as num?)?.toInt(),
|
||||
tabWidth: (raw['tab_width'] as num?)?.toInt(),
|
||||
endOfLine: raw['end_of_line'] as String?,
|
||||
maxLineLength: (raw['max_line_length'] as num?)?.toInt(),
|
||||
trimTrailingWhitespace: raw['trim_trailing_whitespace'] as bool?,
|
||||
insertFinalNewline: raw['insert_final_newline'] as bool?,
|
||||
);
|
||||
}
|
||||
|
||||
/// Apply the on-save text fixes these settings request: end-of-line
|
||||
/// normalization, trailing-whitespace trimming, and final-newline
|
||||
/// insertion/removal. Returns [content] unchanged where nothing is set.
|
||||
String applyOnSave(String content) {
|
||||
if (content.isEmpty) return content;
|
||||
var out = content;
|
||||
|
||||
// Trim trailing spaces/tabs before any line break or end-of-string. Done
|
||||
// first and EOL-agnostically so it composes with the EOL rewrite below.
|
||||
if (trimTrailingWhitespace == true) {
|
||||
out = out.replaceAll(RegExp(r'[ \t]+(?=\r\n|\r|\n|$)'), '');
|
||||
}
|
||||
|
||||
final eol = eolString;
|
||||
if (eol != null) {
|
||||
out = out.replaceAll(RegExp(r'\r\n|\r|\n'), eol);
|
||||
}
|
||||
|
||||
if (insertFinalNewline == true) {
|
||||
if (out.isNotEmpty && !out.endsWith('\n') && !out.endsWith('\r')) {
|
||||
out += eol ?? _detectEol(out) ?? '\n';
|
||||
}
|
||||
} else if (insertFinalNewline == false) {
|
||||
out = out.replaceAll(RegExp(r'(\r\n|\r|\n)+$'), '');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static String? _detectEol(String s) => RegExp(r'\r\n|\r|\n').firstMatch(s)?.group(0);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/// Composes the effective [EditorSettings] for a file from its sources (T-29).
|
||||
///
|
||||
/// This is the single seam the editor stack calls — [EditorRegistry] resolves a
|
||||
/// buffer's settings here on load and whenever a source changes. Today the only
|
||||
/// source is the project's `.editorconfig`; a workspace settings file or a
|
||||
/// settings-panel override layers in by adding another `.merge(...)` below, in
|
||||
/// increasing-precedence order. The editor and save path never learn the
|
||||
/// source — they only see the merged result.
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'editor_settings.dart';
|
||||
import 'editorconfig.dart';
|
||||
|
||||
/// The merged settings for [relPath] (workspace-relative, `/`-separated).
|
||||
EditorSettings resolveEditorSettings(Directory workspaceRoot, String relPath) {
|
||||
// Lowest precedence first; later sources override earlier ones.
|
||||
var settings = EditorSettings.empty;
|
||||
settings = settings.merge(readEditorConfig(workspaceRoot, relPath));
|
||||
// Future sources slot in here, e.g.:
|
||||
// settings = settings.merge(readWorkspaceSettingsFile(workspaceRoot, relPath));
|
||||
// settings = settings.merge(settingsPanelOverrides(relPath));
|
||||
return settings;
|
||||
}
|
||||
@@ -1,142 +1,33 @@
|
||||
/// EditorConfig support (T-29) — read `.editorconfig` from the workspace and
|
||||
/// resolve the properties that apply to a given file.
|
||||
/// `.editorconfig` as an [EditorSettings] source (T-29).
|
||||
///
|
||||
/// Reads `.editorconfig` files from the workspace and resolves the properties
|
||||
/// that apply to a given file into the source-agnostic [EditorSettings] model.
|
||||
/// This is one *source* feeding `editor_settings_resolver.dart`; it is not the
|
||||
/// thing the editor obeys directly.
|
||||
///
|
||||
/// We parse the INI-ish format and match section globs ourselves (no
|
||||
/// dependency, per the prefer-zero-deps rule). Resolution walks from the file's
|
||||
/// dependency, per prefer-zero-deps). Resolution walks from the file's
|
||||
/// directory up to the workspace root, honouring `root = true` to stop the
|
||||
/// ascent, with nearer files and later sections winning on conflict — the
|
||||
/// precedence the EditorConfig spec defines.
|
||||
///
|
||||
/// Flutter-free by construction: this runs daemon-side under `dart test`
|
||||
/// alongside [EditorRegistry], so it imports only `dart:io` + core.
|
||||
/// alongside [EditorRegistry], so it imports only `dart:io` + the model.
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
/// The resolved, typed EditorConfig properties for one file. A `null` field
|
||||
/// means "no opinion" — the editor keeps its default and changes nothing.
|
||||
class EditorConfig {
|
||||
const EditorConfig({
|
||||
this.indentStyle,
|
||||
this.indentSize,
|
||||
this.tabWidth,
|
||||
this.endOfLine,
|
||||
this.maxLineLength,
|
||||
this.trimTrailingWhitespace,
|
||||
this.insertFinalNewline,
|
||||
});
|
||||
import 'editor_settings.dart';
|
||||
|
||||
/// `tab` or `space`.
|
||||
final String? indentStyle;
|
||||
|
||||
/// Columns per indent level. Follows [tabWidth] when the file says
|
||||
/// `indent_size = tab`.
|
||||
final int? indentSize;
|
||||
|
||||
/// Width of a tab character.
|
||||
final int? tabWidth;
|
||||
|
||||
/// `lf`, `crlf`, or `cr`.
|
||||
final String? endOfLine;
|
||||
|
||||
/// Ruler / wrap-guide column. Null when unset or `off`.
|
||||
final int? maxLineLength;
|
||||
|
||||
final bool? trimTrailingWhitespace;
|
||||
final bool? insertFinalNewline;
|
||||
|
||||
static const empty = EditorConfig();
|
||||
|
||||
bool get isEmpty =>
|
||||
indentStyle == null &&
|
||||
indentSize == null &&
|
||||
tabWidth == null &&
|
||||
endOfLine == null &&
|
||||
maxLineLength == null &&
|
||||
trimTrailingWhitespace == null &&
|
||||
insertFinalNewline == null;
|
||||
|
||||
/// The line terminator [endOfLine] names, or null when unset.
|
||||
String? get eolString => switch (endOfLine) {
|
||||
'lf' => '\n',
|
||||
'crlf' => '\r\n',
|
||||
'cr' => '\r',
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// Only the set keys, for the IPC buffer payload. Empty map when [isEmpty].
|
||||
Map<String, Object?> toJson() => {
|
||||
if (indentStyle != null) 'indent_style': indentStyle,
|
||||
if (indentSize != null) 'indent_size': indentSize,
|
||||
if (tabWidth != null) 'tab_width': tabWidth,
|
||||
if (endOfLine != null) 'end_of_line': endOfLine,
|
||||
if (maxLineLength != null) 'max_line_length': maxLineLength,
|
||||
if (trimTrailingWhitespace != null) 'trim_trailing_whitespace': trimTrailingWhitespace,
|
||||
if (insertFinalNewline != null) 'insert_final_newline': insertFinalNewline,
|
||||
};
|
||||
|
||||
/// Build from a merged raw property map (keys already lowercased). Applies
|
||||
/// the spec's `indent_size`/`tab_width` cross-defaulting. A property whose
|
||||
/// value is `unset` (or unparseable for its type) resolves to null.
|
||||
factory EditorConfig.fromProps(Map<String, String> p) {
|
||||
String? lc(String k) {
|
||||
final v = p[k];
|
||||
if (v == null) return null;
|
||||
final t = v.trim().toLowerCase();
|
||||
return t == 'unset' ? null : t;
|
||||
}
|
||||
|
||||
final indentStyle = _oneOf(lc('indent_style'), const {'tab', 'space'});
|
||||
int? tabWidth = _posInt(lc('tab_width'));
|
||||
|
||||
final rawIndent = lc('indent_size');
|
||||
int? indentSize;
|
||||
if (rawIndent == 'tab') {
|
||||
indentSize = tabWidth;
|
||||
} else {
|
||||
indentSize = _posInt(rawIndent);
|
||||
}
|
||||
// tab_width defaults to indent_size; indent_size (for tabs) defaults to
|
||||
// tab_width — the reciprocal defaulting from the spec.
|
||||
tabWidth ??= indentSize;
|
||||
if (indentStyle == 'tab') indentSize ??= tabWidth;
|
||||
|
||||
final maxRaw = lc('max_line_length');
|
||||
final maxLineLength = (maxRaw == 'off') ? null : _posInt(maxRaw);
|
||||
|
||||
return EditorConfig(
|
||||
indentStyle: indentStyle,
|
||||
indentSize: indentSize,
|
||||
tabWidth: tabWidth,
|
||||
endOfLine: _oneOf(lc('end_of_line'), const {'lf', 'crlf', 'cr'}),
|
||||
maxLineLength: maxLineLength,
|
||||
trimTrailingWhitespace: _bool(lc('trim_trailing_whitespace')),
|
||||
insertFinalNewline: _bool(lc('insert_final_newline')),
|
||||
);
|
||||
}
|
||||
|
||||
static String? _oneOf(String? v, Set<String> allowed) => (v != null && allowed.contains(v)) ? v : null;
|
||||
static int? _posInt(String? v) {
|
||||
if (v == null) return null;
|
||||
final n = int.tryParse(v);
|
||||
return (n != null && n > 0) ? n : null;
|
||||
}
|
||||
|
||||
static bool? _bool(String? v) => switch (v) {
|
||||
'true' => true,
|
||||
'false' => false,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Resolve the EditorConfig for [relPath] (a workspace-relative, `/`-separated
|
||||
/// path) against the `.editorconfig` files under [workspaceRoot].
|
||||
/// Resolve the EditorConfig-sourced [EditorSettings] for [relPath] (a
|
||||
/// workspace-relative, `/`-separated path) against the `.editorconfig` files
|
||||
/// under [workspaceRoot].
|
||||
///
|
||||
/// Walks from the file's directory up to (and including) the workspace root,
|
||||
/// stopping once a file declares `root = true`. Never throws — an unreadable or
|
||||
/// malformed file is skipped, so a broken `.editorconfig` can't wedge a file
|
||||
/// open or save.
|
||||
EditorConfig resolveEditorConfig(Directory workspaceRoot, String relPath) {
|
||||
EditorSettings readEditorConfig(Directory workspaceRoot, String relPath) {
|
||||
final rel = relPath.replaceAll('\\', '/').replaceAll(RegExp(r'^/+'), '');
|
||||
final segs = rel.split('/');
|
||||
final dirSegs = segs.sublist(0, segs.length - 1);
|
||||
@@ -170,42 +61,63 @@ EditorConfig resolveEditorConfig(Directory workspaceRoot, String relPath) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return EditorConfig.fromProps(props);
|
||||
return editorSettingsFromProps(props);
|
||||
}
|
||||
|
||||
/// Apply the on-save text fixes [cfg] requests: end-of-line normalization,
|
||||
/// trailing-whitespace trimming, and final-newline insertion/removal. Returns
|
||||
/// the content unchanged where [cfg] has no opinion.
|
||||
String applyEditorConfigOnSave(String content, EditorConfig cfg) {
|
||||
if (content.isEmpty) return content;
|
||||
var out = content;
|
||||
|
||||
// Trim trailing spaces/tabs before any line break or end-of-string. Done
|
||||
// first and EOL-agnostically so it composes with the EOL rewrite below.
|
||||
if (cfg.trimTrailingWhitespace == true) {
|
||||
out = out.replaceAll(RegExp(r'[ \t]+(?=\r\n|\r|\n|$)'), '');
|
||||
/// Build [EditorSettings] from a merged raw EditorConfig property map (keys
|
||||
/// already lowercased). Applies the spec's `indent_size`/`tab_width`
|
||||
/// cross-defaulting; a property valued `unset` (or unparseable for its type)
|
||||
/// resolves to null. Public for direct unit testing of the mapping.
|
||||
EditorSettings editorSettingsFromProps(Map<String, String> p) {
|
||||
String? lc(String k) {
|
||||
final v = p[k];
|
||||
if (v == null) return null;
|
||||
final t = v.trim().toLowerCase();
|
||||
return t == 'unset' ? null : t;
|
||||
}
|
||||
|
||||
final eol = cfg.eolString;
|
||||
if (eol != null) {
|
||||
out = out.replaceAll(RegExp(r'\r\n|\r|\n'), eol);
|
||||
}
|
||||
final indentStyle = _oneOf(lc('indent_style'), const {'tab', 'space'});
|
||||
int? tabWidth = _posInt(lc('tab_width'));
|
||||
|
||||
if (cfg.insertFinalNewline == true) {
|
||||
if (out.isNotEmpty && !out.endsWith('\n') && !out.endsWith('\r')) {
|
||||
out += eol ?? _detectEol(out) ?? '\n';
|
||||
}
|
||||
} else if (cfg.insertFinalNewline == false) {
|
||||
out = out.replaceAll(RegExp(r'(\r\n|\r|\n)+$'), '');
|
||||
final rawIndent = lc('indent_size');
|
||||
int? indentSize;
|
||||
if (rawIndent == 'tab') {
|
||||
indentSize = tabWidth;
|
||||
} else {
|
||||
indentSize = _posInt(rawIndent);
|
||||
}
|
||||
return out;
|
||||
// tab_width defaults to indent_size; indent_size (for tabs) defaults to
|
||||
// tab_width — the reciprocal defaulting from the spec.
|
||||
tabWidth ??= indentSize;
|
||||
if (indentStyle == 'tab') indentSize ??= tabWidth;
|
||||
|
||||
final maxRaw = lc('max_line_length');
|
||||
final maxLineLength = (maxRaw == 'off') ? null : _posInt(maxRaw);
|
||||
|
||||
return EditorSettings(
|
||||
indentStyle: indentStyle,
|
||||
indentSize: indentSize,
|
||||
tabWidth: tabWidth,
|
||||
endOfLine: _oneOf(lc('end_of_line'), const {'lf', 'crlf', 'cr'}),
|
||||
maxLineLength: maxLineLength,
|
||||
trimTrailingWhitespace: _bool(lc('trim_trailing_whitespace')),
|
||||
insertFinalNewline: _bool(lc('insert_final_newline')),
|
||||
);
|
||||
}
|
||||
|
||||
String? _detectEol(String s) {
|
||||
final m = RegExp(r'\r\n|\r|\n').firstMatch(s);
|
||||
return m?.group(0);
|
||||
String? _oneOf(String? v, Set<String> allowed) => (v != null && allowed.contains(v)) ? v : null;
|
||||
int? _posInt(String? v) {
|
||||
if (v == null) return null;
|
||||
final n = int.tryParse(v);
|
||||
return (n != null && n > 0) ? n : null;
|
||||
}
|
||||
|
||||
bool? _bool(String? v) => switch (v) {
|
||||
'true' => true,
|
||||
'false' => false,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// INI parsing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user