mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-10-06 15:13:18 +08:00
feat: 移除sftp文件编辑格式化
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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}',
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user