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>
190 lines
8.0 KiB
Dart
190 lines
8.0 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:clide/src/editor/editorconfig.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
void main() {
|
|
group('editorSettingsFromProps', () {
|
|
test('maps the common indent + newline keys', () {
|
|
final c = editorSettingsFromProps({
|
|
'indent_style': 'space',
|
|
'indent_size': '2',
|
|
'end_of_line': 'lf',
|
|
'trim_trailing_whitespace': 'true',
|
|
'insert_final_newline': 'true',
|
|
'max_line_length': '100',
|
|
});
|
|
expect(c.indentStyle, 'space');
|
|
expect(c.indentSize, 2);
|
|
expect(c.tabWidth, 2); // defaults to indent_size
|
|
expect(c.endOfLine, 'lf');
|
|
expect(c.trimTrailingWhitespace, isTrue);
|
|
expect(c.insertFinalNewline, isTrue);
|
|
expect(c.maxLineLength, 100);
|
|
});
|
|
|
|
test('indent_size = tab follows tab_width', () {
|
|
final c = editorSettingsFromProps({'indent_style': 'tab', 'indent_size': 'tab', 'tab_width': '4'});
|
|
expect(c.indentSize, 4);
|
|
expect(c.tabWidth, 4);
|
|
});
|
|
|
|
test('tab indent with no indent_size defaults to tab_width', () {
|
|
expect(editorSettingsFromProps({'indent_style': 'tab', 'tab_width': '8'}).indentSize, 8);
|
|
});
|
|
|
|
test('max_line_length = off and unset values resolve to null', () {
|
|
final c = editorSettingsFromProps({'max_line_length': 'off', 'indent_style': 'unset', 'insert_final_newline': 'unset'});
|
|
expect(c.maxLineLength, isNull);
|
|
expect(c.indentStyle, isNull);
|
|
expect(c.insertFinalNewline, isNull);
|
|
});
|
|
|
|
test('rejects junk values rather than guessing', () {
|
|
final c = editorSettingsFromProps({'indent_style': 'tabs', 'indent_size': '-3', 'end_of_line': 'mac'});
|
|
expect(c.indentStyle, isNull);
|
|
expect(c.indentSize, isNull);
|
|
expect(c.endOfLine, isNull);
|
|
});
|
|
});
|
|
|
|
group('readEditorConfig — glob matching', () {
|
|
late Directory root;
|
|
setUp(() async => root = await Directory.systemTemp.createTemp('clide-ec-glob-'));
|
|
tearDown(() => root.deleteSync(recursive: true));
|
|
|
|
Future<void> write(String relPath, String contents) async {
|
|
final f = File('${root.path}/$relPath');
|
|
await f.parent.create(recursive: true);
|
|
await f.writeAsString(contents);
|
|
}
|
|
|
|
test('[*] applies to every file', () async {
|
|
await write('.editorconfig', 'root = true\n[*]\nindent_size = 2\n');
|
|
expect(readEditorConfig(root, 'a.txt').indentSize, 2);
|
|
expect(readEditorConfig(root, 'sub/b.dart').indentSize, 2);
|
|
});
|
|
|
|
test('extension globs and brace alternation', () async {
|
|
await write('.editorconfig', 'root = true\n[*.dart]\nindent_size = 2\n[*.{js,ts}]\nindent_size = 4\n');
|
|
expect(readEditorConfig(root, 'main.dart').indentSize, 2);
|
|
expect(readEditorConfig(root, 'app.js').indentSize, 4);
|
|
expect(readEditorConfig(root, 'app.ts').indentSize, 4);
|
|
expect(readEditorConfig(root, 'readme.md').indentSize, isNull);
|
|
});
|
|
|
|
test('a slash anchors the glob to the config dir; ** spans dirs', () async {
|
|
await write('.editorconfig', 'root = true\n[lib/**.dart]\nindent_size = 3\n');
|
|
expect(readEditorConfig(root, 'lib/a.dart').indentSize, 3);
|
|
expect(readEditorConfig(root, 'lib/deep/b.dart').indentSize, 3);
|
|
expect(readEditorConfig(root, 'test/c.dart').indentSize, isNull);
|
|
});
|
|
|
|
test('? matches exactly one non-separator char', () async {
|
|
await write('.editorconfig', 'root = true\n[?.txt]\nindent_size = 9\n');
|
|
expect(readEditorConfig(root, 'a.txt').indentSize, 9);
|
|
expect(readEditorConfig(root, 'ab.txt').indentSize, isNull);
|
|
});
|
|
|
|
test('leading **/ matches in the config dir and below', () async {
|
|
await write('.editorconfig', 'root = true\n[**/foo.txt]\nindent_size = 3\n');
|
|
expect(readEditorConfig(root, 'foo.txt').indentSize, 3);
|
|
expect(readEditorConfig(root, 'a/b/foo.txt').indentSize, 3);
|
|
});
|
|
|
|
test('negated character class', () async {
|
|
await write('.editorconfig', 'root = true\n[[!a-c].txt]\nindent_size = 4\n');
|
|
expect(readEditorConfig(root, 'z.txt').indentSize, 4);
|
|
expect(readEditorConfig(root, 'b.txt').indentSize, isNull);
|
|
});
|
|
|
|
test('numeric range', () async {
|
|
await write('.editorconfig', 'root = true\n[file{1..3}.txt]\nindent_size = 7\n');
|
|
expect(readEditorConfig(root, 'file2.txt').indentSize, 7);
|
|
expect(readEditorConfig(root, 'file9.txt').indentSize, isNull);
|
|
});
|
|
|
|
test('an oversized numeric range falls back to a generic integer match', () async {
|
|
await write('.editorconfig', 'root = true\n[v{1..5000}]\nindent_size = 6\n');
|
|
expect(readEditorConfig(root, 'v123').indentSize, 6);
|
|
expect(readEditorConfig(root, 'vx').indentSize, isNull);
|
|
});
|
|
|
|
test('a brace with no top-level comma and an unterminated class are literal', () async {
|
|
await write('.editorconfig', 'root = true\n[a{b}.txt]\nindent_size = 2\n[lit[.md]\nindent_size = 8\n');
|
|
expect(readEditorConfig(root, 'a{b}.txt').indentSize, 2);
|
|
expect(readEditorConfig(root, 'lit[.md').indentSize, 8);
|
|
});
|
|
|
|
test('backslash escapes a glob metacharacter to a literal', () async {
|
|
await write('.editorconfig', 'root = true\n[a\\{b.txt]\nindent_size = 5\n');
|
|
expect(readEditorConfig(root, 'a{b.txt').indentSize, 5);
|
|
expect(readEditorConfig(root, 'aXb.txt').indentSize, isNull);
|
|
});
|
|
|
|
test('an unterminated brace (even with an escaped }) is literal', () async {
|
|
await write('.editorconfig', 'root = true\n[x{a\\}b.txt]\nindent_size = 6\n');
|
|
expect(readEditorConfig(root, 'x{a}b.txt').indentSize, 6);
|
|
});
|
|
|
|
test('escaped chars inside a class and inside an alternation', () async {
|
|
await write('.editorconfig', 'root = true\n[[a\\-c].txt]\nindent_size = 3\n[{a\\,b,c}.md]\nindent_size = 4\n');
|
|
expect(readEditorConfig(root, 'c.txt').indentSize, 3);
|
|
expect(readEditorConfig(root, 'c.md').indentSize, 4);
|
|
});
|
|
});
|
|
|
|
group('readEditorConfig — precedence', () {
|
|
late Directory root;
|
|
setUp(() async => root = await Directory.systemTemp.createTemp('clide-ec-prec-'));
|
|
tearDown(() => root.deleteSync(recursive: true));
|
|
|
|
Future<void> write(String relPath, String contents) async {
|
|
final f = File('${root.path}/$relPath');
|
|
await f.parent.create(recursive: true);
|
|
await f.writeAsString(contents);
|
|
}
|
|
|
|
test('a nearer config overrides a farther one', () async {
|
|
await write('.editorconfig', 'root = true\n[*]\nindent_size = 2\nindent_style = space\n');
|
|
await write('sub/.editorconfig', '[*]\nindent_size = 4\n');
|
|
final c = readEditorConfig(root, 'sub/x.dart');
|
|
expect(c.indentSize, 4);
|
|
expect(c.indentStyle, 'space');
|
|
});
|
|
|
|
test('root = true halts the ascent', () async {
|
|
await write('.editorconfig', '[*]\nindent_style = tab\n');
|
|
await write('sub/.editorconfig', 'root = true\n[*]\nindent_size = 4\n');
|
|
final c = readEditorConfig(root, 'sub/x.dart');
|
|
expect(c.indentSize, 4);
|
|
expect(c.indentStyle, isNull);
|
|
});
|
|
|
|
test('later sections in one file win', () async {
|
|
await write('.editorconfig', 'root = true\n[*]\nindent_size = 2\n[*.dart]\nindent_size = 4\n');
|
|
expect(readEditorConfig(root, 'a.dart').indentSize, 4);
|
|
});
|
|
|
|
test('a nearer "unset" clears an inherited property', () async {
|
|
await write('.editorconfig', 'root = true\n[*]\nindent_style = space\n');
|
|
await write('sub/.editorconfig', '[*]\nindent_style = unset\n');
|
|
expect(readEditorConfig(root, 'sub/x.txt').indentStyle, isNull);
|
|
});
|
|
|
|
test('no .editorconfig anywhere resolves to empty', () async {
|
|
expect(readEditorConfig(root, 'a.txt').isEmpty, isTrue);
|
|
});
|
|
|
|
test('a malformed file is skipped, not fatal', () async {
|
|
await write('.editorconfig', 'root = true\n[*\nindent_size = 2\nnonsense line\n[*]\nindent_size = 8\n');
|
|
expect(readEditorConfig(root, 'a.txt').indentSize, 8);
|
|
});
|
|
|
|
test('comments (# and ;) are ignored', () async {
|
|
await write('.editorconfig', '# top comment\nroot = true\n[*]\n; inline note\nindent_size = 2\n');
|
|
expect(readEditorConfig(root, 'a.txt').indentSize, 2);
|
|
});
|
|
});
|
|
}
|