Skip to content

Conversation

DebugSteven
Copy link
Contributor

@DebugSteven DebugSteven commented Aug 6, 2025

Summary

This PR adds support for a new copy-to-clipboard button in code blocks. When this feature is enabled through the enable-experimental-code-block flag, a copy-to-clipboard button is rendered in the top-right corner of all code blocks, allowing users to easily copy its contents. When an author does not want the contents of a code block to be copyable, a nocopy option can be used to disable the copy-to-clipboard button.

User Experience

When the enable-experimental-code-block flag is used, a copy button will appear in the top-right corner of all code blocks. Clicking the button copies the full contents of the code block to the clipboard and displays a checkmark to confirm success. If a code block includes the nocopy keyword in the language line, the copy button will not appear on that code block.

Implementation Overview

  • In swift-docc, this change adds a feature flag enable-experimental-code-block, which enables a copy-to-clipboard button on code blocks by default.
  • Parses the nocopy option from the language line in triple-backtick code blocks to disable the copy button on that code block.
  • A copyToClipboard property was added to RenderBlockContent.codeListing passed to the renderer.
  • This flag is forwarded to swift-docc-render. The accompanying branch/PR is here: Add copy-to-clipboard support to code blocks swift-docc-render#961

Dependencies

This PR depends on associated changes in swift-docc-render to actually render and handle the copy button.

Testing

Setup

  1. Use the codeblock-copy branches for swift-docc and swift-docc-render with the copy-to-clipboard changes.
  2. Rebuild documentation using swift-docc with the feature flag enable-experimental-code-block and serve it using a local build of swift-docc-render.

How to Test

  1. In all code listings with the enable-experimental-code-block flag, a copy button will appear in the top-right corner.
  2. Click the copy icon. The code should be copied to your clipboard and the icon should update to a checkmark briefly. Paste to verify the copy functionality works.

To disable the copy icon with the feature flag enabled:

  1. In any code listing using ``` or by adding a code listing, add the nocopy option like this:
```swift, nocopy
print(“Hello, world!”)
```


or like this:


```nocopy
print(“Hello, world!”)
```


2. Verify the copy button does not appear on this code block.

Screenshot 2025-08-11 at 5 47 54 PM

Checklist

Make sure you check off the following items. If they cannot be completed, provide a reason.

  • Added tests – testCopyToClipboard() in RenderContentCompilerTests.swift
  • Ran the ./bin/test script and it succeeded
  • Updated documentation if necessary – I added copyToClipboard as a property of CodeListing in RenderNode.spec.json. I also added a subsection to Formatting Your Documentation Content. Please let me know if there’s any other documentation I should update.

show on hover
always show copy button mobile

@heckj
Copy link
Member

heckj commented Aug 6, 2025

@swift-ci please test

@d-ronnqvist d-ronnqvist added the needs forum discussion Needs to be discussed in the Swift Forums label Aug 7, 2025
@d-ronnqvist
Copy link
Contributor

Since this is adding new user-facing syntax I'm adding the needs-forum-discussion tag until the community has had time to discuss the new syntax.

@d-ronnqvist
Copy link
Contributor

There is a forum thread for this here. It would be good for that thread to cover some discussion on future directions so that we feel comfortable that this syntax can scale to accommodate those future directions (or intentionally not support them if we believe that each would benefit from a different user-facing syntax).

@heckj heckj self-assigned this Aug 7, 2025
@DebugSteven DebugSteven force-pushed the codeblock-copy branch 2 times, most recently from d0b75d8 to 1f4894d Compare August 11, 2025 17:20
@heckj
Copy link
Member

heckj commented Aug 12, 2025

@swift-ci please test

Copy link
Contributor

@d-ronnqvist d-ronnqvist left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for opening this PR.

The code looks good to me with some questions about how nocopy option should work when the feature flag isn't enabled. If the current behavior is intended then that code doesn't need to change.

I have some non-blocking feedback on the developer-facing documentation for this. Because the feature flag help-text mentions additional features that aren't added in this PR I suspect that this documentation (and possibly also the syntax) could frequently as more features are added. If you are fine with updating (and possibly rewriting parts of) the documentation as new experimental features as added, then you can keep it in the pull request. An alternative would be to write the documentation when removing the experimental status from these features, when the scope and syntax has settled.

@@ -223,4 +223,52 @@ class RenderContentCompilerTests: XCTestCase {
XCTAssertEqual(documentThematicBreak, thematicBreak)
}
}

func testCopyToClipboard() async throws {
enableFeatureFlag(\.isExperimentalCodeBlockEnabled)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: is there a corresponding test that checks that code listings don't have a copy-to-clipboard button when the feature flag isn't set? This could be as small as adding XCTAssertEqual(codeListing.copyToClipboard, false) to some existing test.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't see tests that already had Code Listings on them, so I opted to add a new test. If there's a place you'd prefer me to add this assertion, let me know and I can do that instead.

}

func testNoCopyToClipboard() async throws {
enableFeatureFlag(\.isExperimentalCodeBlockEnabled)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Do we want to test the behavior when a code block contains swift, nocopy but the feature flag isn't enabled?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added another test for this as well. It shows that when the feature flag isn't enabled and a code block contains swift, nocopy that codeListing.syntax will equal the contents of the entire line, swift, nocopy in this case. Let me know if there is more I should do on that test.

var nocopy = false
}

func parseLanguageString(_ input: String?) -> ParsedOptions {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any plans to raise diagnostics here if we encounter an unknown option?

For example, the code below will parse swift, nocpoy as lang: nocpoy, nocopy: false and the only way for the developer to notice this is to see that the rendered code listing has a copy-to-clipboard button and doesn't have syntax highlighting.

If we want the possibility of raising diagnostics from this (or future) code block options, it probably needs be parsed earlier in the build, before "rendering".

Copy link
Contributor Author

@DebugSteven DebugSteven Aug 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be a good idea to raise diagnostics for unknown options. I’m not familiar with DocC diagnostics and I don’t understand why it would need to be emitted earlier. Could you give me some more info?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User-facing diagnostics are represented by Problem values that wrap a Diagnostic and optionally some suggested Solution values. To emit a use-facing diagnostic, the caller passes the Problem to DiagnosticEngine/emit(_:).

The "render" phase of the build isn't meant to perform any content validation, so there's nothing in RenderNodeTranslator or RenderContentCompiler that creates and emits user-facing diagnostics. Instead, depending on the scope of the validation, there are few places where it could be parsed/validated;

  • Some content validation is defined in dedicated Checker types. For example InvalidAdditionalTitle or DuplicateTopicsSections
  • Some content (primarily metadata) is validated and turned into diagnostics during the DocumentationNode creation.
  • Some content (primarily links) is complex enough to have it's own SemanticVisitor/MarkupVisitor and run as dedicated steps during the context's "registration" (for example ExternalReferenceWalker and ReferenceResolver).
  • Some validation run as dedicated steps inside DocumentationContext.topicGraphGlobalAnalysis().

For this, I think that a Checker would be my recommendation because it's small and built on a MarkupWalker so you can easily visit the code blocks by overriding visitCodeBlock().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for all of this. Your explanation was very helpful.

I updated the PR with a new checker and tests for it, as well as adding an enum for the code block options directly on CodeListing. It should be ready for re-review.

@d-ronnqvist
Copy link
Contributor

d-ronnqvist commented Aug 21, 2025

It looks like the Forum discussion is mostly settles on this syntax as an experimental feature so I'm removing the needs-forum-discussion tag label.

@d-ronnqvist d-ronnqvist removed the needs forum discussion Needs to be discussed in the Swift Forums label Aug 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants