[ DAS ] Add initial support for widget preview detection service

The Flutter Widget Preview feature is currently implemented within
Flutter Tools, which is responsible for detecting widget preview
annotations in the user's project. When previews are detected, the
Flutter Tool injects code generated based on the detected previews into an artificial widget_preview_scaffold project and performs a hot reload to render updates to the preview set in the scaffold application.

`package:analyzer` is currently being used to detect previews, but this comes with a significant amount of memory overhead. Since widget
previews are mostly being used from within IDEs which already have an
active analysis server, moving widget preview detection into the DAS
will remove the need for creating an additional analysis context in the Flutter Tool itself.

This change includes the initial work to move widget preview detection
into the DAS. It utilizes a pull-based mechanism, where the Flutter Tool listens for file system events and then queries the DAS using the `dart/textDocument/getFlutterWidgetPreviews` and `dart/workspace/getFlutterWidgetPreviews` LSP methods.

Each reported preview contains some generated code based on the annotation used to define the preview. This code has all constants from the original annotation evaluated to either primitive values or constant expressions with namespaces applied to each symbol, allowing for the Flutter Tool to inject this code directly when updating the generated code in the scaffold project.

Towards https://github.com/flutter/flutter/issues/179584

Change-Id: I043cb3235a66b25dda3f852ca7f147bff0e1e537
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/478100
Auto-Submit: Ben Konyi <bkonyi@google.com>
Commit-Queue: Ben Konyi <bkonyi@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
This commit is contained in:
Ben Konyi
2026-03-22 19:19:03 -07:00
committed by Commit Queue
parent 086cb1a441
commit 2a83a78213
22 changed files with 1961 additions and 11 deletions
@@ -155,6 +155,12 @@ abstract final class CustomMethods {
static const publishFlutterOutline = Method(
'dart/textDocument/publishFlutterOutline',
);
static const getFlutterWidgetPreviews = Method(
'dart/textDocument/getFlutterWidgetPreviews',
);
static const getWorkspaceFlutterWidgetPreviews = Method(
'dart/workspace/getFlutterWidgetPreviews',
);
static const summary = Method('dart/textDocument/summary');
static const super_ = Method('dart/textDocument/super');
static const imports = Method('dart/textDocument/imports');
@@ -0,0 +1,157 @@
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:analysis_server/lsp_protocol/protocol.dart' hide Element;
import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analysis_server/src/lsp/error_or.dart';
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
import 'package:analysis_server/src/services/flutter/widget_previews.dart';
import 'package:analyzer/dart/analysis/results.dart';
class FlutterWidgetPreviewsHandler
extends
SharedMessageHandler<TextDocumentIdentifier, FlutterWidgetPreviews?> {
FlutterWidgetPreviewsHandler(super.server);
@override
Method get handlesMessage => CustomMethods.getFlutterWidgetPreviews;
@override
LspJsonHandler<TextDocumentIdentifier> get jsonHandler =>
TextDocumentIdentifier.jsonHandler;
@override
bool get requiresTrustedCaller => false;
@override
Future<ErrorOr<FlutterWidgetPreviews?>> handle(
TextDocumentIdentifier params,
MessageInfo message,
CancellationToken token,
) async {
var lspServer = server as LspAnalysisServer;
var path = pathOfDoc(params);
if (path.isError) {
return failure(path);
}
var pathResult = path.resultOrNull!;
var result = await lspServer.getResolvedUnit(pathResult);
if (result == null) {
return success(null);
}
var graph = <Uri, LibraryPreviewNode>{};
var processed = <Uri>{};
var flutterWidgetPreviewDetector = FlutterWidgetPreviewDetector();
// Build a graph starting from this library to track dependencies and errors.
Future<void> buildGraph(ResolvedUnitResult unit) async {
var fileUri = unit.uri;
if (processed.contains(fileUri)) return;
processed.add(fileUri);
// Scan all units in the library.
var libraryElement = unit.libraryElement;
for (var unitPath in libraryElement.fragments.map(
(f) => f.source.fullName,
)) {
var resolvedUnit = await lspServer.getResolvedUnit(unitPath);
if (resolvedUnit != null) {
flutterWidgetPreviewDetector.findPreviews(resolvedUnit, graph: graph);
}
}
var node = graph[libraryElement.uri]!;
for (var dependency in node.dependsOn) {
if (processed.contains(dependency.uri)) continue;
var depPath = dependency.path;
var driver = lspServer.getAnalysisDriver(depPath);
if (driver == null || !driver.addedFiles.contains(depPath)) {
continue;
}
var depResult = await lspServer.getResolvedUnit(depPath);
if (depResult != null) {
await buildGraph(depResult);
}
}
}
await buildGraph(result);
flutterWidgetPreviewDetector.propagateErrors(graph);
var node = graph[result.libraryElement.uri]!;
return success(
FlutterWidgetPreviews(
scriptUris: [result.uri],
previews: node.previews,
namespaces: flutterWidgetPreviewDetector.namespaces,
),
);
}
}
class WorkspaceFlutterWidgetPreviewsHandler
extends SharedMessageHandler<void, FlutterWidgetPreviews?> {
WorkspaceFlutterWidgetPreviewsHandler(super.server);
@override
Method get handlesMessage => CustomMethods.getWorkspaceFlutterWidgetPreviews;
@override
LspJsonHandler<void> get jsonHandler => nullJsonHandler;
@override
bool get requiresTrustedCaller => false;
@override
Future<ErrorOr<FlutterWidgetPreviews?>> handle(
void _,
MessageInfo message,
CancellationToken token,
) async {
var lspServer = server as LspAnalysisServer;
var graph = <Uri, LibraryPreviewNode>{};
var processedLibraries = <Uri>{};
var flutterWidgetPreviewDetector = FlutterWidgetPreviewDetector();
for (var driver in lspServer.driverMap.values) {
for (var file in driver.addedFiles) {
var libraryResult = await lspServer.getResolvedLibrary(file);
if (libraryResult != null) {
var uri = libraryResult.element.uri;
if (processedLibraries.contains(uri)) continue;
processedLibraries.add(uri);
for (var unit in libraryResult.units) {
flutterWidgetPreviewDetector.findPreviews(unit, graph: graph);
}
}
}
}
flutterWidgetPreviewDetector.propagateErrors(graph);
var allPreviews = <FlutterWidgetPreviewDetails>[];
var allScriptUris = <Uri>{};
for (var node in graph.values) {
allPreviews.addAll(node.previews);
for (var preview in node.previews) {
allScriptUris.add(preview.scriptUri);
}
}
return success(
FlutterWidgetPreviews(
namespaces: flutterWidgetPreviewDetector.namespaces,
previews: allPreviews,
scriptUris: allScriptUris.toList(),
),
);
}
}
@@ -15,6 +15,7 @@ import 'package:analysis_server/src/lsp/handlers/custom/handler_augmented.dart';
import 'package:analysis_server/src/lsp/handlers/custom/handler_connect_to_dtd.dart';
import 'package:analysis_server/src/lsp/handlers/custom/handler_diagnostic_server.dart';
import 'package:analysis_server/src/lsp/handlers/custom/handler_experimental_echo.dart';
import 'package:analysis_server/src/lsp/handlers/custom/handler_get_widget_previews.dart';
import 'package:analysis_server/src/lsp/handlers/custom/handler_imports.dart';
import 'package:analysis_server/src/lsp/handlers/custom/handler_reanalyze.dart';
import 'package:analysis_server/src/lsp/handlers/custom/handler_summary.dart';
@@ -133,6 +134,7 @@ class InitializedStateMessageHandler extends ServerStateMessageHandler {
EditArgumentHandler.new,
ExecuteCommandHandler.new,
ExperimentalEchoHandler.new,
FlutterWidgetPreviewsHandler.new,
FormatOnTypeHandler.new,
FormatRangeHandler.new,
FormattingHandler.new,
@@ -152,6 +154,7 @@ class InitializedStateMessageHandler extends ServerStateMessageHandler {
TypeHierarchySupertypesHandler.new,
UpdateDiagnosticInformationHandler.new,
WillRenameFilesHandler.new,
WorkspaceFlutterWidgetPreviewsHandler.new,
WorkspaceSymbolHandler.new,
];
@@ -0,0 +1,647 @@
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/constant/value.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/diagnostic/diagnostic.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:collection/collection.dart';
import 'package:language_server_protocol/protocol_custom_generated.dart'
hide Element;
import 'package:language_server_protocol/protocol_generated.dart' show Position;
class FlutterWidgetPreviewDetector {
final _namespaceAllocator = NamespaceAllocator();
Map<String, String> get namespaces => _namespaceAllocator.namespaces;
/// Search for functions annotated with `@Preview` in the current project.
void findPreviews(
ResolvedUnitResult resolvedUnit, {
Map<Uri, LibraryPreviewNode>? graph,
}) {
var lib = resolvedUnit.libraryElement;
var previewsForLibrary = graph != null
? graph.putIfAbsent(
lib.uri,
() => LibraryPreviewNode(
library: lib,
namespaceAllocator: _namespaceAllocator,
),
)
: LibraryPreviewNode(
library: lib,
namespaceAllocator: _namespaceAllocator,
);
// Track errors in the current file.
previewsForLibrary.populateErrorsForFile(
uri: resolvedUnit.uri,
diagnostics: resolvedUnit.diagnostics,
);
// If we have a graph, update dependencies for propagation.
if (graph != null) {
previewsForLibrary.updateDependencyGraph(
graph: graph,
unit: resolvedUnit,
);
}
// Iterate over the library's AST to find previews.
previewsForLibrary.addPreviews(unit: resolvedUnit);
}
/// Propagates errors through the dependency graph.
void propagateErrors(Map<Uri, LibraryPreviewNode> graph) {
// Reset the error state for all dependencies.
for (var libraryDetails in graph.values) {
libraryDetails.dependencyHasErrors = false;
}
void propagateErrorsHelper(LibraryPreviewNode errorContainingNode) {
for (var importer in errorContainingNode.dependedOnBy) {
if (importer.dependencyHasErrors) {
// This dependency path has already been processed.
continue;
}
importer.dependencyHasErrors = true;
propagateErrorsHelper(importer);
}
}
// Find the libraries that have errors and mark each of their downstream
// dependencies as having a dependency containing errors.
for (var nodeDetails in graph.values) {
if (nodeDetails.hasErrors) {
propagateErrorsHelper(nodeDetails);
}
}
// Update the error flags on all previews based on the propagated state.
for (var node in graph.values) {
var hasError = node.hasErrors;
var dependencyHasErrors = node.dependencyHasErrors;
for (var i = 0; i < node.previews.length; i++) {
var preview = node.previews[i];
if (preview.hasError != hasError ||
preview.dependencyHasErrors != dependencyHasErrors) {
node.previews[i] = FlutterWidgetPreviewDetails(
scriptUri: preview.scriptUri,
position: preview.position,
packageName: preview.packageName,
functionName: preview.functionName,
isBuilder: preview.isBuilder,
previewAnnotation: preview.previewAnnotation,
isMultiPreview: preview.isMultiPreview,
hasError: hasError,
dependencyHasErrors: dependencyHasErrors,
);
}
}
}
}
}
/// Contains information related to a library being scanned for previews.
final class LibraryPreviewNode {
final NamespaceAllocator namespaceAllocator;
/// The URI pointing to the library.
final Uri uri;
/// The absolute path to the library's defining unit.
final String path;
/// The list of previews contained within the file.
final previews = <FlutterWidgetPreviewDetails>[];
/// Files that import this file.
final dependedOnBy = <LibraryPreviewNode>{};
/// Files this file imports.
final dependsOn = <LibraryPreviewNode>{};
/// `true` if a transitive dependency has compile time errors.
bool dependencyHasErrors = false;
/// The set of errors found in this library.
final errors = <Diagnostic>[];
LibraryPreviewNode({
required LibraryElement library,
required this.namespaceAllocator,
}) : uri = library.uri,
path = library.firstFragment.source.fullName;
/// `true` if this library contains compile time errors.
bool get hasErrors => errors.isNotEmpty;
/// Finds all previews defined in the [unit] and adds them to [previews].
void addPreviews({required ResolvedUnitResult unit}) {
// Iterate over the compilation unit's AST to find previews.
var visitor = _PreviewVisitor(
lib: unit.libraryElement,
previewNode: this,
namespaceAllocator: namespaceAllocator,
);
visitor.findPreviewsInResolvedUnitResult(unit);
// Remove existing previews for this unit before adding new ones.
previews.removeWhere((p) => p.scriptUri == unit.uri);
previews.addAll(visitor.previewEntries);
}
/// Determines the set of errors found in this file.
void populateErrorsForFile({
required Uri uri,
required List<Diagnostic> diagnostics,
}) {
errors
..removeWhere((e) => e.source.uri == uri)
..addAll(diagnostics.where((e) => e.severity == Severity.error));
}
/// Updates the dependency graph based on changes to a compilation [unit].
void updateDependencyGraph({
required Map<Uri, LibraryPreviewNode> graph,
required ResolvedUnitResult unit,
}) {
var updatedDependencies = <LibraryPreviewNode>{};
for (var fragment in unit.libraryElement.fragments) {
for (var importedLib in fragment.libraryImports) {
if (importedLib.importedLibrary == null) {
continue;
}
var importedLibrary = importedLib.importedLibrary!;
var result = graph.putIfAbsent(
importedLibrary.uri,
() => LibraryPreviewNode(
library: importedLibrary,
namespaceAllocator: namespaceAllocator,
),
);
updatedDependencies.add(result);
}
}
// Only update dependsOn for the library unit itself to avoid confusion
// with parts, or just use a cumulative set.
dependsOn.addAll(updatedDependencies);
for (var dependency in updatedDependencies) {
dependency.dependedOnBy.add(this);
}
}
}
/// Tracks imports and assigns namespaces to each unique library URL.
class NamespaceAllocator {
static const _doNotPrefix = ['dart:core'];
final _imports = <String, int>{};
var _keys = 1;
/// Returns import source code for each library seen.
Map<String, String> get namespaces =>
_imports.map((uri, id) => MapEntry(uri, '_i$id'));
/// Returns the name of [symbol] with a namespace prefix assigned based on
/// [url].
String applyNamespaceToSymbol({
required String symbol,
required String? url,
}) {
if (url == null || _doNotPrefix.contains(url)) {
return symbol;
}
return '_i${_imports.putIfAbsent(url, _nextKey)}.$symbol';
}
int _nextKey() => _keys++;
}
/// Visitor which detects previews and extracts [PreviewDetails] for later code
/// generation.
class _PreviewVisitor extends RecursiveAstVisitor<void> {
final LibraryPreviewNode previewNode;
final NamespaceAllocator namespaceAllocator;
late final String? packageName;
final previewEntries = <FlutterWidgetPreviewDetails>[];
FunctionDeclaration? _currentFunction;
ConstructorDeclaration? _currentConstructor;
MethodDeclaration? _currentMethod;
late Uri _currentScriptUri;
late CompilationUnit _currentUnit;
_PreviewVisitor({
required LibraryElement lib,
required this.previewNode,
required this.namespaceAllocator,
}) : packageName = lib.uri.scheme == 'package'
? lib.uri.pathSegments.first
: null;
void findPreviewsInResolvedUnitResult(ResolvedUnitResult unit) {
_currentScriptUri = unit.uri;
_currentUnit = unit.unit;
_currentUnit.visitChildren(this);
}
bool hasRequiredParams(FormalParameterList? params) {
return params?.parameters.any((p) => p.isRequired) ?? false;
}
@override
void visitAnnotation(Annotation node) {
bool isMultiPreview = node.isMultiPreview;
bool isPreview = node.isPreview;
// Skip non-preview annotations.
if (!isPreview && !isMultiPreview) {
return;
}
// The preview annotations must only have constant arguments.
DartObject? preview = node.elementAnnotation!.computeConstantValue();
if (preview == null) {
return;
}
LineInfo lineInfo = _currentUnit.lineInfo;
CharacterLocation location = lineInfo.getLocation(node.offset);
int line = location.lineNumber;
int column = location.columnNumber;
var hasError = previewNode.hasErrors;
var dependencyHasErrors = previewNode.dependencyHasErrors;
FlutterWidgetPreviewDetails buildPreviewDetails({
required String functionName,
required bool isWidgetBuilder,
}) {
return FlutterWidgetPreviewDetails(
scriptUri: _currentScriptUri,
position: Position(character: column, line: line),
packageName: packageName,
functionName: functionName,
isBuilder: isWidgetBuilder,
previewAnnotation: preview.toSource(namespaceAllocator),
isMultiPreview: isMultiPreview,
hasError: hasError,
dependencyHasErrors: dependencyHasErrors,
);
}
if (_currentFunction != null &&
!hasRequiredParams(_currentFunction!.functionExpression.parameters)) {
TypeAnnotation? returnTypeAnnotation = _currentFunction!.returnType;
if (returnTypeAnnotation is NamedType) {
Token returnType = returnTypeAnnotation.name;
if (returnType.isWidget || returnType.isWidgetBuilder) {
previewEntries.add(
buildPreviewDetails(
functionName: _currentFunction!.name.toString(),
isWidgetBuilder: returnType.isWidgetBuilder,
),
);
}
}
} else if (_currentConstructor != null &&
!hasRequiredParams(_currentConstructor!.parameters)) {
var returnType = _currentConstructor!.typeName!;
Token? name = _currentConstructor!.name;
previewEntries.add(
buildPreviewDetails(
functionName: '$returnType${name == null ? '' : '.$name'}',
isWidgetBuilder: false,
),
);
} else if (_currentMethod != null &&
!hasRequiredParams(_currentMethod!.parameters)) {
TypeAnnotation? returnTypeAnnotation = _currentMethod!.returnType;
if (returnTypeAnnotation is NamedType) {
Token returnType = returnTypeAnnotation.name;
if (returnType.isWidget || returnType.isWidgetBuilder) {
var parentClass = _currentMethod!.parent!.parent! as ClassDeclaration;
previewEntries.add(
buildPreviewDetails(
functionName:
'${parentClass.namePart.typeName}.${_currentMethod!.name}',
isWidgetBuilder: returnType.isWidgetBuilder,
),
);
}
}
}
}
/// Handles previews defined on constructors.
@override
void visitConstructorDeclaration(ConstructorDeclaration node) {
_scopedVisitChildren(
node,
(ConstructorDeclaration? node) => _currentConstructor = node,
);
}
/// Handles previews defined on top-level functions.
@override
void visitFunctionDeclaration(FunctionDeclaration node) {
assert(_currentFunction == null);
if (node.name.isPrivate) {
return;
}
TypeAnnotation? returnType = node.returnType;
if (returnType == null || returnType.question != null) {
return;
}
_scopedVisitChildren(
node,
(FunctionDeclaration? node) => _currentFunction = node,
);
}
/// Handles previews defined on static methods within classes.
@override
void visitMethodDeclaration(MethodDeclaration node) {
if (!node.isStatic) {
return;
}
_scopedVisitChildren(
node,
(MethodDeclaration? node) => _currentMethod = node,
);
}
void _scopedVisitChildren<T extends AstNode>(
T node,
void Function(T?) setter,
) {
setter(node);
node.visitChildren(this);
setter(null);
}
}
extension on Annotation {
static final widgetPreviewsLibraryUri = Uri.parse(
'package:flutter/src/widget_previews/widget_previews.dart',
);
/// Convenience getter to identify `@MultiPreview` annotations
bool get isMultiPreview => _isPreviewType('MultiPreview');
/// Convenience getter to identify `@Preview` annotations
bool get isPreview => _isPreviewType('Preview');
bool _isPreviewType(String typeName) {
Element? element = elementAnnotation!.element;
if (element is ConstructorElement) {
InterfaceType type = element.enclosingElement.thisType;
return type.isType(typeName: typeName, uri: widgetPreviewsLibraryUri);
}
return false;
}
}
extension on DartObject {
/// Generates an equivalent source code representation of this constant
/// object using [prefixAllocator] to apply namespaces to types.
String toSource(NamespaceAllocator prefixAllocator) {
DartType type = this.type!;
return switch (type) {
DartType(isDartCoreBool: true) => toBoolValue()!.toString(),
DartType(isDartCoreDouble: true) => toDoubleValue()!.toString(),
DartType(isDartCoreInt: true) => toIntValue()!.toString(),
DartType(isDartCoreString: true) => "'${toStringValue()!}'",
DartType(isDartCoreNull: true) => 'null',
DartType(isDartCoreList: true) => _buildListSource(prefixAllocator),
DartType(isDartCoreMap: true) => _buildMapSource(prefixAllocator),
DartType(isDartCoreSet: true) => _buildSetSource(prefixAllocator),
RecordType() => _buildRecordSource(prefixAllocator),
InterfaceType(element: EnumElement()) => _buildEnumInstanceSource(
prefixAllocator,
),
InterfaceType() => _buildInstanceSource(prefixAllocator),
FunctionType() => _createTearoffSource(prefixAllocator),
_ => throw UnsupportedError('Unexpected DartObject type: $runtimeType'),
};
}
String _buildEnumInstanceSource(NamespaceAllocator prefixAllocator) {
VariableElement variable = this.variable!;
var url = variable.library!.uri.toString();
return switch (variable) {
FieldElement(
isEnumConstant: true,
displayName: var enumValue,
enclosingElement: EnumElement(displayName: var enumName),
) =>
prefixAllocator.applyNamespaceToSymbol(
symbol: '$enumName.$enumValue',
url: url,
),
PropertyInducingElement(:var displayName) =>
prefixAllocator.applyNamespaceToSymbol(symbol: displayName, url: url),
_ => throw UnsupportedError(
'Unexpected enum variable type: ${variable.runtimeType}',
),
};
}
String _buildInstanceSource(NamespaceAllocator prefixAllocator) {
var dartType = type! as InterfaceType;
var invocation = constructorInvocation;
if (invocation == null) {
return prefixAllocator.applyNamespaceToSymbol(
symbol: dartType.element.name!,
url: dartType.element.library.uri.toString(),
);
}
ConstructorElement? constructor = invocation.constructor;
String? constructorName = constructor.name == 'new'
? null
: constructor.name;
List<String> positionalArguments = invocation.positionalArguments
.map((e) => e.toSource(prefixAllocator))
.toList();
var namedArguments = <String, String>{
for (final MapEntry(key: name, :value)
in invocation.namedArguments.entries)
name: value.toSource(prefixAllocator),
};
var typeArguments = <String>[
for (var typeArgument in dartType.typeArguments)
typeArgument.toSource(prefixAllocator),
];
var buffer = StringBuffer();
buffer.write(
prefixAllocator.applyNamespaceToSymbol(
symbol: dartType.element.name!,
url: dartType.element.library.uri.toString(),
),
);
if (typeArguments.isNotEmpty) {
buffer
..write('<')
..writeAll(typeArguments, ', ')
..write('>');
}
if (constructorName != null) {
buffer.write('.$constructorName');
}
buffer
..write('(')
..writeAll([
...positionalArguments,
...namedArguments.entries.map<String>((e) => '${e.key}: ${e.value}'),
], ', ')
..write(')');
return buffer.toString();
}
String _buildListSource(NamespaceAllocator prefixAllocator) {
var list = toListValue()!;
var buffer = StringBuffer();
buffer.write('[');
buffer.writeAll(list.map((e) => e.toSource(prefixAllocator)), ', ');
buffer.write(']');
return buffer.toString();
}
String _buildMapSource(NamespaceAllocator prefixAllocator) {
var map = toMapValue()!;
var buffer = StringBuffer();
buffer.write('{');
buffer.writeAll(
map.entries.map(
(e) =>
'${e.key!.toSource(prefixAllocator)}: ${e.value!.toSource(prefixAllocator)}',
),
', ',
);
buffer.write('}');
return buffer.toString();
}
String _buildRecordSource(NamespaceAllocator prefixAllocator) {
var record = toRecordValue()!;
var buffer = StringBuffer()
..write('(')
..writeAll([
...record.positional.map((e) => e.toSource(prefixAllocator)),
...record.named.entries.map(
(e) => '${e.key}: ${e.value.toSource(prefixAllocator)}',
),
], ', ')
..write(')');
return buffer.toString();
}
String _buildSetSource(NamespaceAllocator prefixAllocator) {
var set = toSetValue()!;
var buffer = StringBuffer();
buffer.write('{');
buffer.writeAll(set.map((e) => e.toSource(prefixAllocator)), ', ');
buffer.write('}');
return buffer.toString();
}
String _createTearoffSource(NamespaceAllocator prefixAllocator) {
var function = toFunctionValue()!;
return prefixAllocator.applyNamespaceToSymbol(
symbol: function.displayName,
url: function.library.uri.toString(),
);
}
}
extension on DartType {
/// Generates an equivalent source code representation of this type using
/// [prefixAllocator] to apply namespaces to all referenced types.
String toSource(NamespaceAllocator prefixAllocator) {
if (this is RecordType) {
return _recordToSource(this as RecordType, prefixAllocator);
}
var typeArguments = switch (this) {
InterfaceType(:var typeArguments) => [
for (var typeArgument in typeArguments)
typeArgument.toSource(prefixAllocator),
],
_ => <String>[],
};
var element = this.element!;
var buffer = StringBuffer();
buffer.write(
prefixAllocator.applyNamespaceToSymbol(
symbol: element.name!,
url: element.library!.uri.toString(),
),
);
if (typeArguments.isNotEmpty) {
buffer
..write('<')
..writeAll(typeArguments, ', ')
..write('>');
}
return buffer.toString();
}
String _recordToSource(RecordType type, NamespaceAllocator prefixAllocator) {
var positionalFields = type.positionalFields
.map((e) => e.type.toSource(prefixAllocator))
.join(', ');
var namedFields = type.namedFields
.map((e) => '${e.type.toSource(prefixAllocator)} ${e.name}')
.join(', ');
var buffer = StringBuffer();
buffer
..write('(')
..writeAll([
if (positionalFields.isNotEmpty) positionalFields,
if (namedFields.isNotEmpty) '{$namedFields}',
], ', ')
..write(')');
return buffer.toString();
}
}
extension on InterfaceType {
bool isType({required String typeName, required Uri uri}) {
if (getDisplayString() == typeName && element.library.uri == uri) {
return true;
}
return allSupertypes.firstWhereOrNull((e) {
return e.getDisplayString() == typeName &&
e.element.library.uri == uri;
}) !=
null;
}
}
extension on Token {
/// Convenience getter to identify tokens for private fields and functions.
bool get isPrivate => toString().startsWith('_');
/// Convenience getter to identify Widget types.
bool get isWidget => toString() == 'Widget';
/// Convenience getter to identify WidgetBuilder types.
bool get isWidgetBuilder => toString() == 'WidgetBuilder';
}
@@ -0,0 +1,398 @@
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analyzer/utilities/package_config_file_builder.dart';
import 'package:language_server_protocol/protocol_custom_generated.dart';
import 'package:language_server_protocol/protocol_generated.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'server_abstract.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(ExhaustiveFlutterWidgetPreviewsTest);
});
}
@reflectiveTest
class ExhaustiveFlutterWidgetPreviewsTest
extends AbstractLspAnalysisServerTest {
@override
bool get addFlutterLocalizationsPackageDep => true;
Future<FlutterWidgetPreviews?> getFlutterWidgetPreviews(Uri uri) {
var request = makeRequest(
CustomMethods.getFlutterWidgetPreviews,
TextDocumentIdentifier(uri: uri),
);
return expectSuccessfulResponseTo(request, FlutterWidgetPreviews.fromJson);
}
Future<FlutterWidgetPreviews?> getWorkspaceFlutterWidgetPreviews() {
var request = makeRequest(
CustomMethods.getWorkspaceFlutterWidgetPreviews,
null,
);
return expectSuccessfulResponseTo(request, FlutterWidgetPreviews.fromJson);
}
@override
void setUp() {
super.setUp();
writeTestPackageConfig(flutter: true);
addFlutter();
addSkyEngine(sdkPath: sdkRoot.path);
failTestOnErrorDiagnostic = false;
}
Future<void> test_addDeletePreviews() async {
var filePath = join(projectFolderPath, 'lib', 'previews.dart');
var fileUri = Uri.file(filePath);
newFile(filePath, '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';
@Preview(name: 'Initial')
Widget preview1() => Text('1');
''');
await initialize();
var result = await getFlutterWidgetPreviews(fileUri);
expect(result!.previews, hasLength(1));
expect(result.previews.first.functionName, 'preview1');
// Add a preview
newFile(filePath, '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';
@Preview(name: 'Initial')
Widget preview1() => Text('1');
@Preview(name: 'Added')
Widget preview2() => Text('2');
''');
result = await getFlutterWidgetPreviews(fileUri);
expect(result!.previews, hasLength(2));
expect(result.previews.any((p) => p.functionName == 'preview2'), isTrue);
// Delete a preview
newFile(filePath, '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';
@Preview(name: 'Added')
Widget preview2() => Text('2');
''');
result = await getFlutterWidgetPreviews(fileUri);
expect(result!.previews, hasLength(1));
expect(result.previews.first.functionName, 'preview2');
}
Future<void> test_annotationProperties() async {
newFile(join(projectFolderPath, 'lib', 'previews.dart'), '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';
class MyMultiPreview extends MultiPreview {
const MyMultiPreview(List<Preview> previews) : super(previews);
}
@Preview(
name: 'Custom Name',
group: 'My Group',
)
Widget myPreview() => Text('Hello');
@MyMultiPreview([
Preview(name: 'Light', brightness: Brightness.light),
Preview(name: 'Dark', brightness: Brightness.dark),
])
Widget multiPreview() => Text('Multi');
''');
await initialize();
var result = await getFlutterWidgetPreviews(
Uri.file(join(projectFolderPath, 'lib', 'previews.dart')),
);
expect(result, isNotNull);
expect(result!.previews, hasLength(2));
var custom = result.previews.firstWhere(
(p) => p.functionName == 'myPreview',
);
expect(custom.previewAnnotation, contains("name: 'Custom Name'"));
expect(custom.previewAnnotation, contains("group: 'My Group'"));
expect(custom.isMultiPreview, isFalse);
var multi = result.previews.firstWhere(
(p) => p.functionName == 'multiPreview',
);
expect(multi.isMultiPreview, isTrue);
// Since namespacing is applied, we check for the literal value without assuming prefixing.
expect(multi.previewAnnotation, contains("'Light'"));
expect(multi.previewAnnotation, contains('Brightness.light'));
expect(multi.previewAnnotation, contains("'Dark'"));
expect(multi.previewAnnotation, contains('Brightness.dark'));
}
Future<void> test_annotationSourceGeneration() async {
newFile(join(projectFolderPath, 'lib', 'previews.dart'), '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';
enum MyEnum { a, b }
class ComplexPreview extends Preview {
final List<int> list;
final Map<String, dynamic> map;
final MyEnum e;
final (int, {String s}) record;
final Size? size;
const ComplexPreview({
required super.name,
required this.list,
required this.map,
required this.e,
required this.record,
this.size,
});
}
@ComplexPreview(
name: 'Complex',
list: [1, 2, 3],
map: {'key': 'value', 'nested': [true, false]},
e: MyEnum.a,
record: (1, s: 'hello'),
size: Size(100, 200),
)
Widget complexPreview() => Text('Complex');
class CustomSize {
final double value;
const CustomSize.square(this.value);
}
class NamedConstructorPreview extends Preview {
final CustomSize size;
const NamedConstructorPreview({required super.name, required this.size});
}
@NamedConstructorPreview(
name: 'Named Constructor',
size: CustomSize.square(150),
)
Widget namedConstructorPreview() => Text('Named');
''');
await initialize();
var result = await getFlutterWidgetPreviews(
Uri.file(join(projectFolderPath, 'lib', 'previews.dart')),
);
expect(result, isNotNull);
expect(result!.previews, hasLength(2));
var complex = result.previews.firstWhere(
(p) => p.functionName == 'complexPreview',
);
var source = complex.previewAnnotation;
// Validate that namespaces/prefixes are applied (e.g., _i1.ComplexPreview)
// and that nested structures are correctly formatted.
expect(source, contains("name: 'Complex'"));
expect(source, contains('list: [1, 2, 3]'));
expect(source, contains("map: {'key': 'value', 'nested': [true, false]}"));
expect(source, contains('MyEnum.a'));
expect(source, contains("record: (1, s: 'hello')"));
expect(source, contains('Size(100.0, 200.0)'));
var named = result.previews.firstWhere(
(p) => p.functionName == 'namedConstructorPreview',
);
expect(named.previewAnnotation, contains('CustomSize.square(150.0)'));
}
Future<void> test_customPreviewTypes() async {
newFile(join(projectFolderPath, 'lib', 'previews.dart'), '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';
class MyPreview extends Preview {
final String customAttribute;
const MyPreview({required String name, required this.customAttribute}) : super(name: name);
}
@MyPreview(name: 'Custom', customAttribute: 'Some Value')
Widget customPreview() => Text('Custom');
''');
await initialize();
var result = await getFlutterWidgetPreviews(
Uri.file(join(projectFolderPath, 'lib', 'previews.dart')),
);
expect(result, isNotNull);
expect(result!.previews, hasLength(1));
var preview = result.previews.first;
expect(preview.functionName, 'customPreview');
expect(preview.previewAnnotation, contains("name: 'Custom'"));
expect(
preview.previewAnnotation,
contains("customAttribute: 'Some Value'"),
);
}
Future<void> test_errorsAndPropagation() async {
var depPath = join(projectFolderPath, 'lib', 'dep.dart');
var mainPath = join(projectFolderPath, 'lib', 'main.dart');
var mainUri = Uri.file(mainPath);
newFile(depPath, 'int x = "not an int"; // Error');
newFile(mainPath, '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';
import 'dep.dart';
@Preview(name: 'Has Dep Error')
Widget preview() => Text(x.toString());
''');
await initialize();
var result = await getFlutterWidgetPreviews(mainUri);
expect(result!.previews, hasLength(1));
var preview = result.previews.first;
expect(preview.hasError, isFalse);
expect(preview.dependencyHasErrors, isTrue);
// Fix error in dep
newFile(depPath, 'int x = 1;');
result = await getFlutterWidgetPreviews(mainUri);
expect(result!.previews.first.dependencyHasErrors, isFalse);
// Add error to main
newFile(mainPath, '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';
import 'dep.dart';
@Preview(name: 'Has Local Error')
Widget preview() => Text(x.toString()) // Missing semicolon
''');
result = await getFlutterWidgetPreviews(mainUri);
expect(result!.previews.first.hasError, isTrue);
expect(result.previews.first.dependencyHasErrors, isFalse);
}
Future<void> test_parts() async {
var mainPath = join(projectFolderPath, 'lib', 'main.dart');
var partPath = join(projectFolderPath, 'lib', 'part.dart');
var mainUri = Uri.file(mainPath);
newFile(mainPath, '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';
part 'part.dart';
@Preview(name: 'Main Preview')
Widget mainPreview() => Text('Main');
''');
newFile(partPath, '''
part of 'main.dart';
@Preview(name: 'Part Preview')
Widget partPreview() => Text('Part');
''');
await initialize();
var result = await getFlutterWidgetPreviews(mainUri);
expect(result!.previews, hasLength(2));
expect(result.previews.any((p) => p.functionName == 'mainPreview'), isTrue);
expect(result.previews.any((p) => p.functionName == 'partPreview'), isTrue);
// Use package: URI as observed in the Actual results.
expect(result.scriptUris.first.toString(), 'package:test/main.dart');
}
Future<void> test_pubWorkspace() async {
// Setup a workspace with two packages
newFile(join(projectFolderPath, 'pubspec.yaml'), '''
workspace:
- pkgs/a
- pkgs/b
''');
newFile(join(projectFolderPath, 'pkgs', 'a', 'pubspec.yaml'), '''
name: a
environment:
sdk: ^3.7.0
dependencies:
flutter:
sdk: flutter
''');
newFile(join(projectFolderPath, 'pkgs', 'b', 'pubspec.yaml'), '''
name: b
environment:
sdk: ^3.7.0
dependencies:
flutter:
sdk: flutter
''');
newFile(join(projectFolderPath, 'pkgs', 'a', 'lib', 'a.dart'), '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';
@Preview(name: 'Pkg A')
Widget a() => Text('A');
''');
newFile(join(projectFolderPath, 'pkgs', 'b', 'lib', 'b.dart'), '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';
@Preview(name: 'Pkg B')
Widget b() => Text('B');
''');
var config = PackageConfigFileBuilder();
// Do NOT add 'test' package here as writeTestPackageConfig will add it.
config.add(name: 'a', rootPath: join(projectFolderPath, 'pkgs', 'a'));
config.add(name: 'b', rootPath: join(projectFolderPath, 'pkgs', 'b'));
writeTestPackageConfig(config: config, flutter: true);
await initialize();
var result = await getWorkspaceFlutterWidgetPreviews();
expect(result!.previews, hasLength(2));
expect(result.previews.any((p) => p.packageName == 'a'), isTrue);
expect(result.previews.any((p) => p.packageName == 'b'), isTrue);
}
Future<void> test_workspacePreviews() async {
newFile(join(projectFolderPath, 'lib', 'a.dart'), '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';
@Preview(name: 'A')
Widget a() => Text('A');
''');
newFile(join(projectFolderPath, 'lib', 'b.dart'), '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';
@Preview(name: 'B')
Widget b() => Text('B');
''');
await initialize();
var result = await getWorkspaceFlutterWidgetPreviews();
expect(result!.previews, hasLength(2));
expect(result.previews.any((p) => p.functionName == 'a'), isTrue);
expect(result.previews.any((p) => p.functionName == 'b'), isTrue);
}
}
@@ -32,6 +32,8 @@ import 'document_symbols_test.dart' as document_symbols;
import 'edit_argument_test.dart' as edit_argument;
import 'editable_arguments_test.dart' as editable_arguments;
import 'error_or_test.dart' as error_or;
import 'exhaustive_flutter_widget_preview_test.dart'
as exhaustive_flutter_widget_preview;
import 'file_modification_test.dart' as file_modification;
import 'flutter_outline_test.dart' as flutter_outline;
import 'folding_test.dart' as folding;
@@ -96,6 +98,7 @@ void main() {
edit_argument.main();
editable_arguments.main();
error_or.main();
exhaustive_flutter_widget_preview.main();
file_modification.main();
flutter_outline.main();
folding.main();
@@ -12,13 +12,20 @@ import 'package:analyzer_testing/utilities/extensions/resource_provider.dart';
/// A mixin adding functionality to write `.dart_tool/package_config.json`
/// files along with mock packages to a [ResourceProvider].
mixin ConfigurationFilesMixin on MockPackagesMixin {
/// Adds the 'flutter_localizations' package to the package config file for
/// the package-under-test.
///
/// This allows `package:flutter_localizations/flutter_localizations.dart`
/// imports to resolve.
bool get addFlutterLocalizationsPackageDep => false;
/// Adds the 'flutter_test' package to the package config file for the
/// package-under-test.
///
/// This allows `package:flutter_test/flutter_test.dart` imports to resolve.
bool get addFlutterTestPackageDep => false;
/// Adds the 'pedantic' package to the package config file for the
/// Adds the 'vector_math' package to the package config file for the
/// package-under-test.
///
/// This allows `package:vector_math/vector_math_64.dart` imports to resolve.
@@ -279,6 +279,22 @@ Notifies the client when Flutter outline information is available (or updated) f
Nodes contains multiple ranges as described for the `dart/textDocument/publishOutline` notification.
### dart/textDocument/getFlutterWidgetPreviews Method
Direction: Client -> Server
Params: `TextDocumentIdentifier`
Returns: `FlutterWidgetPreviews | null`
Returns the set of detected Flutter Widget Previews in the provided document or null if the document doesn't exist.
### dart/workspace/getFlutterWidgetPreviews Method
Direction: Client -> Server
Params: None
Returns: `FlutterWidgetPreviews | null`
Returns the set of detected Flutter Widget Previews in the analyzed project.
### dart/openUri Notification
Direction: Server -> Client
@@ -244,7 +244,7 @@ bool _isOverride(Interface interface, Field field) {
}
bool _isSimpleType(TypeBase type) {
const literals = ['num', 'String', 'bool', 'int'];
const literals = ['num', 'String', 'bool', 'int', 'double'];
return type is TypeReference && literals.contains(type.dartType);
}
@@ -1153,6 +1153,8 @@ void _writeToJsonCode(
buffer.write('$valueCode$nullOp.toJson()');
} else if (_isUriType(type)) {
buffer.write('$valueCode$nullOp.toString()');
} else if (type is ArrayType && _isUriType(type.elementType)) {
buffer.write('$valueCode$nullOp.map((uri) => uri.toString()).toList()');
} else {
buffer.write(valueCode);
}
@@ -343,6 +343,110 @@ List<LspEntity> getCustomClasses() {
field('label', type: 'string'),
field('valueRange', type: 'Range', canBeUndefined: true),
]),
interface(
'FlutterWidgetPreviews',
[
field(
'scriptUris',
array: true,
type: 'Uri',
comment: 'The URIs for the updated scripts.',
),
Field(
name: 'namespaces',
type: MapType(TypeReference.string, TypeReference.string),
allowsNull: false,
allowsUndefined: false,
comment:
'A set of library URIs and the prefixes used for types in '
'"previewAnnotation" sources.',
),
field(
'previews',
type: 'FlutterWidgetPreviewDetails',
array: true,
comment: 'The current set of previews in the script.',
),
],
comment:
'The set of widget previews defined in a script of an analyzed '
'Flutter project.',
),
interface(
'FlutterWidgetPreviewDetails',
[
field(
'scriptUri',
type: 'Uri',
comment:
'The file:// URI pointing to the script in which the '
'preview is defined.',
),
field(
'position',
type: 'Position',
comment:
'The source location at which the Preview annotation was applied.',
),
field(
'packageName',
type: 'string',
canBeNull: true,
comment:
'The name of the package in which this annotated preview '
'function was defined.'
'\n\n For example, if this preview is defined in '
'"package:foo/src/bar.dart", this will have the value "foo".\n\n'
'This should only be null if the preview is defined in a file '
"that's not part of a Flutter package (e.g., is defined in a "
'test).',
),
field(
'functionName',
type: 'string',
comment: 'The name of the function returning the preview.',
),
field(
'isBuilder',
type: 'bool',
comment:
'Set to true if the preview function is returning a '
'`WidgetBuilder` instead of a `Widget`.',
),
field(
'previewAnnotation',
type: 'string',
comment:
'An equivalent Dart expression to the applied preview '
'annotation, with namespaces applied to individual types and '
'constant values evaluated.\n\nThis can be any object which '
'extends `Preview` or `MultiPreview`.',
),
field(
'isMultiPreview',
type: 'bool',
comment:
'Set to true if `previewAnnotation` represents a `MultiPreview`.',
),
field(
'hasError',
type: 'bool',
comment:
'Set to true if there is an error that will prevent this preview '
'from being rendered.',
),
field(
'dependencyHasErrors',
type: 'bool',
comment:
'Set to true if there is an error in a dependency that will '
'prevent this preview from being rendered.',
),
],
comment:
'A representation of a widget preview declaration containing all '
'information needed to import the preview into the widget previewer.',
),
interface(
// Used as a base class for all resolution data classes.
'CompletionItemResolutionInfo',
@@ -14,6 +14,8 @@ import 'package:analyzer_testing/src/mock_packages/flutter/cupertino.dart'
as mock_flutter_cupertino;
import 'package:analyzer_testing/src/mock_packages/flutter/foundation.dart'
as mock_flutter_foundation;
import 'package:analyzer_testing/src/mock_packages/flutter_localizations/flutter_localizations.dart'
as mock_flutter_localizations;
import 'package:analyzer_testing/src/mock_packages/flutter/material.dart'
as mock_flutter_material;
import 'package:analyzer_testing/src/mock_packages/flutter/painting.dart'
@@ -102,6 +104,7 @@ mixin MockPackagesMixin {
...mock_flutter_animation.units,
...mock_flutter_cupertino.units,
...mock_flutter_foundation.units,
...mock_flutter_localizations.units,
...mock_flutter_material.units,
...mock_flutter_painting.units,
...mock_flutter_rendering.units,
@@ -11,4 +11,5 @@ final List<MockLibraryUnit> units = [cupertinoLibrary, cupertinoColorsLibrary];
final cupertinoLibrary = MockLibraryUnit('lib/cupertino.dart', r'''
export 'src/cupertino/colors.dart';
export 'src/cupertino/theme.dart';
''');
@@ -0,0 +1,18 @@
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:analyzer_testing/src/mock_packages/mock_library.dart';
final cupertinoThemeLibrary = MockLibraryUnit(
'lib/src/cupertino/theme.dart',
r'''
class NoDefaultCupertinoThemeData {
const NoDefaultCupertinoThemeData({Color? primaryColor});
}
class CupertinoThemeData extends NoDefaultCupertinoThemeData {
const CupertinoThemeData({super.primaryColor});
}
''',
);
@@ -25,9 +25,11 @@ final List<MockLibraryUnit> units = [
final materialLibrary = MockLibraryUnit('lib/material.dart', r'''
export 'src/material/app_bar.dart';
export 'src/material/button.dart';
export 'src/material/color_scheme.dart';
export 'src/material/colors.dart';
export 'src/material/icons.dart';
export 'src/material/ink_well.dart';
export 'src/material/scaffold.dart';
export 'src/material/theme_data.dart';
export 'widgets.dart';
''');
@@ -0,0 +1,20 @@
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:analyzer_testing/src/mock_packages/mock_library.dart';
final materialColorsLibrary = MockLibraryUnit(
'lib/src/material/color_scheme.dart',
r'''
class ColorScheme {
const ColorScheme.light({
Color primary = const Color(0xff6200ee)
});
const ColorScheme.dark({
Color primary = const Color(0xff6200ee)
});
}
''',
);
@@ -0,0 +1,16 @@
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:analyzer_testing/src/mock_packages/mock_library.dart';
final materialThemeDataLibrary = MockLibraryUnit(
'lib/src/material/theme_data.dart',
'''
import 'color_scheme.dart';
class ThemeData {
const ThemeData({ColorScheme? colorScheme});
}
''',
);
@@ -7,30 +7,163 @@ import 'package:analyzer_testing/src/mock_packages/mock_library.dart';
final widgetPreviewsWidgetPreviewsLibrary = MockLibraryUnit(
'lib/src/widget_previews/widget_previews.dart',
r'''
import 'package:flutter/material.dart' show Brightness;
import 'package:flutter/cupertino.dart' show CupertinoThemeData;
import 'package:flutter/material.dart' show Brightness, ThemeData;
import 'package:flutter/widgets.dart';
base class Preview {
const Preview({
this.name,
String group = 'Default',
String? name,
Size? size,
this.textScaleFactor,
this.wrapper,
this.theme,
this.brightness,
double? textScaleFactor,
WidgetWrapper? wrapper,
PreviewTheme? theme,
Brightness? brightness,
PreviewLocalizations? localizations,
}) : this._required(
group: group,
name: name,
size: size,
textScaleFactor: textScaleFactor,
wrapper: wrapper,
theme: theme,
brightness: brightness,
localizations: localizations,
);
const Preview._required({
required this.group,
required this.name,
required this.size,
required this.textScaleFactor,
required this.wrapper,
required this.theme,
required this.brightness,
required this.localizations,
});
final String group;
final String? name;
final Size? size;
final double? textScaleFactor;
final Widget Function(Widget)? wrapper;
final WidgetWrapper? wrapper;
final PreviewThemeData Function()? theme;
final PreviewTheme? theme;
final Brightness? brightness;
final PreviewLocalizations? localizations;
@mustCallSuper
Preview transform() => this;
PreviewBuilder toBuilder() => PreviewBuilder._fromPreview(this);
}
base class PreviewThemeData {}
abstract base class MultiPreview {
const MultiPreview();
List<Preview> get previews;
@mustCallSuper
List<Preview> transform() => previews.map((Preview e) => e.transform()).toList();
}
final class PreviewBuilder {
PreviewBuilder();
PreviewBuilder._fromPreview(Preview preview)
: group = preview.group,
name = preview.name,
size = preview.size,
textScaleFactor = preview.textScaleFactor,
wrapper = preview.wrapper,
theme = preview.theme,
brightness = preview.brightness,
localizations = preview.localizations;
String? group;
String? name;
Size? size;
double? textScaleFactor;
WidgetWrapper? wrapper;
void addWrapper(WidgetWrapper newWrapper) {
final WidgetWrapper? wrapperLocal = wrapper;
if (wrapperLocal != null) {
wrapper = (Widget widget) => newWrapper(wrapperLocal(widget));
return;
}
wrapper = newWrapper;
}
PreviewTheme? theme;
Brightness? brightness;
PreviewLocalizations? localizations;
Preview build() {
return Preview._required(
group: group ?? 'Default',
name: name,
size: size,
textScaleFactor: textScaleFactor,
wrapper: wrapper,
theme: theme,
brightness: brightness,
localizations: localizations,
);
}
}
base class PreviewLocalizationsData {
const PreviewLocalizationsData({
this.locale,
this.supportedLocales = const <Locale>[Locale('en', 'US')],
this.localizationsDelegates,
this.localeListResolutionCallback,
this.localeResolutionCallback,
});
final Locale? locale;
final Iterable<LocalizationsDelegate<Object?>>? localizationsDelegates;
final LocaleListResolutionCallback? localeListResolutionCallback;
final LocaleResolutionCallback? localeResolutionCallback;
}
base class PreviewThemeData {
const PreviewThemeData({
this.materialLight,
this.materialDark,
this.cupertinoLight,
this.cupertinoDark,
});
final ThemeData? materialLight;
final ThemeData? materialDark;
final CupertinoThemeData? cupertinoLight;
final CupertinoThemeData? cupertinoDark;
(ThemeData?, CupertinoThemeData?) themeForBrightness(Brightness brightness) {
if (brightness == Brightness.light) {
return (materialLight, cupertinoLight);
}
return (materialDark, cupertinoDark);
}
}
''',
);
@@ -52,6 +52,7 @@ final widgetsLibrary = MockLibraryUnit('lib/widgets.dart', r'''
export 'package:vector_math/vector_math.dart';
export 'foundation.dart' show UniqueKey;
export 'src/widgets/app.dart';
export 'src/widgets/async.dart';
export 'src/widgets/basic.dart';
export 'src/widgets/container.dart';
@@ -0,0 +1,15 @@
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:analyzer_testing/src/mock_packages/mock_library.dart';
final widgetsBasicLibrary = MockLibraryUnit('lib/src/widgets/app.dart', r'''
export 'dart:ui' show Locale;
typedef LocaleListResolutionCallback =
Locale? Function(List<Locale>? locales, Iterable<Locale> supportedLocales);
typedef LocaleResolutionCallback =
Locale? Function(Locale? locale, Iterable<Locale> supportedLocales);
''');
@@ -0,0 +1,36 @@
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:analyzer_testing/src/mock_packages/mock_library.dart';
/// The set of compilation units that make up the mock 'flutter_localizations' package.
final List<MockLibraryUnit> units = [_flutterLocalizationsUnit];
final _flutterLocalizationsUnit = MockLibraryUnit(
'lib/flutter_localizations.dart',
r'''
library flutter_localizations;
class LocalizationsDelegate<T> {
const LocalizationsDelegate();
}
abstract class WidgetsLocalizations {}
abstract class GlobalWidgetsLocalizations implements WidgetsLocalizations {
static const LocalizationsDelegate<WidgetsLocalizations> delegate =
LocalizationsDelegate<WidgetsLocalizations>();
}
abstract class GlobalMaterialLocalizations implements WidgetsLocalizations {
static const LocalizationsDelegate<WidgetsLocalizations> delegate =
LocalizationsDelegate<WidgetsLocalizations>();
}
abstract class GlobalCupertinoLocalizations implements WidgetsLocalizations {
static const LocalizationsDelegate<WidgetsLocalizations> delegate =
LocalizationsDelegate<WidgetsLocalizations>();
}
''',
);
@@ -238,4 +238,13 @@ enum Clip { none, hardEdge, antiAlias, antiAliasWithSaveLayer }
class TextHeightBehavior {}
typedef VoidCallback = void Function();
enum Brightness {
dark,
light,
}
class Locale {
Locale(String languageCode, [String? countryCode]);
}
''');
@@ -343,6 +343,34 @@ bool _canParseListFlutterOutlineAttribute(
return true;
}
bool _canParseListFlutterWidgetPreviewDetails(
Map<String, Object?> map, LspJsonReporter reporter, String fieldName,
{required bool allowsUndefined, required bool allowsNull}) {
reporter.push(fieldName);
try {
if (!allowsUndefined && !map.containsKey(fieldName)) {
reporter.reportError('must not be undefined');
return false;
}
final value = map[fieldName];
final nullCheck = allowsNull || allowsUndefined;
if (!nullCheck && value == null) {
reporter.reportError('must not be null');
return false;
}
if ((!nullCheck || value != null) &&
(value is! List<Object?> ||
value.any((item) =>
!FlutterWidgetPreviewDetails.canParse(item, reporter)))) {
reporter.reportError('must be of type List<FlutterWidgetPreviewDetails>');
return false;
}
} finally {
reporter.pop();
}
return true;
}
bool _canParseListOutline(
Map<String, Object?> map, LspJsonReporter reporter, String fieldName,
{required bool allowsUndefined, required bool allowsNull}) {
@@ -396,6 +424,34 @@ bool _canParseListString(
return true;
}
bool _canParseListUri(
Map<String, Object?> map, LspJsonReporter reporter, String fieldName,
{required bool allowsUndefined, required bool allowsNull}) {
reporter.push(fieldName);
try {
if (!allowsUndefined && !map.containsKey(fieldName)) {
reporter.reportError('must not be undefined');
return false;
}
final value = map[fieldName];
final nullCheck = allowsNull || allowsUndefined;
if (!nullCheck && value == null) {
reporter.reportError('must not be null');
return false;
}
if ((!nullCheck || value != null) &&
(value is! List<Object?> ||
value.any(
(item) => (item is! String || Uri.tryParse(item) == null)))) {
reporter.reportError('must be of type List<Uri>');
return false;
}
} finally {
reporter.pop();
}
return true;
}
bool _canParseLiteral(
Map<String, Object?> map, LspJsonReporter reporter, String fieldName,
{required bool allowsUndefined,
@@ -454,6 +510,35 @@ bool _canParseMapStringListString(
return true;
}
bool _canParseMapStringString(
Map<String, Object?> map, LspJsonReporter reporter, String fieldName,
{required bool allowsUndefined, required bool allowsNull}) {
reporter.push(fieldName);
try {
if (!allowsUndefined && !map.containsKey(fieldName)) {
reporter.reportError('must not be undefined');
return false;
}
final value = map[fieldName];
final nullCheck = allowsNull || allowsUndefined;
if (!nullCheck && value == null) {
reporter.reportError('must not be null');
return false;
}
if ((!nullCheck || value != null) &&
(value is! Map ||
(value.keys.any((item) =>
item is! String ||
value.values.any((item) => item is! String))))) {
reporter.reportError('must be of type Map<String, String>');
return false;
}
} finally {
reporter.pop();
}
return true;
}
bool _canParseMethod(
Map<String, Object?> map, LspJsonReporter reporter, String fieldName,
{required bool allowsUndefined, required bool allowsNull}) {
@@ -2081,6 +2166,274 @@ class FlutterOutlineAttribute implements ToJsonable {
}
}
/// A representation of a widget preview declaration containing all information
/// needed to import the preview into the widget previewer.
class FlutterWidgetPreviewDetails implements ToJsonable {
static const jsonHandler = LspJsonHandler(
FlutterWidgetPreviewDetails.canParse,
FlutterWidgetPreviewDetails.fromJson,
);
/// Set to true if there is an error in a dependency that will prevent this
/// preview from being rendered.
final bool dependencyHasErrors;
/// The name of the function returning the preview.
final String functionName;
/// Set to true if there is an error that will prevent this preview from being
/// rendered.
final bool hasError;
/// Set to true if the preview function is returning a `WidgetBuilder` instead
/// of a `Widget`.
final bool isBuilder;
/// Set to true if `previewAnnotation` represents a `MultiPreview`.
final bool isMultiPreview;
/// The name of the package in which this annotated preview function was
/// defined.
///
/// For example, if this preview is defined in "package:foo/src/bar.dart",
/// this will have the value "foo".
///
/// This should only be null if the preview is defined in a file that's not
/// part of a Flutter package (e.g., is defined in a test).
final String? packageName;
/// The source location at which the Preview annotation was applied.
final Position position;
/// An equivalent Dart expression to the applied preview annotation, with
/// namespaces applied to individual types and constant values evaluated.
///
/// This can be any object which extends `Preview` or `MultiPreview`.
final String previewAnnotation;
/// The file:// URI pointing to the script in which the preview is defined.
final Uri scriptUri;
FlutterWidgetPreviewDetails({
required this.dependencyHasErrors,
required this.functionName,
required this.hasError,
required this.isBuilder,
required this.isMultiPreview,
this.packageName,
required this.position,
required this.previewAnnotation,
required this.scriptUri,
});
@override
int get hashCode => Object.hash(
dependencyHasErrors,
functionName,
hasError,
isBuilder,
isMultiPreview,
packageName,
position,
previewAnnotation,
scriptUri,
);
@override
bool operator ==(Object other) {
return other is FlutterWidgetPreviewDetails &&
other.runtimeType == FlutterWidgetPreviewDetails &&
dependencyHasErrors == other.dependencyHasErrors &&
functionName == other.functionName &&
hasError == other.hasError &&
isBuilder == other.isBuilder &&
isMultiPreview == other.isMultiPreview &&
packageName == other.packageName &&
position == other.position &&
previewAnnotation == other.previewAnnotation &&
scriptUri == other.scriptUri;
}
@override
Map<String, Object?> toJson() {
var result = <String, Object?>{};
result['dependencyHasErrors'] = dependencyHasErrors;
result['functionName'] = functionName;
result['hasError'] = hasError;
result['isBuilder'] = isBuilder;
result['isMultiPreview'] = isMultiPreview;
result['packageName'] = packageName;
result['position'] = position.toJson();
result['previewAnnotation'] = previewAnnotation;
result['scriptUri'] = scriptUri.toString();
return result;
}
@override
String toString() => jsonEncoder.convert(toJson());
static bool canParse(Object? obj, LspJsonReporter reporter) {
if (obj is Map<String, Object?>) {
if (!_canParseBool(obj, reporter, 'dependencyHasErrors',
allowsUndefined: false, allowsNull: false)) {
return false;
}
if (!_canParseString(obj, reporter, 'functionName',
allowsUndefined: false, allowsNull: false)) {
return false;
}
if (!_canParseBool(obj, reporter, 'hasError',
allowsUndefined: false, allowsNull: false)) {
return false;
}
if (!_canParseBool(obj, reporter, 'isBuilder',
allowsUndefined: false, allowsNull: false)) {
return false;
}
if (!_canParseBool(obj, reporter, 'isMultiPreview',
allowsUndefined: false, allowsNull: false)) {
return false;
}
if (!_canParseString(obj, reporter, 'packageName',
allowsUndefined: false, allowsNull: true)) {
return false;
}
if (!_canParsePosition(obj, reporter, 'position',
allowsUndefined: false, allowsNull: false)) {
return false;
}
if (!_canParseString(obj, reporter, 'previewAnnotation',
allowsUndefined: false, allowsNull: false)) {
return false;
}
return _canParseUri(obj, reporter, 'scriptUri',
allowsUndefined: false, allowsNull: false);
} else {
reporter.reportError('must be of type FlutterWidgetPreviewDetails');
return false;
}
}
static FlutterWidgetPreviewDetails fromJson(Map<String, Object?> json) {
final dependencyHasErrorsJson = json['dependencyHasErrors'];
final dependencyHasErrors = dependencyHasErrorsJson as bool;
final functionNameJson = json['functionName'];
final functionName = functionNameJson as String;
final hasErrorJson = json['hasError'];
final hasError = hasErrorJson as bool;
final isBuilderJson = json['isBuilder'];
final isBuilder = isBuilderJson as bool;
final isMultiPreviewJson = json['isMultiPreview'];
final isMultiPreview = isMultiPreviewJson as bool;
final packageNameJson = json['packageName'];
final packageName = packageNameJson as String?;
final positionJson = json['position'];
final position = Position.fromJson(positionJson as Map<String, Object?>);
final previewAnnotationJson = json['previewAnnotation'];
final previewAnnotation = previewAnnotationJson as String;
final scriptUriJson = json['scriptUri'];
final scriptUri = Uri.parse(scriptUriJson as String);
return FlutterWidgetPreviewDetails(
dependencyHasErrors: dependencyHasErrors,
functionName: functionName,
hasError: hasError,
isBuilder: isBuilder,
isMultiPreview: isMultiPreview,
packageName: packageName,
position: position,
previewAnnotation: previewAnnotation,
scriptUri: scriptUri,
);
}
}
/// The set of widget previews defined in a script of an analyzed Flutter
/// project.
class FlutterWidgetPreviews implements ToJsonable {
static const jsonHandler = LspJsonHandler(
FlutterWidgetPreviews.canParse,
FlutterWidgetPreviews.fromJson,
);
/// A set of library URIs and the prefixes used for types in
/// "previewAnnotation" sources.
final Map<String, String> namespaces;
/// The current set of previews in the script.
final List<FlutterWidgetPreviewDetails> previews;
/// The URIs for the updated scripts.
final List<Uri> scriptUris;
FlutterWidgetPreviews({
required this.namespaces,
required this.previews,
required this.scriptUris,
});
@override
int get hashCode => Object.hash(
lspHashCode(namespaces),
lspHashCode(previews),
lspHashCode(scriptUris),
);
@override
bool operator ==(Object other) {
return other is FlutterWidgetPreviews &&
other.runtimeType == FlutterWidgetPreviews &&
const DeepCollectionEquality().equals(namespaces, other.namespaces) &&
const DeepCollectionEquality().equals(previews, other.previews) &&
const DeepCollectionEquality().equals(scriptUris, other.scriptUris);
}
@override
Map<String, Object?> toJson() {
var result = <String, Object?>{};
result['namespaces'] = namespaces;
result['previews'] = previews.map((item) => item.toJson()).toList();
result['scriptUris'] = scriptUris.map((uri) => uri.toString()).toList();
return result;
}
@override
String toString() => jsonEncoder.convert(toJson());
static bool canParse(Object? obj, LspJsonReporter reporter) {
if (obj is Map<String, Object?>) {
if (!_canParseMapStringString(obj, reporter, 'namespaces',
allowsUndefined: false, allowsNull: false)) {
return false;
}
if (!_canParseListFlutterWidgetPreviewDetails(obj, reporter, 'previews',
allowsUndefined: false, allowsNull: false)) {
return false;
}
return _canParseListUri(obj, reporter, 'scriptUris',
allowsUndefined: false, allowsNull: false);
} else {
reporter.reportError('must be of type FlutterWidgetPreviews');
return false;
}
}
static FlutterWidgetPreviews fromJson(Map<String, Object?> json) {
final namespacesJson = json['namespaces'];
final namespaces = (namespacesJson as Map<Object, Object?>)
.map((key, value) => MapEntry(key as String, value as String));
final previewsJson = json['previews'];
final previews = (previewsJson as List<Object?>)
.map((item) =>
FlutterWidgetPreviewDetails.fromJson(item as Map<String, Object?>))
.toList();
final scriptUrisJson = json['scriptUris'];
final scriptUris = (scriptUrisJson as List<Object?>)
.map((item) => Uri.parse(item as String))
.toList();
return FlutterWidgetPreviews(
namespaces: namespaces,
previews: previews,
scriptUris: scriptUris,
);
}
}
class IncomingMessage implements Message, ToJsonable {
static const jsonHandler = LspJsonHandler(
IncomingMessage.canParse,