Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

import '../../shared/base.dart';
import '../../shared/common_operations.dart';
import '../../shared/document_test_operations.dart';
import '../../shared/expectation.dart';
import '../document/document_codeblock_paste_test.dart';

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

testWidgets('Code Block Language Selector Test', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await _initializeCodeBlockTest(tester);

/// create a new document
await tester.createNewPageWithNameUnderParent();
await tester.tapNewPageButton();

/// tap editor to get focus
await tester.tapButton(find.byType(AppFlowyEditor));
await tester.editor.tapLineOfEditorAt(0);

expect(find.byType(CodeBlockLanguageSelector), findsNothing);
await insertCodeBlockInDocument(tester);
Expand Down Expand Up @@ -83,6 +87,182 @@ void main() {
.toString();
expect(language.toLowerCase(), 'rust');
});

testWidgets('Code blocks wrap independently and persist their state', (
tester,
) async {
await _initializeCodeBlockTest(tester);

const documentName = 'Code block wrap test';
const otherDocumentName = 'Other document';
final longText = List.filled(500, 'x').join();
final longLine = 'final value = "$longText";';

await tester.createNewPageWithNameUnderParent(name: documentName);
await tester.tapButton(find.byType(AppFlowyEditor));
await insertCodeBlockInDocument(tester);
tester.testTextInput.enterText(longLine);
await tester.pumpAndSettle();

var editorState = tester.editor.getCurrentEditorState();
await editorState.apply(
editorState.transaction
..insertNode(
[1],
codeBlockNode(delta: Delta()..insert(longLine)),
),
);
await tester.pumpAndSettle();

var codeBlocks = find.byType(CodeBlockComponentWidget);
expect(codeBlocks, findsNWidgets(2));
expect(
editorState
.getNodeAtPath([0])!.attributes[AppFlowyCodeBlockKeys.wrapLines],
isNull,
);
expect(
tester
.widget<CodeBlockComponentWidget>(codeBlocks.first)
.style
?.wrapLines,
isFalse,
);
expect(
find.descendant(
of: codeBlocks.first,
matching: find.byType(SingleChildScrollView),
),
findsOneWidget,
);
final unwrappedHeight = tester.getSize(codeBlocks.first).height;

await tester.hoverOnWidget(codeBlocks.first);
final firstWrapButton = find.descendant(
of: codeBlocks.first,
matching: find.byKey(codeBlockWrapButtonKey),
);
expect(firstWrapButton, findsOneWidget);
expect(
tester.widget<FlowyIconButton>(firstWrapButton).tooltipText,
LocaleKeys.document_codeBlock_wrapTooltip.tr(),
);
expect(
tester.widget<FlowyIconButton>(firstWrapButton).isSelected,
isFalse,
);
await tester.tapButton(firstWrapButton);
await tester.pumpAndSettle();

codeBlocks = find.byType(CodeBlockComponentWidget);
expect(
editorState
.getNodeAtPath([0])!.attributes[AppFlowyCodeBlockKeys.wrapLines],
isTrue,
);
expect(
editorState
.getNodeAtPath([1])!.attributes[AppFlowyCodeBlockKeys.wrapLines],
isNull,
);
expect(
tester
.widget<CodeBlockComponentWidget>(codeBlocks.first)
.style
?.wrapLines,
isTrue,
);
expect(
tester.widget<FlowyIconButton>(firstWrapButton).isSelected,
isTrue,
);
expect(
tester
.widget<CodeBlockComponentWidget>(codeBlocks.at(1))
.style
?.wrapLines,
isFalse,
);
expect(
find.descendant(
of: codeBlocks.first,
matching: find.byType(SingleChildScrollView),
),
findsNothing,
);
final wrappedHeight = tester.getSize(codeBlocks.first).height;
expect(wrappedHeight, greaterThan(unwrappedHeight));

await tester.createNewPageWithNameUnderParent(name: otherDocumentName);
await tester.openPage(documentName);
await tester.pumpAndSettle();

editorState = tester.editor.getCurrentEditorState();
codeBlocks = find.byType(CodeBlockComponentWidget);
expect(codeBlocks, findsNWidgets(2));
expect(
editorState
.getNodeAtPath([0])!.attributes[AppFlowyCodeBlockKeys.wrapLines],
isTrue,
);
expect(
tester
.widget<CodeBlockComponentWidget>(codeBlocks.first)
.style
?.wrapLines,
isTrue,
);

await tester.hoverOnWidget(codeBlocks.first);
await tester.tapButton(
find.descendant(
of: codeBlocks.first,
matching: find.byKey(codeBlockWrapButtonKey),
),
);
await tester.pumpAndSettle();

codeBlocks = find.byType(CodeBlockComponentWidget);
expect(
editorState
.getNodeAtPath([0])!.attributes[AppFlowyCodeBlockKeys.wrapLines],
isFalse,
);
expect(
tester
.widget<CodeBlockComponentWidget>(codeBlocks.first)
.style
?.wrapLines,
isFalse,
);
expect(
tester
.widget<FlowyIconButton>(
find.descendant(
of: codeBlocks.first,
matching: find.byKey(codeBlockWrapButtonKey),
),
)
.isSelected,
isFalse,
);
expect(tester.getSize(codeBlocks.first).height, lessThan(wrappedHeight));
});
}

