lspTool function
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}');
}
},
);
}