Code editor in formulaeditor

This commit is contained in:
Álvaro González 2026-03-21 13:54:24 +01:00
parent 02fd849484
commit e49f6c3079
6 changed files with 98 additions and 190 deletions

View file

@ -9,6 +9,7 @@
- `flutter pub get` --> `./flutterw pub get` - `flutter pub get` --> `./flutterw pub get`
- `flutter run -d linux` --> `./flutterw run -d linux` - `flutter run -d linux` --> `./flutterw run -d linux`
- See `./Makefile` for more examples. - See `./Makefile` for more examples.
- If you are an agent, you may be also containerized. Try `distrobox-host-exec $(pwd)/flutterw`
# MANDATORY WORKFLOW # MANDATORY WORKFLOW

View file

@ -1,43 +1,45 @@
all: build-container clean-container build-builders build-linux-debug-container all: build-container clean-container build-builders build-linux-debug-container
DB=~/.local/share/com.example.d4rt_formulas/d4rt_formulas/formulas.sqlite DATABASEFILE=~/.local/share/com.example.d4rt_formulas/d4rt_formulas/formulas.sqlite
FLUTTERW := $(shell if [ "$$CONTAINER_ID" = "" ]; then echo "./flutterw"; else echo "distrobox-host-exec $(CURDIR)/flutterw"; fi)
build-container: build-container:
./flutterw --build-container $(FLUTTERW) --build-container
clean: clean:
flutter clean flutter clean
[ -f $(DB) ] && rm $(DB) [ -f $(DATABASEFILE) ] && rm $(DATABASEFILE)
clean-container: clean-container:
rm -r .build-container-cache rm -r .build-container-cache
./flutterw clean $(FLUTTERW) clean
pub-get-container: pub-get-container:
./flutterw pub get $(FLUTTERW) pub get
test: test:
./flutterw test $(FLUTTERW) test
build-builders: build-builders:
./flutterw pub run build_runner build --delete-conflicting-outputs $(FLUTTERW) pub run build_runner build --delete-conflicting-outputs
build-android-release-container: build-android-release-container:
./flutterw build apk --release $(FLUTTERW) build apk --release
build-linux-debug-container: build-linux-debug-container:
./flutterw build linux --debug $(FLUTTERW) build linux --debug
build-web-debug-container: build-web-debug-container:
./flutterw build web --debug $(FLUTTERW) build web --debug
run-linux-debug-container: run-linux-debug-container:
./flutterw run -d linux $(FLUTTERW) run -d linux
run-web-debug-container: run-web-debug-container:
./flutterw run --web-port $${WEB_PORT:-8081} -d web-server $(FLUTTERW) run --web-port $${WEB_PORT:-8081} -d web-server
run-linux-debug-native: run-linux-debug-native:
flutter run -d linux flutter run -d linux
@ -50,4 +52,4 @@ ai:
run-emulator: run-emulator:
flutter emulators --launch Medium_Phone flutter emulators --launch Medium_Phone
flutter run -d emulator-5554 flutter run -d emulator-5554

View file

@ -79,5 +79,6 @@
- It will show a screen with a text editor with dart syntax and a button "paste". - It will show a screen with a text editor with dart syntax and a button "paste".
- The "paste" button will copy the clipboard into the text editor. - The "paste" button will copy the clipboard into the text editor.
- A second button "import" will use the import preview screen - A second button "import" will use the import preview screen
- Make formulaSolver() asyncronous, and show a CircularProgressIndicator while the formula is being solved. Honor a new optinal parameter "timeout" in formulaSolver, that will throw a TimeoutException.
- [ ] Add a uuid column to the table or FormulaElements, so it is not necessary to load all the formulas to find a formula by uuid. This will improve performance when updating and deleting. - [ ] Add a uuid column to the table or FormulaElements, so it is not necessary to load all the formulas to find a formula by uuid. This will improve performance when updating and deleting.
- [ ] Make formulaSolver() asyncronous, and show a CircularProgressIndicator while the formula is being solved. Honor a new optinal parameter "timeout" in formulaSolver, that will throw a TimeoutException.
- [ ] When importing FormulaElements, save the FormulaElements in the database (currently, they are only added to the Corpus in memory).

View file

@ -9,6 +9,9 @@ import '../database/database_service.dart';
import '../service_locator.dart'; import '../service_locator.dart';
import 'formula_screen.dart'; import 'formula_screen.dart';
import 'unit_dropdown.dart'; import 'unit_dropdown.dart';
import 'package:flutter_code_editor/flutter_code_editor.dart';
import 'package:flutter_highlight/themes/monokai-sublime.dart';
import 'package:highlight/languages/dart.dart';
/// A screen for editing a Formula's properties including name, description, /// A screen for editing a Formula's properties including name, description,
/// input/output variables, and d4rt code. /// input/output variables, and d4rt code.
@ -17,12 +20,7 @@ class FormulaEditor extends StatefulWidget {
final Corpus corpus; final Corpus corpus;
final Function(Formula)? onSave; final Function(Formula)? onSave;
const FormulaEditor({ const FormulaEditor({super.key, required this.formula, required this.corpus, this.onSave});
super.key,
required this.formula,
required this.corpus,
this.onSave,
});
@override @override
State<FormulaEditor> createState() => _FormulaEditorState(); State<FormulaEditor> createState() => _FormulaEditorState();
@ -32,14 +30,14 @@ class _FormulaEditorState extends State<FormulaEditor> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
late TextEditingController _nameController; late TextEditingController _nameController;
late TextEditingController _descriptionController; late TextEditingController _descriptionController;
late TextEditingController _d4rtCodeController; late CodeController _d4rtCodeController;
// Track input variables // Track input variables
final List<_InputVariableRowData> _inputVariables = []; final List<_InputVariableRowData> _inputVariables = [];
// Output variable // Output variable
late _OutputVariableRowData _outputVariable; late _OutputVariableRowData _outputVariable;
bool _isPreviewVisible = false; bool _isPreviewVisible = false;
@override @override
@ -47,17 +45,19 @@ class _FormulaEditorState extends State<FormulaEditor> {
super.initState(); super.initState();
_nameController = TextEditingController(text: widget.formula.name); _nameController = TextEditingController(text: widget.formula.name);
_descriptionController = TextEditingController(text: widget.formula.description ?? ''); _descriptionController = TextEditingController(text: widget.formula.description ?? '');
_d4rtCodeController = TextEditingController(text: widget.formula.d4rtCode); _d4rtCodeController = CodeController(language: dart, text: widget.formula.d4rtCode ?? '');
// Initialize input variables // Initialize input variables
for (final input in widget.formula.input) { for (final input in widget.formula.input) {
_inputVariables.add(_InputVariableRowData( _inputVariables.add(
nameController: TextEditingController(text: input.name), _InputVariableRowData(
unit: input.unit, nameController: TextEditingController(text: input.name),
values: input.values != null ? List.from(input.values!) : null, unit: input.unit,
)); values: input.values != null ? List.from(input.values!) : null,
),
);
} }
// Initialize output variable // Initialize output variable
_outputVariable = _OutputVariableRowData( _outputVariable = _OutputVariableRowData(
nameController: TextEditingController(text: widget.formula.output.name), nameController: TextEditingController(text: widget.formula.output.name),
@ -79,11 +79,13 @@ class _FormulaEditorState extends State<FormulaEditor> {
void _addInputVariable() { void _addInputVariable() {
setState(() { setState(() {
_inputVariables.add(_InputVariableRowData( _inputVariables.add(
nameController: TextEditingController(text: 'var${_inputVariables.length + 1}'), _InputVariableRowData(
unit: null, nameController: TextEditingController(text: 'var${_inputVariables.length + 1}'),
values: null, unit: null,
)); values: null,
),
);
}); });
} }
@ -117,10 +119,7 @@ class _FormulaEditorState extends State<FormulaEditor> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => FormulaScreen( builder: (context) => FormulaScreen(formula: formula, corpus: widget.corpus),
formula: formula,
corpus: widget.corpus,
),
), ),
); );
} }
@ -159,17 +158,12 @@ class _FormulaEditorState extends State<FormulaEditor> {
try { try {
final input = <VariableSpec>[]; final input = <VariableSpec>[];
for (final variable in _inputVariables) { for (final variable in _inputVariables) {
input.add(VariableSpec( input.add(
name: variable.nameController.text.trim(), VariableSpec(name: variable.nameController.text.trim(), unit: variable.unit, values: variable.values),
unit: variable.unit, );
values: variable.values,
));
} }
final output = VariableSpec( final output = VariableSpec(name: _outputVariable.nameController.text.trim(), unit: _outputVariable.unit);
name: _outputVariable.nameController.text.trim(),
unit: _outputVariable.unit,
);
return Formula( return Formula(
uuid: widget.formula.uuid, uuid: widget.formula.uuid,
@ -177,7 +171,7 @@ class _FormulaEditorState extends State<FormulaEditor> {
description: _descriptionController.text.isEmpty ? null : _descriptionController.text, description: _descriptionController.text.isEmpty ? null : _descriptionController.text,
input: input, input: input,
output: output, output: output,
d4rtCode: _d4rtCodeController.text, d4rtCode: _d4rtCodeController.fullText,
tags: widget.formula.tags, // Preserve existing tags tags: widget.formula.tags, // Preserve existing tags
); );
} catch (e) { } catch (e) {
@ -296,21 +290,9 @@ class _FormulaEditorState extends State<FormulaEditor> {
appBar: AppBar( appBar: AppBar(
title: const Text('Edit Formula'), title: const Text('Edit Formula'),
actions: [ actions: [
IconButton( IconButton(icon: const Icon(Icons.play_arrow), onPressed: _testFormula, tooltip: 'Test Formula'),
icon: const Icon(Icons.play_arrow), IconButton(icon: const Icon(Icons.copy), onPressed: _saveFormulaAsCopy, tooltip: 'Save as copy'),
onPressed: _testFormula, IconButton(icon: const Icon(Icons.save), onPressed: _saveFormula, tooltip: 'Save'),
tooltip: 'Test Formula',
),
IconButton(
icon: const Icon(Icons.copy),
onPressed: _saveFormulaAsCopy,
tooltip: 'Save as copy',
),
IconButton(
icon: const Icon(Icons.save),
onPressed: _saveFormula,
tooltip: 'Save',
),
], ],
), ),
body: Form( body: Form(
@ -363,13 +345,7 @@ class _FormulaEditorState extends State<FormulaEditor> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Text( const Text('Description (Markdown)', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
'Description (Markdown)',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -400,13 +376,8 @@ class _FormulaEditorState extends State<FormulaEditor> {
child: Markdown( child: Markdown(
data: _descriptionController.text, data: _descriptionController.text,
shrinkWrap: true, shrinkWrap: true,
builders: { builders: {'latex': LatexElementBuilder()},
'latex': LatexElementBuilder(), extensionSet: markdown.ExtensionSet([LatexBlockSyntax()], [LatexInlineSyntax()]),
},
extensionSet: markdown.ExtensionSet(
[LatexBlockSyntax()],
[LatexInlineSyntax()],
),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@ -432,13 +403,7 @@ class _FormulaEditorState extends State<FormulaEditor> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( const Text('Input Variables', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
'Input Variables',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8), const SizedBox(height: 8),
..._inputVariables.asMap().entries.map((entry) { ..._inputVariables.asMap().entries.map((entry) {
final index = entry.key; final index = entry.key;
@ -470,10 +435,7 @@ class _FormulaEditorState extends State<FormulaEditor> {
flex: 2, flex: 2,
child: TextFormField( child: TextFormField(
controller: variable.nameController, controller: variable.nameController,
decoration: const InputDecoration( decoration: const InputDecoration(labelText: 'Name', border: OutlineInputBorder()),
labelText: 'Name',
border: OutlineInputBorder(),
),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@ -530,7 +492,8 @@ class _FormulaEditorState extends State<FormulaEditor> {
final unitSpec = widget.corpus.getUnit(unit); final unitSpec = widget.corpus.getUnit(unit);
return DropdownMenuItem<String?>( return DropdownMenuItem<String?>(
value: unit, value: unit,
child: Text('${unitSpec.symbol} - ${unit}', child: Text(
'${unitSpec.symbol} - ${unit}',
style: const TextStyle(fontSize: 14), style: const TextStyle(fontSize: 14),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -567,13 +530,7 @@ class _FormulaEditorState extends State<FormulaEditor> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( const Text('Output Variable', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
'Output Variable',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8), const SizedBox(height: 8),
Row( Row(
children: [ children: [
@ -581,10 +538,7 @@ class _FormulaEditorState extends State<FormulaEditor> {
flex: 2, flex: 2,
child: TextFormField( child: TextFormField(
controller: _outputVariable.nameController, controller: _outputVariable.nameController,
decoration: const InputDecoration( decoration: const InputDecoration(labelText: 'Name', border: OutlineInputBorder()),
labelText: 'Name',
border: OutlineInputBorder(),
),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@ -641,7 +595,8 @@ class _FormulaEditorState extends State<FormulaEditor> {
final unitSpec = widget.corpus.getUnit(unit); final unitSpec = widget.corpus.getUnit(unit);
return DropdownMenuItem<String?>( return DropdownMenuItem<String?>(
value: unit, value: unit,
child: Text('${unitSpec.symbol} - ${unit}', child: Text(
'${unitSpec.symbol} - ${unit}',
style: const TextStyle(fontSize: 14), style: const TextStyle(fontSize: 14),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -667,67 +622,18 @@ class _FormulaEditorState extends State<FormulaEditor> {
Widget _buildD4rtCodeSection() { Widget _buildD4rtCodeSection() {
return Card( return Card(
child: Padding( child: Expanded(
padding: const EdgeInsets.all(16.0), child: Padding(
child: Column( padding: const EdgeInsets.all(16.0),
crossAxisAlignment: CrossAxisAlignment.start, child: CodeTheme(
children: [ data: CodeThemeData(styles: monokaiSublimeTheme),
const Text( child: Padding(
'D4RT Code', padding: const EdgeInsets.all(8.0),
style: TextStyle( child: SingleChildScrollView(
fontSize: 16, child: CodeField(controller: _d4rtCodeController),
fontWeight: FontWeight.bold,
), ),
), ),
const SizedBox(height: 8), ),
Container(
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).dividerColor),
borderRadius: BorderRadius.circular(4),
),
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(4),
topRight: Radius.circular(4),
),
),
child: Row(
children: [
Icon(Icons.code, size: 16, color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 8),
Text(
'Dart Syntax',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
),
TextFormField(
controller: _d4rtCodeController,
decoration: const InputDecoration(
hintText: 'Enter D4RT/Dart code here',
border: InputBorder.none,
contentPadding: EdgeInsets.all(12),
),
maxLines: 10,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 14,
),
),
],
),
),
],
), ),
), ),
); );
@ -763,19 +669,12 @@ class _InputVariableRowData {
String? unit; String? unit;
List<dynamic>? values; List<dynamic>? values;
_InputVariableRowData({ _InputVariableRowData({required this.nameController, this.unit, this.values});
required this.nameController,
this.unit,
this.values,
});
} }
class _OutputVariableRowData { class _OutputVariableRowData {
final TextEditingController nameController; final TextEditingController nameController;
String? unit; String? unit;
_OutputVariableRowData({ _OutputVariableRowData({required this.nameController, this.unit});
required this.nameController,
this.unit,
});
} }

View file

@ -14,7 +14,6 @@ extension CorpusDatabaseExtension on FormulasDatabase {
for (final element in elements) { for (final element in elements) {
try { try {
final parsed = SetUtils.parseCorpusElements('[${element.elementText}]'); final parsed = SetUtils.parseCorpusElements('[${element.elementText}]');
print("PARSED:$element");
parsedElements.addAll(parsed); parsedElements.addAll(parsed);
} catch (e) { } catch (e) {
print('Error parsing database element: $e'); print('Error parsing database element: $e');

View file

@ -37,7 +37,6 @@ abstract class SetUtils {
} }
/// Escapes special characters in a string for use in D4RT literals /// Escapes special characters in a string for use in D4RT literals
@deprecated
static String escapeD4rtString(String input) { static String escapeD4rtString(String input) {
return input return input
.replaceAll(r'\\', r'\\\\') // escape backslashes first .replaceAll(r'\\', r'\\\\') // escape backslashes first
@ -75,9 +74,9 @@ abstract class SetUtils {
/// Uses JSON-like formatting but for Dart language, with proper indentation. /// Uses JSON-like formatting but for Dart language, with proper indentation.
static String prettyPrint(dynamic value, {int indent = 0}) { static String prettyPrint(dynamic value, {int indent = 0}) {
if (value is String) { if (value is String) {
return _prettyPrintString(value, indent); return _prettyPrintString(value);
} else if (value is num) { } else if (value is num) {
return _prettyPrintNumber(value, indent); return _prettyPrintNumber(value);
} else if (value is Set) { } else if (value is Set) {
return _prettyPrintSet(value, indent); return _prettyPrintSet(value, indent);
} else if (value is List) { } else if (value is List) {
@ -90,15 +89,15 @@ abstract class SetUtils {
} }
/// Pretty prints a simple string, escaping special characters if needed. /// Pretty prints a simple string, escaping special characters if needed.
static String _prettyPrintString(String s, int indent) { static String _prettyPrintString(String s) {
// Check if the string needs raw string formatting (newlines, $, backslashes, quotes) // Check if the string needs raw string formatting (newlines, $, backslashes, quotes)
final needsRawString = s.contains('\n') || final needsRawString = s.contains('\n') ||
s.contains(r'$') || s.contains(r'$') ||
s.contains(r'\\') || s.contains(r'\\') ||
s.contains('"'); s.contains('"');
if (needsRawString) { if (needsRawString && s != '"' ) {
return _prettyPrintRawString(s, indent); return _prettyPrintRawString(s);
} }
// Simple string with escaped quotes // Simple string with escaped quotes
@ -107,7 +106,7 @@ abstract class SetUtils {
} }
/// Pretty prints a number. /// Pretty prints a number.
static String _prettyPrintNumber(num n, int indent) { static String _prettyPrintNumber(num n) {
return n.toString(); return n.toString();
} }
@ -157,9 +156,16 @@ abstract class SetUtils {
/// Pretty prints a raw string (for strings containing newlines, $, backslashes, etc.) /// Pretty prints a raw string (for strings containing newlines, $, backslashes, etc.)
/// Uses Dart's raw string syntax r"""...""" /// Uses Dart's raw string syntax r"""..."""
static String _prettyPrintRawString(String s, int indent) { static String _prettyPrintRawString(String s) {
// Escape triple quotes by replacing """ with ""\" if( s == '"'){
final escaped = s.replaceAll('"""', r'""\\"'); return "'\"";
return 'r"""$escaped"""'; }
if( s.contains('"""') && s.contains("'''") ){
return escapeD4rtString(s);
}
if( s.contains('"""') ){
return "r'''$s'''";
}
return 'r"""$s"""';
} }
} }