dissolve app/ into repo root (D-056)

Single Flutter package at the repo root. All code, tests, assets,
and platform directories moved from app/ to root. Package renamed
from clide_app to clide — all imports rewritten. Merged pubspec
combines core (ffi) and app (flutter, yaml, xterm) dependencies.
Makefile simplified: no APP_PRESENT conditionals, no cd, no daemon
lifecycle. 317 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 00:37:20 +02:00
co-authored by Claude Opus 4.6
parent a526c5b9b7
commit 46329700d5
394 changed files with 978 additions and 1090 deletions
+118
View File
@@ -0,0 +1,118 @@
/// Maps file extensions and special filenames to grammar asset names.
library;
String? grammarForPath(String path) {
final name = path.split('/').last;
final special = _filenameMap[name];
if (special != null) return special;
final dot = name.lastIndexOf('.');
if (dot < 0) return null;
final ext = name.substring(dot).toLowerCase();
return _extMap[ext];
}
const _filenameMap = <String, String>{
'Makefile': 'make',
'makefile': 'make',
'GNUmakefile': 'make',
'Dockerfile': 'dockerfile',
'dockerfile': 'dockerfile',
'.gitignore': 'gitignore',
'.gitconfig': 'git-config',
'.gitmodules': 'git-config',
'justfile': 'just',
'Justfile': 'just',
};
const _extMap = <String, String>{
// Systems
'.c': 'c',
'.h': 'c',
'.cpp': 'cpp',
'.cxx': 'cpp',
'.cc': 'cpp',
'.hpp': 'cpp',
'.hxx': 'cpp',
'.cs': 'c-sharp',
// Application
'.dart': 'dart',
'.go': 'go',
'.rs': 'rust',
'.java': 'java',
'.kt': 'kotlin',
'.kts': 'kotlin',
'.swift': 'swift',
'.rb': 'ruby',
'.py': 'python',
'.pyw': 'python',
'.ex': 'elixir',
'.exs': 'elixir',
'.erl': 'erlang',
'.hrl': 'erlang',
'.hs': 'haskell',
'.lhs': 'haskell',
'.jl': 'julia',
'.r': 'r',
'.R': 'r',
'.zig': 'zig',
'.nix': 'nix',
'.lua': 'lua',
'.php': 'php',
'.nkl': 'nickel',
// Web
'.js': 'javascript',
'.mjs': 'javascript',
'.cjs': 'javascript',
'.jsx': 'javascript',
'.ts': 'typescript',
'.tsx': 'typescript',
'.html': 'html',
'.htm': 'html',
'.css': 'css',
'.svelte': 'svelte',
'.vue': 'vue',
// Data / Config
'.json': 'json',
'.yaml': 'yaml',
'.yml': 'yaml',
'.toml': 'toml',
'.xml': 'xml',
'.svg': 'xml',
'.plist': 'xml',
'.hcl': 'hcl',
'.tf': 'hcl',
'.tfvars': 'hcl',
'.proto': 'proto',
'.regex': 'regex',
// Markdown
'.md': 'markdown',
'.mdx': 'markdown',
'.markdown': 'markdown',
// Shell
'.sh': 'bash',
'.bash': 'bash',
'.zsh': 'bash',
'.fish': 'bash',
// Game dev
'.gd': 'gdscript',
'.glsl': 'glsl',
'.vert': 'glsl',
'.frag': 'glsl',
'.hlsl': 'hlsl',
'.wgsl': 'wgsl',
// Data / Query
'.sql': 'sqlite',
'.sqlite': 'sqlite',
// Other
'.diff': 'diff',
'.patch': 'diff',
};
+256
View File
@@ -0,0 +1,256 @@
library;
import 'dart:ffi';
import 'dart:io' show File, Platform;
import 'package:ffi/ffi.dart';
// -- Opaque handles ----------------------------------------------------------
final class TSParser extends Opaque {}
final class TSTree extends Opaque {}
final class TSQuery extends Opaque {}
final class TSQueryCursor extends Opaque {}
final class TSWasmStore extends Opaque {}
final class TSWasmEngine extends Opaque {}
// -- Structs -----------------------------------------------------------------
final class TSNode extends Struct {
@Array(4)
external Array<Uint32> context;
external Pointer<Void> id;
external Pointer<Void> tree;
}
final class TSQueryCapture extends Struct {
external TSNode node;
@Uint32()
external int index;
}
final class TSQueryMatch extends Struct {
@Uint32()
external int id;
@Uint16()
external int patternIndex;
@Uint16()
external int captureCount;
external Pointer<TSQueryCapture> captures;
}
final class TSWasmError extends Struct {
@Int32()
external int kind;
external Pointer<Utf8> message;
}
// -- Native function typedefs ------------------------------------------------
// Parser
typedef _TsParserNew = Pointer<TSParser> Function();
typedef _TsParserDelete = Void Function(Pointer<TSParser>);
typedef _TsParserSetLanguage = Bool Function(Pointer<TSParser>, Pointer<Void>);
typedef _TsParserSetWasmStore = Void Function(
Pointer<TSParser>, Pointer<TSWasmStore>);
typedef _TsParserParseString = Pointer<TSTree> Function(
Pointer<TSParser>, Pointer<TSTree>, Pointer<Utf8>, Uint32);
// Tree
typedef _TsTreeDelete = Void Function(Pointer<TSTree>);
typedef _TsTreeRootNode = TSNode Function(Pointer<TSTree>);
// Node
typedef _TsNodeStartByte = Uint32 Function(TSNode);
typedef _TsNodeEndByte = Uint32 Function(TSNode);
// Query
typedef _TsQueryNew = Pointer<TSQuery> Function(
Pointer<Void>, Pointer<Utf8>, Uint32, Pointer<Uint32>, Pointer<Int32>);
typedef _TsQueryDelete = Void Function(Pointer<TSQuery>);
typedef _TsQueryCaptureCount = Uint32 Function(Pointer<TSQuery>);
typedef _TsQueryCaptureNameForId = Pointer<Utf8> Function(
Pointer<TSQuery>, Uint32, Pointer<Uint32>);
// Query cursor
typedef _TsQueryCursorNew = Pointer<TSQueryCursor> Function();
typedef _TsQueryCursorDelete = Void Function(Pointer<TSQueryCursor>);
typedef _TsQueryCursorExec = Void Function(
Pointer<TSQueryCursor>, Pointer<TSQuery>, TSNode);
typedef _TsQueryCursorNextMatch = Bool Function(
Pointer<TSQueryCursor>, Pointer<TSQueryMatch>);
// WASM store
typedef _TsWasmStoreNew = Pointer<TSWasmStore> Function(
Pointer<TSWasmEngine>, Pointer<TSWasmError>);
typedef _TsWasmStoreDelete = Void Function(Pointer<TSWasmStore>);
typedef _TsWasmStoreLoadLanguage = Pointer<Void> Function(
Pointer<TSWasmStore>, Pointer<Utf8>, Pointer<Uint8>, Uint32,
Pointer<TSWasmError>);
// WASM engine (from wasmtime C API, re-exported by tree-sitter)
typedef _WasmEngineNew = Pointer<TSWasmEngine> Function();
typedef _WasmEngineDelete = Void Function(Pointer<TSWasmEngine>);
// -- Dart function typedefs --------------------------------------------------
typedef DTsParserNew = Pointer<TSParser> Function();
typedef DTsParserDelete = void Function(Pointer<TSParser>);
typedef DTsParserSetLanguage = bool Function(Pointer<TSParser>, Pointer<Void>);
typedef DTsParserSetWasmStore = void Function(
Pointer<TSParser>, Pointer<TSWasmStore>);
typedef DTsParserParseString = Pointer<TSTree> Function(
Pointer<TSParser>, Pointer<TSTree>, Pointer<Utf8>, int);
typedef DTsTreeDelete = void Function(Pointer<TSTree>);
typedef DTsTreeRootNode = TSNode Function(Pointer<TSTree>);
typedef DTsNodeStartByte = int Function(TSNode);
typedef DTsNodeEndByte = int Function(TSNode);
typedef DTsQueryNew = Pointer<TSQuery> Function(
Pointer<Void>, Pointer<Utf8>, int, Pointer<Uint32>, Pointer<Int32>);
typedef DTsQueryDelete = void Function(Pointer<TSQuery>);
typedef DTsQueryCaptureCount = int Function(Pointer<TSQuery>);
typedef DTsQueryCaptureNameForId = Pointer<Utf8> Function(
Pointer<TSQuery>, int, Pointer<Uint32>);
typedef DTsQueryCursorNew = Pointer<TSQueryCursor> Function();
typedef DTsQueryCursorDelete = void Function(Pointer<TSQueryCursor>);
typedef DTsQueryCursorExec = void Function(
Pointer<TSQueryCursor>, Pointer<TSQuery>, TSNode);
typedef DTsQueryCursorNextMatch = bool Function(
Pointer<TSQueryCursor>, Pointer<TSQueryMatch>);
typedef DTsWasmStoreNew = Pointer<TSWasmStore> Function(
Pointer<TSWasmEngine>, Pointer<TSWasmError>);
typedef DTsWasmStoreDelete = void Function(Pointer<TSWasmStore>);
typedef DTsWasmStoreLoadLanguage = Pointer<Void> Function(
Pointer<TSWasmStore>, Pointer<Utf8>, Pointer<Uint8>, int,
Pointer<TSWasmError>);
typedef DWasmEngineNew = Pointer<TSWasmEngine> Function();
typedef DWasmEngineDelete = void Function(Pointer<TSWasmEngine>);
// -- Bindings ----------------------------------------------------------------
class TreeSitterLib {
TreeSitterLib._(DynamicLibrary lib)
: parserNew = lib.lookupFunction<_TsParserNew, DTsParserNew>(
'ts_parser_new'),
parserDelete = lib.lookupFunction<_TsParserDelete, DTsParserDelete>(
'ts_parser_delete'),
parserSetLanguage =
lib.lookupFunction<_TsParserSetLanguage, DTsParserSetLanguage>(
'ts_parser_set_language'),
parserSetWasmStore =
lib.lookupFunction<_TsParserSetWasmStore, DTsParserSetWasmStore>(
'ts_parser_set_wasm_store'),
parserParseString =
lib.lookupFunction<_TsParserParseString, DTsParserParseString>(
'ts_parser_parse_string'),
treeDelete = lib.lookupFunction<_TsTreeDelete, DTsTreeDelete>(
'ts_tree_delete'),
treeRootNode = lib.lookupFunction<_TsTreeRootNode, DTsTreeRootNode>(
'ts_tree_root_node'),
nodeStartByte = lib.lookupFunction<_TsNodeStartByte, DTsNodeStartByte>(
'ts_node_start_byte'),
nodeEndByte = lib.lookupFunction<_TsNodeEndByte, DTsNodeEndByte>(
'ts_node_end_byte'),
queryNew =
lib.lookupFunction<_TsQueryNew, DTsQueryNew>('ts_query_new'),
queryDelete = lib.lookupFunction<_TsQueryDelete, DTsQueryDelete>(
'ts_query_delete'),
queryCaptureCount =
lib.lookupFunction<_TsQueryCaptureCount, DTsQueryCaptureCount>(
'ts_query_capture_count'),
queryCaptureNameForId = lib.lookupFunction<_TsQueryCaptureNameForId,
DTsQueryCaptureNameForId>('ts_query_capture_name_for_id'),
queryCursorNew =
lib.lookupFunction<_TsQueryCursorNew, DTsQueryCursorNew>(
'ts_query_cursor_new'),
queryCursorDelete =
lib.lookupFunction<_TsQueryCursorDelete, DTsQueryCursorDelete>(
'ts_query_cursor_delete'),
queryCursorExec =
lib.lookupFunction<_TsQueryCursorExec, DTsQueryCursorExec>(
'ts_query_cursor_exec'),
queryCursorNextMatch =
lib.lookupFunction<_TsQueryCursorNextMatch, DTsQueryCursorNextMatch>(
'ts_query_cursor_next_match'),
wasmStoreNew = lib.lookupFunction<_TsWasmStoreNew, DTsWasmStoreNew>(
'ts_wasm_store_new'),
wasmStoreDelete =
lib.lookupFunction<_TsWasmStoreDelete, DTsWasmStoreDelete>(
'ts_wasm_store_delete'),
wasmStoreLoadLanguage = lib.lookupFunction<_TsWasmStoreLoadLanguage,
DTsWasmStoreLoadLanguage>('ts_wasm_store_load_language'),
wasmEngineNew = lib.lookupFunction<_WasmEngineNew, DWasmEngineNew>(
'wasm_engine_new'),
wasmEngineDelete =
lib.lookupFunction<_WasmEngineDelete, DWasmEngineDelete>(
'wasm_engine_delete');
final DTsParserNew parserNew;
final DTsParserDelete parserDelete;
final DTsParserSetLanguage parserSetLanguage;
final DTsParserSetWasmStore parserSetWasmStore;
final DTsParserParseString parserParseString;
final DTsTreeDelete treeDelete;
final DTsTreeRootNode treeRootNode;
final DTsNodeStartByte nodeStartByte;
final DTsNodeEndByte nodeEndByte;
final DTsQueryNew queryNew;
final DTsQueryDelete queryDelete;
final DTsQueryCaptureCount queryCaptureCount;
final DTsQueryCaptureNameForId queryCaptureNameForId;
final DTsQueryCursorNew queryCursorNew;
final DTsQueryCursorDelete queryCursorDelete;
final DTsQueryCursorExec queryCursorExec;
final DTsQueryCursorNextMatch queryCursorNextMatch;
final DTsWasmStoreNew wasmStoreNew;
final DTsWasmStoreDelete wasmStoreDelete;
final DTsWasmStoreLoadLanguage wasmStoreLoadLanguage;
final DWasmEngineNew wasmEngineNew;
final DWasmEngineDelete wasmEngineDelete;
static TreeSitterLib? _instance;
static TreeSitterLib? get instance => _instance;
static bool init() {
if (_instance != null) return true;
final lib = _openLibrary();
if (lib == null) return false;
_instance = TreeSitterLib._(lib);
return true;
}
static DynamicLibrary? _openLibrary() {
final libName = Platform.isLinux
? 'libtree-sitter.so'
: Platform.isMacOS
? 'libtree-sitter.dylib'
: Platform.isWindows
? 'tree-sitter.dll'
: null;
if (libName == null) return null;
// Try standard dlopen path first (works when lib is in bundle/lib/).
try {
return DynamicLibrary.open(libName);
} catch (_) {}
// Try next to executable.
final exe = File(Platform.resolvedExecutable).parent.path;
for (final dir in ['$exe/lib', exe]) {
final path = '$dir/$libName';
if (File(path).existsSync()) {
try {
return DynamicLibrary.open(path);
} catch (_) {}
}
}
return null;
}
}
@@ -0,0 +1,291 @@
library;
import 'dart:convert' show utf8;
import 'dart:ffi';
import 'dart:ui' show Color;
import 'package:clide/kernel/src/syntax/language_map.dart';
import 'package:clide/kernel/src/syntax/tree_sitter_ffi.dart';
import 'package:clide/kernel/src/theme/tokens.dart';
import 'package:ffi/ffi.dart';
import 'package:flutter/services.dart' show rootBundle;
class SyntaxSpan {
const SyntaxSpan({
required this.start,
required this.end,
required this.role,
});
final int start;
final int end;
final String role;
}
class SyntaxResult {
const SyntaxResult(this.spans);
final List<SyntaxSpan> spans;
static const empty = SyntaxResult([]);
}
class _LoadedGrammar {
_LoadedGrammar({
required this.language,
required this.query,
required this.captureNames,
});
final Pointer<Void> language;
final Pointer<TSQuery> query;
final List<String> captureNames;
}
class TreeSitterService {
final Map<String, _LoadedGrammar> _grammars = {};
final Set<String> _unavailable = {};
Pointer<TSWasmStore>? _store;
Pointer<TSParser>? _parser;
Pointer<TSQueryCursor>? _cursor;
bool _initDone = false;
bool _init() {
if (_initDone) return _parser != null;
_initDone = true;
final lib = TreeSitterLib.instance;
if (lib == null) return false;
final engine = lib.wasmEngineNew();
if (engine == nullptr) return false;
final error = calloc<TSWasmError>();
_store = lib.wasmStoreNew(engine, error);
lib.wasmEngineDelete(engine);
if (_store == null || _store == nullptr) {
calloc.free(error);
return false;
}
calloc.free(error);
_parser = lib.parserNew();
if (_parser == null || _parser == nullptr) return false;
lib.parserSetWasmStore(_parser!, _store!);
_cursor = lib.queryCursorNew();
return true;
}
Future<_LoadedGrammar?> _loadGrammar(String language) async {
if (_unavailable.contains(language)) return null;
final cached = _grammars[language];
if (cached != null) return cached;
if (!_init()) {
_unavailable.add(language);
return null;
}
final lib = TreeSitterLib.instance!;
try {
// Load grammar WASM bytes.
final wasmData =
await rootBundle.load('assets/grammars/$language.wasm');
final wasmBytes = wasmData.buffer.asUint8List();
// Load into WASM store.
final nameNative = language.toNativeUtf8();
final wasmNative = calloc<Uint8>(wasmBytes.length);
wasmNative.asTypedList(wasmBytes.length).setAll(0, wasmBytes);
final error = calloc<TSWasmError>();
final lang = lib.wasmStoreLoadLanguage(
_store!, nameNative.cast(), wasmNative, wasmBytes.length, error,
);
calloc.free(wasmNative);
calloc.free(nameNative);
if (lang == nullptr) {
final msg = error.ref.message;
if (msg != nullptr) calloc.free(msg);
calloc.free(error);
_unavailable.add(language);
return null;
}
calloc.free(error);
// Load highlight query.
String? querySource;
try {
querySource =
await rootBundle.loadString('assets/queries/$language.scm');
} catch (_) {}
Pointer<TSQuery> query = nullptr;
List<String> captureNames = [];
if (querySource != null) {
final queryNative = querySource.toNativeUtf8();
final queryLen = utf8.encode(querySource).length;
final errorOffset = calloc<Uint32>();
final errorType = calloc<Int32>();
query = lib.queryNew(
lang, queryNative.cast(), queryLen, errorOffset, errorType,
);
calloc.free(queryNative);
calloc.free(errorOffset);
calloc.free(errorType);
if (query != nullptr) {
final count = lib.queryCaptureCount(query);
final lenOut = calloc<Uint32>();
for (var i = 0; i < count; i++) {
final namePtr = lib.queryCaptureNameForId(query, i, lenOut);
final len = lenOut.value;
captureNames.add(namePtr.cast<Utf8>().toDartString(length: len));
}
calloc.free(lenOut);
}
}
final grammar = _LoadedGrammar(
language: lang,
query: query,
captureNames: captureNames,
);
_grammars[language] = grammar;
return grammar;
} catch (_) {
_unavailable.add(language);
return null;
}
}
Future<bool> hasGrammar(String path) async {
final lang = grammarForPath(path);
if (lang == null) return false;
return (await _loadGrammar(lang)) != null;
}
Future<String?> languageFor(String path) async {
final lang = grammarForPath(path);
if (lang == null) return null;
return (await _loadGrammar(lang)) != null ? lang : null;
}
List<String> get loadedLanguages => _grammars.keys.toList();
Future<SyntaxResult> highlight(String path, String source) async {
final lang = grammarForPath(path);
if (lang == null) return SyntaxResult.empty;
final grammar = await _loadGrammar(lang);
if (grammar == null || grammar.query == nullptr) {
return SyntaxResult.empty;
}
final lib = TreeSitterLib.instance!;
final parser = _parser!;
final cursor = _cursor!;
// Set language on parser for this parse.
lib.parserSetLanguage(parser, grammar.language);
// Parse source.
final sourceNative = source.toNativeUtf8();
final sourceLen = utf8.encode(source).length;
final tree = lib.parserParseString(
parser, nullptr, sourceNative.cast(), sourceLen,
);
if (tree == nullptr) {
calloc.free(sourceNative);
return SyntaxResult.empty;
}
final root = lib.treeRootNode(tree);
// Run highlight query.
lib.queryCursorExec(cursor, grammar.query, root);
final match = calloc<TSQueryMatch>();
final spans = <SyntaxSpan>[];
while (lib.queryCursorNextMatch(cursor, match)) {
final m = match.ref;
for (var i = 0; i < m.captureCount; i++) {
final cap = m.captures[i];
final captureIndex = cap.index;
if (captureIndex < grammar.captureNames.length) {
spans.add(SyntaxSpan(
start: lib.nodeStartByte(cap.node),
end: lib.nodeEndByte(cap.node),
role: grammar.captureNames[captureIndex],
));
}
}
}
calloc.free(match);
lib.treeDelete(tree);
calloc.free(sourceNative);
return SyntaxResult(spans);
}
void dispose() {
final lib = TreeSitterLib.instance;
if (lib == null) return;
for (final grammar in _grammars.values) {
if (grammar.query != nullptr) lib.queryDelete(grammar.query);
}
_grammars.clear();
if (_cursor != null && _cursor != nullptr) lib.queryCursorDelete(_cursor!);
// Parser and WASM store are cleaned up together — deleting the parser
// does not delete the store, but the store owns the languages.
if (_parser != null && _parser != nullptr) lib.parserDelete(_parser!);
if (_store != null && _store != nullptr) lib.wasmStoreDelete(_store!);
_parser = null;
_store = null;
_cursor = null;
_unavailable.clear();
}
static Color colorForRole(String role, SurfaceTokens tokens) {
return switch (role) {
'keyword' || 'repeat' || 'conditional' || 'include' ||
'exception' || 'operator' =>
tokens.syntaxKeyword,
'type' || 'type.builtin' || 'constructor' => tokens.syntaxType,
'string' || 'string.special' => tokens.syntaxString,
'number' || 'float' || 'boolean' => tokens.syntaxNumber,
'comment' => tokens.syntaxComment,
'function' || 'function.builtin' || 'function.method' ||
'method' =>
tokens.syntaxMethod,
'punctuation.bracket' || 'punctuation.delimiter' ||
'punctuation.special' =>
tokens.syntaxPunct,
'variable' || 'variable.builtin' || 'variable.parameter' =>
tokens.globalForeground,
'property' || 'field' => tokens.syntaxMethod,
'constant' || 'constant.builtin' => tokens.syntaxNumber,
'tag' || 'attribute' => tokens.syntaxKeyword,
'namespace' || 'module' => tokens.syntaxType,
'text.title' => tokens.syntaxKeyword,
'text.literal' || 'text.reference' || 'text.uri' => tokens.syntaxString,
'text.emphasis' || 'text.strong' => tokens.syntaxType,
_ => tokens.globalForeground,
};
}
}