blob: 2eaf8d292b67f1551a7f3bde90963f1a8af31c37 [file]
import 'dart:async';
import 'dart:js_interop';
import 'package:codemirror_dart/codemirror_dart.dart' show LSPCodeAction;
import 'package:dartpad_editor/dartpad_editor.dart';
import 'package:jaspr/dom.dart';
import 'package:jaspr/jaspr.dart';
import 'package:web/web.dart' as web;
import '../../../utils/styles.dart';
class CodeActionPanel extends StatefulComponent {
const CodeActionPanel({required this.codeActionsController, super.key});
final CodeActionsController codeActionsController;
@override
State<CodeActionPanel> createState() => _CodeActionPanelState();
@css
static List<StyleRule> get styles => _CodeActionPanelState.styles;
}
class _CodeActionPanelState extends State<CodeActionPanel> {
final GlobalNodeKey panelKey = GlobalNodeKey();
StreamSubscription<web.MouseEvent>? _clickSubscription;
@override
void initState() {
super.initState();
Timer.run(() {
if (!mounted) {
return;
}
_clickSubscription = web.EventStreamProviders.mouseDownEvent.forTarget(web.document).listen((event) {
final element = panelKey.currentNode;
final target = event.target as web.Node?;
if (element != null && target != null) {
if (!element.contains(target)) {
component.codeActionsController.hideCodeActionPanel();
}
}
});
});
}
@override
void dispose() {
_clickSubscription?.cancel();
super.dispose();
}
Map<String, List<LSPCodeAction>> _groupActions(List<LSPCodeAction> actions) {
final Map<String, List<LSPCodeAction>> groups = {
'Quick Fix': [],
'Refactor': [],
'Inline': [],
'More Actions': [],
};
for (final action in actions) {
final kind = action.kind?.toDart;
if (kind == null) {
groups['More Actions']!.add(action);
} else if (kind.startsWith('quickfix')) {
groups['Quick Fix']!.add(action);
} else if (kind.startsWith('refactor.inline')) {
groups['Inline']!.add(action);
} else if (kind.startsWith('refactor')) {
groups['Refactor']!.add(action);
} else {
groups['More Actions']!.add(action);
}
}
return groups;
}
@override
Component build(BuildContext context) {
final actions = component.codeActionsController.codeActions;
final left = component.codeActionsController.panelLeft;
final top = component.codeActionsController.panelTop;
final List<Component> children = [];
if (actions == null) {
children.add(
const div(classes: 'code-action-empty-item', [
.text('Loading code actions...'),
]),
);
} else if (actions.isEmpty) {
children.add(
const div(classes: 'code-action-empty-item', [
.text('No code actions available'),
]),
);
} else {
final grouped = _groupActions(actions);
bool isFirstGroup = true;
for (final entry in grouped.entries) {
final groupName = entry.key;
final groupActions = entry.value;
if (groupActions.isNotEmpty) {
if (!isFirstGroup) {
children.add(const div(classes: 'code-action-divider', []));
}
isFirstGroup = false;
children.add(
div(classes: 'code-action-group-header', [
.text(groupName),
]),
);
children.add(
div(classes: 'code-action-group-list', [
for (final action in groupActions)
button(
classes: 'code-action-btn',
type: .button,
onClick: () => component.codeActionsController.applyCodeAction(action),
[.text(action.title.toDart)],
),
]),
);
}
}
}
return div(
key: panelKey,
classes: 'code-action-floating-panel',
styles: Styles(
raw: {
'left': '${left}px',
'top': '${top}px',
},
),
children,
);
}
static List<StyleRule> get styles => [
css('.code-action-floating-panel').styles(
display: .flex,
position: const .fixed(),
zIndex: const ZIndex(100),
minWidth: 180.px,
maxWidth: 320.px,
border: .all(color: const .rgba(255, 255, 255, 0.08), width: 1.px),
radius: .circular(8.px),
shadow: BoxShadow(
offsetX: .zero,
offsetY: 8.px,
blur: 24.px,
color: const .rgba(0, 0, 0, 0.5),
),
flexDirection: .column,
color: colorOnSurface,
fontFamily: const .list([FontFamily('Inter'), FontFamilies.sansSerif]),
fontSize: 12.px,
raw: {
'background-color': 'rgba(30, 30, 30, 0.9)',
'backdrop-filter': 'blur(10px)',
'-webkit-backdrop-filter': 'blur(10px)',
},
),
css('.code-action-group-header').styles(
padding: .only(top: 8.px, right: 12.px, bottom: 4.px, left: 12.px),
color: colorOnSurfaceVariant,
fontSize: 10.px,
fontWeight: .w700,
raw: {
'text-transform': 'uppercase',
'letter-spacing': '0.5px',
},
),
css('.code-action-group-list').styles(
display: .flex,
flexDirection: .column,
),
css('.code-action-btn').styles(
padding: .symmetric(vertical: 6.px, horizontal: 12.px),
border: .none,
cursor: .pointer,
transition: Transition('background-color', duration: 100.ms),
color: colorOnSurface,
textAlign: .left,
fontSize: 12.px,
backgroundColor: Colors.transparent,
),
css('.code-action-btn:hover').styles(
backgroundColor: const .rgba(255, 255, 255, 0.08),
),
css('.code-action-divider').styles(
height: 1.px,
margin: .symmetric(vertical: 4.px),
backgroundColor: const .rgba(255, 255, 255, 0.08),
),
css('.code-action-empty-item').styles(
padding: .symmetric(vertical: 12.px, horizontal: 16.px),
color: colorOnSurfaceVariant,
textAlign: .center,
),
];
}