lspTool function

AgentTool lspTool(
  1. ExecutionEnv env, {
  2. required LspToolConfig config,
})

Creates the lsp tool bound to env. Registered from builtinTools only when the host supplies an LspToolConfig — process-capable envs (CLI/desktop) do; web/stub construction leaves the tool out.

Implementation

AgentTool lspTool(ExecutionEnv env, {required LspToolConfig config}) {
  Future<LspClientManager>? managerFuture;
  Future<LspClientManager> ensureManager() {
    final injected = config.manager;
    if (injected != null) return Future.value(injected);
    return managerFuture ??= () async {
      final loaded = config.config ?? await LspConfig.load(env);
      return LspClientManager(
        env: env,
        config: loaded,
        transportFactory: config.transportFactory,
        processId: config.processId,
        requestTimeout: config.requestTimeout,
        initTimeout: config.initTimeout,
        idleTimeout: config.idleTimeout,
      );
    }();
  }

  return AgentTool(
    name: 'lsp',
    label: 'lsp',
    tier: ApprovalTier.write,
    description: lspToolDescriptionPrompt,
    parameters: const {
      'type': 'object',
      'properties': {
        'op': {
          'type': 'string',
          'enum': ['diagnostics', 'definition', 'references', 'rename'],
          'description': 'The language-server operation to run',
        },
        'path': {
          'type': 'string',
          'description':
              'File to inspect (relative or absolute). For rename, the file '
              'containing the symbol',
        },
        'line': {
          'type': 'integer',
          'description':
              '1-indexed line of the symbol (default 1); used by '
              'definition/references/rename',
        },
        'character': {
          'type': 'integer',
          'description':
              '1-indexed character (column) of the symbol (default 1); '
              'used by definition/references/rename',
        },
        'newName': {
          'type': 'string',
          'description': 'The new identifier (required for rename)',
        },
      },
      'required': ['op', 'path'],
    },
    execute: (arguments, cancelToken, onUpdate) async {
      cancelToken?.throwIfCancelled();
      final op = arguments['op'] as String;
      final path = arguments['path'] as String;
      final line = (arguments['line'] as num?)?.toInt() ?? 1;
      final character = (arguments['character'] as num?)?.toInt() ?? 1;
      final newName = arguments['newName'] as String?;

      if (op == 'rename' && (newName == null || newName.isEmpty)) {
        return ToolExecutionResult.text(
          'newName is required for the rename op',
        );
      }

      final manager = await ensureManager();
      final absolute = (await env.absolutePath(path)).valueOrNull ?? path;

      final LspClient client;
      try {
        client = await manager.clientForFile(absolute);
      } on LspNoServerException {
        return ToolExecutionResult.text(
          'No LSP server configured for $path. Configure servers in '
          '$lspConfigFileName.',
        );
      } on LspServerUnavailableException catch (error) {
        return ToolExecutionResult.text('LSP server unavailable: $error');
      }
      cancelToken?.throwIfCancelled();

      final read = await env.readTextFile(absolute);
      if (read.isErr) throw StateError('${read.errorOrNull}');
      final server = client.config;
      final uri = fileToUri(absolute);
      final capturedVersion = client.diagnosticsVersion;
      if (op == 'diagnostics') {
        // Sync the on-disk content so the server re-analyzes and publishes
        // fresh diagnostics (omp's refreshFile); opens the file when new.
        client.syncContent(
          absolute,
          read.valueOrNull!,
          server.languageIdFor(path),
        );
      } else {
        client.ensureOpen(
          absolute,
          read.valueOrNull!,
          server.languageIdFor(path),
        );
      }
      cancelToken?.throwIfCancelled();

      try {
        return await switch (op) {
          'diagnostics' => _diagnostics(
            env,
            client,
            absolute,
            uri,
            capturedVersion,
            config.diagnosticsWait,
          ),
          'definition' => _locations(
            env,
            client,
            'textDocument/definition',
            uri,
            line,
            character,
            'definition',
          ),
          'references' => _locations(
            env,
            client,
            'textDocument/references',
            uri,
            line,
            character,
            'reference',
          ),
          'rename' => _rename(
            env,
            client,
            absolute,
            uri,
            line,
            character,
            newName!,
          ),
          _ => throw StateError('unknown lsp op: $op'),
        };
      } on LspRequestException catch (error) {
        throw StateError('LSP error: ${error.message}');
      }
    },
  );
}