localCompletionCandidates function

Future<List<String>> localCompletionCandidates(
  1. ShellBackend backend,
  2. ShellDialect dialect,
  3. String word, {
  4. required bool isCommand,
  5. required ShellFamily family,
  6. String? cwd,
  7. Duration timeout = const Duration(seconds: 4),
})

Generates TAB-completion candidates for word by running the dialect's completion command as a one-shot SessionMode.exec on a local backend, reading its stdout to EOF.

This is the local counterpart to the remote path, which issues the same dialect.completionCommand(...) via ClientRuntime.execute. Running it in cwd under family's interpreter keeps relative-path completion correct. Best-effort: callers treat a thrown error (including the timeout) as "no candidates".

Implementation

Future<List<String>> localCompletionCandidates(
  ShellBackend backend,
  ShellDialect dialect,
  String word, {
  required bool isCommand,
  required ShellFamily family,
  String? cwd,
  Duration timeout = const Duration(seconds: 4),
}) async {
  final session = await backend.start(
    ShellRequest(
      mode: SessionMode.exec,
      command: dialect.completionCommand(word, isCommand: isCommand),
      cwd: cwd,
      shellFamily: family,
    ),
  );
  // Drain stderr so a chatty completion command never blocks on a full pipe.
  final errSub = session.stderr.listen((_) {});
  final BytesBuilder out;
  try {
    // Accumulate stdout until the stream closes (EOF) — i.e. all output has been
    // delivered. Awaiting `exitCode` instead would race: the process can exit
    // before its buffered stdout is dispatched, dropping the candidates.
    out = await session.stdout
        .fold<BytesBuilder>(
          BytesBuilder(copy: false),
          (builder, chunk) => builder..add(chunk),
        )
        .timeout(timeout);
  } finally {
    await errSub.cancel();
  }
  final candidates = utf8
      .decode(out.takeBytes(), allowMalformed: true)
      .split('\n')
      .map((s) => s.trimRight())
      .where((s) => s.isNotEmpty)
      .toList();
  return candidates.length > _maxCandidates
      ? candidates.sublist(0, _maxCandidates)
      : candidates;
}