lapce_editor_flutter 0.1.0
lapce_editor_flutter: ^0.1.0 copied to clipboard
Reusable Flutter code editor backed by the pure-Dart Lapce editor core.
import 'package:flutter/material.dart';
import 'package:lapce_editor_flutter/lapce_editor_flutter.dart';
void main() => runApp(const EditorExampleApp());
class EditorExampleApp extends StatelessWidget {
const EditorExampleApp({super.key, this.grammarLoader});
final TreeSitterGrammarLoader? grammarLoader;
@override
Widget build(BuildContext context) => MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Lapce Editor for Flutter',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xff7c5cff),
brightness: Brightness.dark,
),
scaffoldBackgroundColor: const Color(0xff121318),
useMaterial3: true,
),
home: EditorExamplePage(grammarLoader: grammarLoader),
);
}
class EditorExamplePage extends StatefulWidget {
const EditorExamplePage({super.key, this.grammarLoader});
final TreeSitterGrammarLoader? grammarLoader;
@override
State<EditorExamplePage> createState() => _EditorExamplePageState();
}
class _EditorExamplePageState extends State<EditorExamplePage> {
static const _sample = '''import 'package:flutter/material.dart';
void main() => runApp(const HelloLapce());
class HelloLapce extends StatelessWidget {
const HelloLapce({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(child: Text('Edit me! 👩💻')),
),
);
}
}
''';
late final LapceEditorController _controller;
late final TreeSitterSyntaxHighlighter _syntax;
final _focusNode = FocusNode();
bool _wrap = true;
bool _showGutter = true;
bool _showWhitespace = false;
bool _readOnly = false;
bool _autoBraces = true;
LapceLanguage _language = LapceLanguage.dart;
int _revision = 0;
int _characters = _sample.length;
@override
void initState() {
super.initState();
_controller = LapceEditorController(text: _sample);
_controller.addListener(_updated);
_syntax = TreeSitterSyntaxHighlighter(
controller: _controller,
grammarLoader: widget.grammarLoader,
);
}
@override
void dispose() {
_controller.removeListener(_updated);
_focusNode.dispose();
_syntax.dispose();
_controller.dispose();
super.dispose();
}
LapceEditorTheme get _editorTheme => LapceEditorTheme(
background: const Color(0xff181a21),
foreground: const Color(0xffe4e7f1),
caret: const Color(0xffbca9ff),
selection: const Color(0x667c5cff),
currentLine: const Color(0xff20232d),
placeholder: const Color(0xff73798b),
whitespace: const Color(0xff51586a),
indentGuide: const Color(0xff343947),
showIndentGuides: true,
renderWhitespace: _showWhitespace
? RenderWhitespace.all
: RenderWhitespace.none,
wrapMethod: _wrap
? const WrapMethod.editorWidth()
: const WrapMethod.none(),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
textStyle: const TextStyle(
fontFamily: 'SourceCodePro',
fontSize: 14,
height: 1.55,
),
);
void _updated() {
setState(() {
_revision = _controller.revision;
_characters = _controller.buffer.text.length;
});
}
void _setLanguage(LapceLanguage language) {
setState(() => _language = language);
_syntax.language = language;
_controller.buffer.indentStyle = IndentStyle.fromString(
language.indentUnit,
);
_focusNode.requestFocus();
}
String get _languageStatus {
if (_language == LapceLanguage.plainText) {
return 'Plain Text • highlighting disabled';
}
final error = _syntax.lastError;
if (error != null) {
return '${_language.displayName} • highlighting unavailable';
}
if (_syntax.isLoading) {
final phase = switch (_syntax.loadPhase) {
SyntaxHighlightLoadPhase.grammar => 'loading grammar',
SyntaxHighlightLoadPhase.parsing => 'parsing',
SyntaxHighlightLoadPhase.captures => 'applying highlights',
SyntaxHighlightLoadPhase.idle => 'ready',
};
return '${_language.displayName} • $phase…';
}
final runtime = switch (_syntax.lastExecutionResult?.workerRuntime) {
'wasm' => 'WasmGC worker',
'js' => 'JavaScript fallback',
_ => 'background worker',
};
return '${_language.displayName} • '
'Pure-Dart Tree-sitter • $runtime';
}
@override
Widget build(BuildContext context) => Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const _Header(),
const SizedBox(height: 16),
_Toolbar(
language: _language,
wrap: _wrap,
showGutter: _showGutter,
showWhitespace: _showWhitespace,
readOnly: _readOnly,
autoBraces: _autoBraces,
onWrapChanged: (value) => setState(() => _wrap = value),
onGutterChanged: (value) => setState(() => _showGutter = value),
onWhitespaceChanged: (value) =>
setState(() => _showWhitespace = value),
onReadOnlyChanged: (value) => setState(() => _readOnly = value),
onAutoBracesChanged: (value) {
setState(() => _autoBraces = value);
_controller.autoClosingMatchingPairs = value;
_focusNode.requestFocus();
},
onLanguageChanged: _setLanguage,
onReset: () {
_controller.setText(_sample);
_focusNode.requestFocus();
},
),
const SizedBox(height: 10),
Expanded(
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: const Color(0xff303541)),
borderRadius: BorderRadius.circular(10),
boxShadow: const [
BoxShadow(
color: Color(0x55000000),
blurRadius: 22,
offset: Offset(0, 10),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(9),
child: TextEditor(
controller: _controller,
focusNode: _focusNode,
autofocus: true,
theme: _editorTheme,
readOnly: _readOnly,
showGutter: _showGutter,
placeholder: 'Start writing code…',
syntaxHighlightProvider: _syntax,
),
),
),
),
const SizedBox(height: 10),
Row(
children: [
const Icon(Icons.info_outline, size: 16),
const SizedBox(width: 8),
Text('revision $_revision • $_characters UTF-16 code units'),
const SizedBox(width: 16),
Expanded(
child: ListenableBuilder(
listenable: _syntax,
builder: (context, child) => Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (_syntax.isLoading) ...const [
SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(strokeWidth: 1.5),
),
SizedBox(width: 8),
],
Flexible(
child: Text(
_languageStatus,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.end,
style: TextStyle(color: Color(0xff9ba2b5)),
),
),
],
),
),
),
],
),
],
),
),
),
);
}
class _Header extends StatelessWidget {
const _Header();
@override
Widget build(BuildContext context) => const Row(
children: [
DecoratedBox(
decoration: BoxDecoration(
color: Color(0xff7c5cff),
borderRadius: BorderRadius.all(Radius.circular(8)),
),
child: Padding(
padding: EdgeInsets.all(8),
child: Icon(Icons.code_rounded, color: Colors.white),
),
),
SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Lapce editor widget',
style: TextStyle(fontSize: 21, fontWeight: FontWeight.w700),
),
Text(
'Pure Dart engine + viewport-local Flutter renderer',
style: TextStyle(color: Color(0xff9ba2b5)),
),
],
),
],
);
}
class _Toolbar extends StatelessWidget {
const _Toolbar({
required this.language,
required this.wrap,
required this.showGutter,
required this.showWhitespace,
required this.readOnly,
required this.autoBraces,
required this.onWrapChanged,
required this.onGutterChanged,
required this.onWhitespaceChanged,
required this.onReadOnlyChanged,
required this.onAutoBracesChanged,
required this.onLanguageChanged,
required this.onReset,
});
final LapceLanguage language;
final bool wrap;
final bool showGutter;
final bool showWhitespace;
final bool readOnly;
final bool autoBraces;
final ValueChanged<bool> onWrapChanged;
final ValueChanged<bool> onGutterChanged;
final ValueChanged<bool> onWhitespaceChanged;
final ValueChanged<bool> onReadOnlyChanged;
final ValueChanged<bool> onAutoBracesChanged;
final ValueChanged<LapceLanguage> onLanguageChanged;
final VoidCallback onReset;
@override
Widget build(BuildContext context) => Wrap(
spacing: 8,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: const Color(0xff414754)),
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: DropdownButtonHideUnderline(
child: DropdownButton<LapceLanguage>(
key: const Key('language-dropdown'),
value: language,
menuMaxHeight: 360,
borderRadius: BorderRadius.circular(8),
icon: const Icon(Icons.expand_more, size: 20),
items: [
for (final item in LapceLanguage.languages.where(
(language) =>
language == LapceLanguage.plainText ||
TreeSitterSyntaxHighlighter.generatedLanguages.contains(
language,
),
))
DropdownMenuItem(value: item, child: Text(item.displayName)),
],
onChanged: (value) {
if (value != null) onLanguageChanged(value);
},
),
),
),
),
FilterChip(
label: const Text('Wrap'),
selected: wrap,
onSelected: onWrapChanged,
),
FilterChip(
label: const Text('Line numbers'),
selected: showGutter,
onSelected: onGutterChanged,
),
FilterChip(
label: const Text('Whitespace'),
selected: showWhitespace,
onSelected: onWhitespaceChanged,
),
FilterChip(
key: const Key('auto-braces-toggle'),
label: const Text('Auto braces'),
selected: autoBraces,
onSelected: onAutoBracesChanged,
),
FilterChip(
label: const Text('Read only'),
selected: readOnly,
onSelected: onReadOnlyChanged,
),
OutlinedButton.icon(
onPressed: onReset,
icon: const Icon(Icons.restart_alt, size: 18),
label: const Text('Reset'),
),
],
);
}