lapce_editor_flutter 0.1.0 copy "lapce_editor_flutter: ^0.1.0" to clipboard
lapce_editor_flutter: ^0.1.0 copied to clipboard

Reusable Flutter code editor backed by the pure-Dart Lapce editor core.

example/lib/main.dart

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'),
      ),
    ],
  );
}
0
likes
150
points
160
downloads

Documentation

API reference

Publisher

verified publishermeshagent.com

Weekly Downloads

Reusable Flutter code editor backed by the pure-Dart Lapce editor core.

Repository (GitHub)
View/report issues

License

Apache-2.0 (license)

Dependencies

flutter, lapce_editor_core, tree_sitter_dart, web

More

Packages that depend on lapce_editor_flutter