Skip to content

Add copy-to-clipboard support to code blocks #1273

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

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
@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.

Comment on lines +53 to +78
struct ParsedOptions {
var lang: String?
var nocopy = false
}

func parseLanguageString(_ input: String?) -> ParsedOptions {
guard let input else { return ParsedOptions() }

let parts = input
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespaces) }

var options = ParsedOptions()

for part in parts {
let lower = part.lowercased()
if lower == "nocopy" {
options.nocopy = true
} else if options.lang == nil {
options.lang = part
}
}
return options
}

let options = parseLanguageString(codeBlock.language)
Copy link
Contributor

Choose a reason for hiding this comment

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

If I understand correctly, I think that we'd want to parse the code block's languages string into its components even when the feature flags is not enabled. Otherwise, a string like swift, nocopy wouldn't produce the any syntax highlighting unless the developer also passes the feature flag.

Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like i have the opposite opinion here. If someone has written swift,nocopy, then they are explicitly aware of this feature flag and should be required to enable it to maintain the expected behavior. Otherwise they're writing a "weird language flag" that we would otherwise know nothing about.

Copy link
Contributor

Choose a reason for hiding this comment

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

A way we can soften the blow of working with this is to add this feature flag to BundleFeatureFlags to allow it to be set in Info.plist, like i did with the overloaded-symbol presentation in #891. That way, authors can set the feature flag in the same commit that they start to use the nocopy flags, and it should Just Work™.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's fair. I can imagine different people being surprised about both behaviors.

@DebugSteven has already added this flag to BundleFeatureFlags, so that seems like a good way for people to adopt this while this feature is experimental.

Copy link
Contributor

Choose a reason for hiding this comment

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

As a mental note for myself and @QuietMisdreavus:
Looking at the BundleFeatureFlags code now, we will at some point need to figure out a strategy for deprecating and migrating an experimental flag in the Info.plist to its non-experimental replacement. We haven't needed to do so yet, but it's kind of weird that the container key includes the word "experimental" if it can contain non-experimental flags.

@@ -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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs forum discussion Needs to be discussed in the Swift Forums
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants