feat: 移除sftp文件编辑格式化

This commit is contained in:
chaos-zhu
2026-05-23 22:41:42 +08:00
parent 7d013d6eb2
commit b84a436917
12 changed files with 7 additions and 261 deletions
@@ -1,99 +0,0 @@
import 'dart:convert';
import 'package:xml/xml.dart';
import 'package:yaml/yaml.dart';
String formatJson(String src) {
try {
final decoded = jsonDecode(src);
return const JsonEncoder.withIndent(' ').convert(decoded);
} on FormatException {
rethrow;
}
}
String formatYaml(String src) {
try {
final decoded = loadYaml(src);
final buffer = StringBuffer();
_dumpYaml(decoded, buffer, 0);
final output = buffer.toString();
return output.endsWith('\n') ? output : '$output\n';
} on YamlException catch (err) {
throw FormatException(err.message);
}
}
String formatXml(String src) {
try {
final doc = XmlDocument.parse(src);
return doc.toXmlString(pretty: true, indent: ' ');
} on XmlException catch (err) {
throw FormatException(err.message);
}
}
void _dumpYaml(dynamic node, StringBuffer buf, int indent) {
final pad = ' ' * indent;
if (node is YamlMap || node is Map) {
final map = node is YamlMap
? node.nodes.map((k, v) => MapEntry(k.toString(), v.value))
: (node as Map);
if (map.isEmpty) {
buf.write('{}\n');
return;
}
var first = true;
for (final entry in map.entries) {
if (!first || indent > 0) buf.write(pad);
first = false;
buf.write('${_yamlKey(entry.key.toString())}:');
final v = entry.value;
if (v is YamlMap || v is Map || v is YamlList || v is List) {
buf.write('\n');
_dumpYaml(v, buf, indent + 1);
} else {
buf.write(' ${_yamlScalar(v)}\n');
}
}
} else if (node is YamlList || node is List) {
final list = node is YamlList ? node.toList() : (node as List);
if (list.isEmpty) {
buf.write('$pad[]\n');
return;
}
for (final item in list) {
buf.write('$pad- ');
if (item is YamlMap || item is Map || item is YamlList || item is List) {
buf.write('\n');
_dumpYaml(item, buf, indent + 1);
} else {
buf.write('${_yamlScalar(item)}\n');
}
}
} else {
buf.write('$pad${_yamlScalar(node)}\n');
}
}
String _yamlKey(String key) {
if (RegExp(r'^[A-Za-z_][\w\-]*$').hasMatch(key)) return key;
return _yamlQuote(key);
}
String _yamlScalar(dynamic value) {
if (value == null) return 'null';
if (value is bool) return value.toString();
if (value is num) return value.toString();
final s = value.toString();
if (s.isEmpty) return '""';
if (RegExp(r'^(true|false|null|~|\d|-)').hasMatch(s) || s.contains(': ') || s.contains('#')) {
return _yamlQuote(s);
}
return s;
}
String _yamlQuote(String s) {
final escaped = s.replaceAll(r'\', r'\\').replaceAll('"', r'\"');
return '"$escaped"';
}
@@ -18,20 +18,17 @@ class EditorLanguage {
const EditorLanguage({
required this.id,
required this.highlightMode,
required this.formatSupported,
required this.defaultIndent,
});
final String id;
final Mode? highlightMode;
final bool formatSupported;
final int defaultIndent;
}
const _plainText = EditorLanguage(
id: 'Plain Text',
highlightMode: null,
formatSupported: false,
defaultIndent: 2,
);
@@ -75,7 +72,6 @@ EditorLanguage detectFromFileName(String name) {
return EditorLanguage(
id: 'JSON',
highlightMode: langJson,
formatSupported: true,
defaultIndent: 2,
);
case 'yaml':
@@ -83,7 +79,6 @@ EditorLanguage detectFromFileName(String name) {
return EditorLanguage(
id: 'YAML',
highlightMode: langYaml,
formatSupported: true,
defaultIndent: 2,
);
case 'xml':
@@ -93,7 +88,6 @@ EditorLanguage detectFromFileName(String name) {
return EditorLanguage(
id: 'XML',
highlightMode: langXml,
formatSupported: ext == 'xml',
defaultIndent: 2,
);
case 'ts':
@@ -101,7 +95,6 @@ EditorLanguage detectFromFileName(String name) {
return EditorLanguage(
id: 'TypeScript',
highlightMode: langTypescript,
formatSupported: false,
defaultIndent: 2,
);
case 'js':
@@ -111,7 +104,6 @@ EditorLanguage detectFromFileName(String name) {
return EditorLanguage(
id: 'JavaScript',
highlightMode: langJavascript,
formatSupported: false,
defaultIndent: 2,
);
case 'sh':
@@ -120,35 +112,30 @@ EditorLanguage detectFromFileName(String name) {
return EditorLanguage(
id: 'Bash',
highlightMode: langBash,
formatSupported: false,
defaultIndent: 2,
);
case 'py':
return EditorLanguage(
id: 'Python',
highlightMode: langPython,
formatSupported: false,
defaultIndent: 4,
);
case 'go':
return EditorLanguage(
id: 'Go',
highlightMode: langGo,
formatSupported: false,
defaultIndent: 2,
);
case 'sql':
return EditorLanguage(
id: 'SQL',
highlightMode: langSql,
formatSupported: false,
defaultIndent: 2,
);
case 'dart':
return EditorLanguage(
id: 'Dart',
highlightMode: langDart,
formatSupported: false,
defaultIndent: 2,
);
case 'md':
@@ -156,7 +143,6 @@ EditorLanguage detectFromFileName(String name) {
return EditorLanguage(
id: 'Markdown',
highlightMode: langMarkdown,
formatSupported: false,
defaultIndent: 2,
);
case 'ini':
@@ -166,21 +152,18 @@ EditorLanguage detectFromFileName(String name) {
return EditorLanguage(
id: 'INI',
highlightMode: langIni,
formatSupported: false,
defaultIndent: 2,
);
case 'dockerfile':
return EditorLanguage(
id: 'Dockerfile',
highlightMode: langDockerfile,
formatSupported: false,
defaultIndent: 2,
);
case 'nginx':
return EditorLanguage(
id: 'Nginx',
highlightMode: langNginx,
formatSupported: false,
defaultIndent: 2,
);
default:
@@ -1,7 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:re_editor/re_editor.dart';
import 'editor_formatters.dart';
import 'editor_language.dart';
abstract class TextEditorWriter {
@@ -33,7 +32,6 @@ class TextEditorController extends ChangeNotifier {
bool get isDirty => code.text != _originalText;
bool get saving => _saving;
bool get canFormat => language.formatSupported;
void _onCodeChanged() {
notifyListeners();
@@ -55,28 +53,6 @@ class TextEditorController extends ChangeNotifier {
}
}
void format() {
if (!language.formatSupported) return;
final src = code.text;
final String formatted;
switch (language.id) {
case 'JSON':
formatted = formatJson(src);
break;
case 'YAML':
formatted = formatYaml(src);
break;
case 'XML':
formatted = formatXml(src);
break;
default:
return;
}
if (formatted != src) {
code.text = formatted;
}
}
@override
void dispose() {
code.removeListener(_onCodeChanged);
@@ -456,12 +456,6 @@ class _TextEditorPageState extends ConsumerState<TextEditorPage> {
),
),
const Spacer(),
TextButton.icon(
icon: const Icon(Icons.auto_fix_high, size: 16),
label: Text(l.tr('editor.format')),
onPressed: controller.canFormat ? () => _onFormat(context, controller) : null,
),
const SizedBox(width: 8),
FilledButton.icon(
icon: controller.saving
? const SizedBox(
@@ -495,26 +489,6 @@ class _TextEditorPageState extends ConsumerState<TextEditorPage> {
}
}
void _onFormat(BuildContext context, TextEditorController controller) {
final l = AppLocalizations.of(context);
if (!controller.canFormat) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(l.tr('editor.formatUnsupported'))));
return;
}
try {
controller.format();
} on FormatException catch (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l.trf('editor.formatFailed', [error.message]))),
);
} catch (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l.trf('editor.formatFailed', [error.toString()]))),
);
}
}
Future<void> _openSettings(BuildContext context, _EditorPalette palette) {
return showModalBottomSheet<void>(
context: context,
-3
View File
@@ -210,7 +210,6 @@ const Map<String, String> stringsEn = {
'editor.saveFailed': 'Save failed: {0}',
'editor.saved': 'Saved',
'editor.unsaved': 'Unsaved',
'editor.format': 'Format',
'editor.save': 'Save',
'editor.undo': 'Undo',
'editor.redo': 'Redo',
@@ -220,8 +219,6 @@ const Map<String, String> stringsEn = {
'editor.discardLeave': 'Discard',
'editor.discardSaveAndLeave': 'Save & leave',
'editor.malformedUtf8': 'File contains non-UTF-8 bytes; saving may lose some characters.',
'editor.formatUnsupported': 'Format not supported for this language.',
'editor.formatFailed': 'Format failed: {0}',
'editor.statusEncoding': 'UTF-8 · LF · {0}',
'editor.statusPosition': 'Ln {0}, Col {1}',
'editor.statusLineCount': '{0} / {1}',
-3
View File
@@ -199,7 +199,6 @@ const Map<String, String> stringsZh = {
'editor.saveFailed': '保存失败:{0}',
'editor.saved': '已保存',
'editor.unsaved': '未保存',
'editor.format': '格式化',
'editor.save': '保存',
'editor.undo': '撤销',
'editor.redo': '重做',
@@ -209,8 +208,6 @@ const Map<String, String> stringsZh = {
'editor.discardLeave': '放弃',
'editor.discardSaveAndLeave': '保存并退出',
'editor.malformedUtf8': '文件含非 UTF-8 字节,保存可能丢失部分字符',
'editor.formatUnsupported': '当前语言不支持格式化',
'editor.formatFailed': '格式化失败:{0}',
'editor.statusEncoding': 'UTF-8 · LF · {0}',
'editor.statusPosition': 'Ln {0}, Col {1}',
'editor.statusLineCount': '{0} / {1}',
@@ -7,12 +7,14 @@ import Foundation
import file_picker
import flutter_secure_storage_macos
import path_provider_foundation
import share_plus
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
}
+1 -9
View File
@@ -843,7 +843,7 @@ packages:
source: hosted
version: "1.1.0"
xml:
dependency: "direct main"
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
@@ -858,14 +858,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.0"
yaml:
dependency: "direct main"
description:
name: yaml
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
zmodem:
dependency: transitive
description:
-2
View File
@@ -30,8 +30,6 @@ dependencies:
share_plus: ^12.0.2
re_editor: ^0.8.0
re_highlight: ^0.0.3
yaml: ^3.1.2
xml: ^6.5.0
dev_dependencies:
flutter_test:
@@ -1,41 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile/features/shell/editor/editor_formatters.dart';
void main() {
group('formatJson', () {
test('pretty-prints with 2-space indent', () {
final out = formatJson('{"a":1,"b":[1,2,3]}');
expect(out, contains('\n "a": 1'));
expect(out, contains(' 1'));
});
test('throws FormatException on invalid JSON', () {
expect(() => formatJson('{not json'), throwsA(isA<FormatException>()));
});
});
group('formatYaml', () {
test('round-trips a simple map with 2-space indent', () {
final out = formatYaml('foo: 1\nbar:\n baz: hello');
expect(out, contains('foo: 1'));
expect(out, contains('bar:'));
expect(out, contains(' baz: hello'));
});
test('throws FormatException on invalid YAML', () {
expect(() => formatYaml(': : : not yaml'), throwsA(isA<FormatException>()));
});
});
group('formatXml', () {
test('pretty-prints valid xml with 2-space indent', () {
final out = formatXml('<a><b>x</b></a>');
expect(out, contains('<a>'));
expect(out, contains(' <b>x</b>'));
});
test('throws FormatException on invalid XML', () {
expect(() => formatXml('<a><b></a>'), throwsA(isA<FormatException>()));
});
});
}
@@ -3,10 +3,9 @@ import 'package:mobile/features/shell/editor/editor_language.dart';
void main() {
group('detectFromFileName', () {
test('maps .json to JSON with formatter and highlight', () {
test('maps .json to JSON with highlight', () {
final lang = detectFromFileName('config.json');
expect(lang.id, 'JSON');
expect(lang.formatSupported, isTrue);
expect(lang.highlightMode, isNotNull);
expect(lang.defaultIndent, 2);
});
@@ -14,32 +13,26 @@ void main() {
test('maps .yaml and .yml to YAML', () {
expect(detectFromFileName('a.yaml').id, 'YAML');
expect(detectFromFileName('a.yml').id, 'YAML');
expect(detectFromFileName('a.yaml').formatSupported, isTrue);
});
test('maps .xml to XML', () {
final lang = detectFromFileName('pom.xml');
expect(lang.id, 'XML');
expect(lang.formatSupported, isTrue);
expect(detectFromFileName('pom.xml').id, 'XML');
});
test('maps .ts to TypeScript without formatter support', () {
test('maps .ts to TypeScript', () {
final lang = detectFromFileName('app.ts');
expect(lang.id, 'TypeScript');
expect(lang.formatSupported, isFalse);
expect(lang.highlightMode, isNotNull);
});
test('maps .sh to Bash without formatter support', () {
test('maps .sh to Bash', () {
final lang = detectFromFileName('deploy.sh');
expect(lang.id, 'Bash');
expect(lang.formatSupported, isFalse);
});
test('unknown extension falls back to plaintext with no highlight', () {
final lang = detectFromFileName('notes.unknownext');
expect(lang.id, 'Plain Text');
expect(lang.formatSupported, isFalse);
expect(lang.highlightMode, isNull);
});
@@ -63,31 +63,5 @@ void main() {
expect(controller.isDirty, isTrue);
controller.dispose();
});
test('format applies JSON formatter when supported', () {
final controller = TextEditorController(
writer: _FakeWriter(),
remotePath: '/etc/app.json',
originalText: '{"a":1,"b":2}',
language: detectFromFileName('app.json'),
totalBytes: 14,
);
controller.code.text = '{"a":1,"b":2}';
controller.format();
expect(controller.code.text, contains('\n "a": 1'));
controller.dispose();
});
test('canFormat is false for plaintext', () {
final controller = TextEditorController(
writer: _FakeWriter(),
remotePath: '/etc/notes',
originalText: 'plain',
language: detectFromFileName('notes'),
totalBytes: 5,
);
expect(controller.canFormat, isFalse);
controller.dispose();
});
});
}