Future<void> _initializeCodeBlockTest(WidgetTester tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();

final gettingStartedPage = find.textContaining(
gettingStarted,
findRichText: true,
);
await tester.pumpUntilFound(
gettingStartedPage,
timeout: const Duration(seconds: 30),
);
expect(gettingStartedPage, findsAtLeastNWidgets(1));
}

Future<void> onKey(WidgetTester tester, LogicalKeyboardKey key) async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -878,12 +878,12 @@ MathEquationBlockComponentBuilder _buildMathEquationBlockComponentBuilder(
);
}

CodeBlockComponentBuilder _buildCodeBlockComponentBuilder(
AppFlowyCodeBlockComponentBuilder _buildCodeBlockComponentBuilder(
BuildContext context,
BlockComponentConfiguration configuration,
EditorStyleCustomizer styleCustomizer,
) {
return CodeBlockComponentBuilder(
return AppFlowyCodeBlockComponentBuilder(
styleBuilder: styleCustomizer.codeBlockStyleBuilder,
configuration: configuration,
padding: const EdgeInsets.only(left: 20, right: 30, bottom: 34),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';

class AppFlowyCodeBlockKeys {
const AppFlowyCodeBlockKeys._();

/// Whether long lines should wrap inside the code block.
///
/// The value is a bool and defaults to false when absent or invalid.
static const String wrapLines = 'wrap_lines';
}

extension AppFlowyCodeBlockNodeExtension on Node {
bool get codeBlockWrapLines =>
attributes[AppFlowyCodeBlockKeys.wrapLines] == true;
}

/// Adds AppFlowy-specific, per-node styling to the editor plugin's code block.
class AppFlowyCodeBlockComponentBuilder extends CodeBlockComponentBuilder {
AppFlowyCodeBlockComponentBuilder({
required CodeBlockStyle Function() styleBuilder,
required super.configuration,
required super.padding,
required CodeBlockLanguagePickerBuilder languagePickerBuilder,
required CodeBlockCopyBuilder copyButtonBuilder,
}) : super(
styleBuilder: styleBuilder,
languagePickerBuilder: languagePickerBuilder,
copyButtonBuilder: copyButtonBuilder,
);

@override
BlockComponentWidget build(BlockComponentContext blockComponentContext) {
final node = blockComponentContext.node;
final wrapLines = node.codeBlockWrapLines;
final baseStyle = styleBuilder?.call() ?? const CodeBlockStyle();

return CodeBlockComponentWidget(
key: node.key,
node: node,
configuration: configuration,
padding: padding,
showActions: showActions(node),
actionBuilder: (_, state) => actionBuilder(
blockComponentContext,
state,
),
actionWrapperBuilder: actionWrapperBuilder,
style: CodeBlockStyle(
textStyle: baseStyle.textStyle,
backgroundColor: baseStyle.backgroundColor,
foregroundColor: baseStyle.foregroundColor,
showLineNumbers: wrapLines ? false : baseStyle.showLineNumbers,
wrapLines: wrapLines,
),
languagePickerBuilder: languagePickerBuilder,
actions: actions,
copyButtonBuilder: copyButtonBuilder,
localizations: localizations,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:convert';

import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
Expand All @@ -12,9 +13,71 @@ import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:universal_platform/universal_platform.dart';

CodeBlockCopyBuilder codeBlockCopyBuilder =
(_, node) => _CopyButton(node: node);
const codeBlockWrapButtonKey = ValueKey('code_block_wrap_button');

CodeBlockCopyBuilder codeBlockCopyBuilder = (editorState, node) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (UniversalPlatform.isDesktopOrWeb)
_WrapCodeButton(
editorState: editorState,
node: node,
),
_CopyButton(node: node),
],
);
};

class _WrapCodeButton extends StatelessWidget {
const _WrapCodeButton({
required this.editorState,
required this.node,
});

final EditorState editorState;
final Node node;

@override
Widget build(BuildContext context) {
final isWrapped = node.codeBlockWrapLines;
final colorScheme = Theme.of(context).colorScheme;
final tooltip = LocaleKeys.document_codeBlock_wrapTooltip.tr();

return Padding(
padding: const EdgeInsets.all(4),
child: Semantics(
label: tooltip,
button: true,
toggled: isWrapped,
child: FlowyIconButton(
key: codeBlockWrapButtonKey,
tooltipText: tooltip,
isSelected: isWrapped,
fillColor:
isWrapped ? colorScheme.secondaryContainer : Colors.transparent,
hoverColor: colorScheme.secondaryContainer,
onPressed: () async {
final transaction = editorState.transaction
..updateNode(
node,
{AppFlowyCodeBlockKeys.wrapLines: !isWrapped},
);
await editorState.apply(transaction);
},
icon: Icon(
Icons.wrap_text_rounded,
color: isWrapped
? colorScheme.primary
: AFThemeExtension.of(context).textColor,
),
),
),
);
}
}

class _CopyButton extends StatelessWidget {
const _CopyButton({required this.node});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export 'base/cover_title_command.dart';
export 'base/toolbar_extension.dart';
export 'bulleted_list/bulleted_list_icon.dart';
export 'callout/callout_block_component.dart';
export 'code_block/code_block_component.dart';
export 'code_block/code_block_language_selector.dart';
export 'code_block/code_block_menu_item.dart';
export 'columns/simple_column_block_component.dart';
Expand Down
Loading
Loading