[GSoC-2024] Expansion of Swift Macros in Visual Studio Code (Detailed Post) - Lokesh.T.R (Alex Hoppen & Adam Fowler)

Hello Everyone,

I'm Lokesh.T.R from India. I'm a sophomore currently pursuing my bachelor's degree in Computer Science and Engineering in Vel Tech University, Chennai. I'm thrilled to share with you on what I have accomplished over the summer with my mentors @ahoppen and @adam-fowler for Google Summer of Code 2024.

Overview

Over the summer, we worked on adding support for expansion of Swift Macros in Visual Studio Code. Our project's main goal is to implement a code action in VS Code that allows users to view the generated contents of a Swift Macro.

Here's what it looks like to show the generated contents of a Swift Macro in a peeked editor when the "Expand Macro" Code Action is invoked by the user:

There were also some stretch goals which include:

  1. Bringing Semantic Functionality (such as jump-to-defintion, quick help on hover, Syntax Highlighting, etc.) to the macro expansion being previewed.
  2. Allowing to perform the "Expand Macro" Code Action on a macro that is present in the generated macro expansion to support the expansion of nested macros.

And as a bonus, we also worked on supporting macro expansions in other LSP-based editors which by default cannot make use of the LSP extensions that we introduced.

When can you start using this feature?

This will be available with SourceKit-LSP bundled with Swift 6.1 and the next VS Code Swift Extension Release.

For the curious minds, This feature is available in the main branch of sourcekit-lsp and vscode-swift, right now.

Implementation Details

How Swift Language Features work in LSP-based editors

Let's have a look at three key components that you use in your everyday life in an LSP-based editor (e.g. VS Code):

  1. VS Code-Swift Extension (Client):
    • primarily acts as a bridge between VS Code and SourceKit-LSP
  2. SourceKit-LSP (Server):
    • provides the necessary editor features to VS Code
    • communicates using Language Server Protocol (LSP)
  3. SourceKitD (Background Service):
    • provides the raw data and operations to SourceKit-LSP
    • baked into the swift compiler

Main Goal

In order to achieve the main goal, we introduced two new LSP extensions and new custom URL scheme as follows:

// NEW LSP EXTENSIONS (SPECIFICATIONS)
// -----------------------------------

// workspace/peekDocuments (sourcekit-lsp -> vscode-swift)
export interface PeekDocumentsParams {
  uri: DocumentUri;
  position: Position;
  locations: DocumentUri[];
}

export interface PeekDocumentsResult {
  success: boolean;
}

// workspace/getReferenceDocument (vscode-swift -> sourcekit-lsp)
export interface GetReferenceDocumentParams {
  uri: DocumentUri;
}

export interface GetReferenceDocumentResult {
  content: string;
}

// NEW CUSTOM URL SCHEME (SPECIFICATIONS)
// --------------------------------------

// Reference Document URL
("sourcekit-lsp://<document-type>/<display-name>?<parameters>");

// Reference Document URL with Macro Expansion Document Type
("sourcekit-lsp://swift-macro-expansion/LaCb-LcCd.swift?fromLine=&fromColumn=&toLine=&toColumn=&bufferName=&parent=");
  • "workspace/peekDocuments" allows the SourceKit-LSP Server to show the the contents stored in the locations inside the source file uri as a peek window
  • Reference Document URL Scheme sourcekit-lsp:// so that we can use it to encode the necessary data required to generate any form of content to which the URL corresponds to.
  • We introduce the very first document-type of the Reference Document URL which is swift-macro expansion. This will encode all the necessary data required to generate the macro expansion contents.
  • "workspace/getReferenceDocument" is introduced so that the Editor Client can make a request to the SourceKit-LSP Server with the Reference Document URL to fetch its contents.

The way this works is that, we generate the Reference Document URLs from the macro expansions generated using sourcekitd and make a "workspace/peekDocuments" request to the Editor Client. In VS Code, this executes the "editor.action.peekLocations" command to present a peeked editor.

Since VS Code can't resolve the contents of Reference Document URL, it makes a "workspace/getReferenceDocument" request to the SourceKit-LSP Server, thereby retrieveing the contents and successfully displaying it in the peeked editor.

Stretch Goals

  1. Achieving Semantic Functionality (jump-to-definition, quick help on hover, syntax highlighting, etc.):
    • SourceKit-LSP and SourceKitD by default doesn't know how to handle Reference Document URLs.
    • We need the build arguments of a file to provide semantic functionality.
    • We used the source file's build arguments as build arguments of the reference documents to trick sourcekitd to provide Semantic Functionality for the reference documents.
  2. Achieving Nested Macro Expansion:
    • Due to the flexible nature of the Reference Document URLs, nested macro expansions becomes trivial.
    • The beauty of the Reference Document URLs is that we can nest the Reference Document URLs.
    • We set the parent parameter of the macro expansion reference document URL to the source file if it's a first level macro expansion or to the reference document from which the macro expansion originates if it was second or third or n-th level macro expansion. This allows us to expand nested macros efficiently.

Bonus

While the new LSP Extensions which we introduced, doesn't work out-of-the-box in other LSP-based editors and requires the extension / plugin developers of respective editors to make use of it, We worked on providing a basic form of first level macro expansion support using the standard LSP Requests to support other LSP-based editors.

This works as follows:

  1. SourceKitLSP asks SourceKitD for macro expansions.
  2. It then stitches all the macro expansions together in a single file.
  3. It stores the file in some temporary location in the disk.
  4. It then makes a ShowDocumentRequest to open and show the file in the editor.

That wraps up the GSoC project successfully!

What's left to do?

  • I will be working on implementing a test case that encompasses all the semantic features in all nested macro levels in sourcekit-lsp.
  • I will also implement some end-to-end test cases in the vscode-swift side which ensures that they really work as intended in a real world situation.

Test cases for freestanding macros, attached macros and nested macros are already in place.
Code Documentation is also in place for everything that was implemented so far.

Future Directions

  1. Feedback, Feedback, Feedback!

    • We had put so much attention to detail and ensured that every decision made in the design process was thoughtful.
    • But, you may have a better idea, or you may find some issues, and we would love to improve this feature.
    • Please file an issue to suggest an idea or report bugs in the sourcekit-lsp or vscode-swift repository wherever you face the issue.
  2. Migrating the non-standard "workspace/getReferenceDocument" to the new standard "workspace/textDocumentContent" Request

    • We should be able to perform this migration when the specifications of LSP 3.18 gets finalised.
    • Thanks to @fwcd for noticing the new change and making a PR to be ready for migration when LSP 3.18 releases.
  3. Migrate from generating temporary files in the Disk to Reference Document URLs for other LSP-based editors

    • We are currently unable to generate macro expansions on-the-fly in other LSP-based editors since we don't have our LSP Extension for getting the contents of the reference document.
    • Building on top of the previous idea, when LSP 3.18 gets finalised, we should be able to completely eliminate temporary file storage in favour of the standard "workspace/textDocumentContent" Request.
  4. Adding Semantic Functionality & Nested Macro Expansion support for other LSP-based editors

    • This is tricky to implement since the temporary file (or reference document after LSP 3.18) will have all the macro expansions of a given macro in a single file.
    • Although the same approach can be used such as passing the source file's build arguments and parent to be the macro's originating file, there will be line and character position shifts that should be taken into consideration.
    • For example, the third macro expansion of a given attached macro will be expected to start at 0:0 but its actual location will be shifted by the first and second macro expansion's content length and three lines of comments that describes where the macro will be present in the original file
  5. Other Use Cases for the Reference Document URL

    • Reference Document URLs where built from the ground up to allow for encoding the data required to show any form of content, one such example which we discussed today is swift-macro-expansion document type.
    • This should allow anyone to show any content of their choice, in a peeked editor or a fully open document, as long as its generated during compile time.
    • The following use cases are not related to macro expansions but uses the Reference Document URL that we created to show other document types:
      • Migrating OpenInterfaceRequest to Reference Document URLs to show Swift Generated Interfaces
        (Thanks to @ahoppen & @adam-fowler for the idea)
      • Showing Implicitly generated constructors and Synthesized code upon Equatable, Hashable and Codable Conformances.
        (Thanks to @douglas_gregor and @rintaro for pointing this out and some use cases for reference documents in their projects)
      • Showing a preview of generated HTML or rendering the generated HTML from the Mustache template engine by hooking up its CLI.
        (Thanks to @adam-fowler for his feedback on my idea)
    • These are just few examples, and the fact that you can show various document formats based on various code generation behaviours brings a wide range of possibilities, and hence, you can bring your own idea.
    • And with LSP 3.18, this will be standardised across all editors, not just VS Code.

Thanks & Gratitude

I offer my deepest gratitude to my mentors @ahoppen and @adam-fowler without whom this journey is impossible. GSoC is not only the work of me as a contributor but also the work of my mentors in guiding me and helping me out whenever possible.

Hey @ahoppen, Thank you for accepting my proposal, spending as many hours as possible with me to set up my environment and trying to debug all the bugs in my environment. Thanks for getting me started with the PRs. Thanks for your wonderful ideas and feedback on my ideas. Thanks for waking up to attend the 8:00 AM meeting. Thanks for your detailed PR reviews. Thanks for helping me out when I got constrained by time. Thanks for allowing me to change meeting dates due to my schedule. Thanks for all the immediate and quick responses. Pardon me for any mistakes that I did. I hope I gave you a good GSoC experience too. And, Thank you for all the work which you did in sourcekit-lsp.

Hey @adam-fowler, Thank you for accepting my proposal. Thank you so much for suggesting me to get started with the project in a docker container. Thanks for your wonderful ideas and feedback on my ideas. Thanks for reviewing my PRs. Thank you for always being ready to jump into LSP specfications and VS Code's codebase to identify things. Thanks for allowing me to change meeting dates due to my schedule. Thanks for all the immediate and quick responses. Pardon me for any mistakes that I did. I believe this is your first time also, I hope I gave you a good GSoC experience too. And, Thank you for all the work which you did in vscode-swift.

If not for you two people, this project wouldn't be a success.

Special Thanks

  • I was in the middle of lots of works & exams in my university when GSoC's contributor proposal submission period, I didn't have much time to go in detail into any project from any organisation. I always wanted to do some contribution to Swift. Among all the ideas, The initial foundations and discussions laid by @fwcd helped me to understand the project and its requirements and also allowed me to understand both the sourcekit-lsp and vscode-swift code base faster, and make a detailed proposal that got me selected in the first place.

  • Thanks to @plemarquand for testing out the very first implementation of my project. I certainly didn't expect that. And, it did give me a boost in confidence that the community is very welcoming and also motivated me that I'm in my right direction. Also, Thanks to you for offering me some initial guidance on writing end-to-end test cases in vscode-swift and also for reviewing my PRs.

  • Thanks to @douglas_gregor and @rintaro for making me realise that you guys have already started finding use cases of my work in your own projects. I certainly didn't expect that the design decision which me and my mentors made would have immediate benefits to the community in its own way.

  • Thanks to @Matejkob and @parispittman for encouraging me to present my project in conferences whenever and wherever possible.

  • Thanks to the entire Swift Community for being so welcoming, I started my journey with Swift in 2019 and I always wanted to do some contribution, I didn't take a step forward since I wasn't very confident with my communication / social skills. Now, years have passed, I believe I grew up seeing Swift evolve. GSoC gave me a perfect excuse to push me out of my comfort zone and made me to contribute to Swift and to have weekly meetings with unimaginably intelligent mentors and thus, Here we are with a successful project. (My mentors might have noticed the difference in how I spoke in the first introductory meeting and in the final
    presentation).

Appendix: Pull Request Stats

Pre-GSoC (Community Bonding Period)

sourcekit-lsp [GOOD FIRST ISSUE]

  1. Change static method DocumentURI.for(_:testName:) to an initializer DocumentURI.init(for:testName:) #1348
  2. Rename note to notification throughout the codebase wherever necessary #1353

GSoC (Coding Period)

What got merged?

sourcekit-lsp

  1. Add LSP support for showing Macro Expansions #1436
  2. Add LSP extension to show Macro Expansions (or any document) in a "peeked" editor (and some minor quality improvements) #1479
  3. Skip testFreestandingMacroExpansion if host toolchain does not support background indexing #1548 by @ahoppen
  4. Skip testAttachedMacroExpansion if host toolchain does not support background indexing #1553
  5. Allow macro expansions to be viewed through GetReferenceDocumentRequest instead of storing in temporary files #1567
  6. Support expansion of nested macros #1631 by @ahoppen (Co-authored by me from #1610)
  7. Add support for semantic functionality in macro expansion reference documents #1634
  8. Remove ExperimentalFeature.showMacroExpansions flag for macro expansions #1635
  9. Add an extra percent encoding layer when encoding DocumentURIs to LSP requests #1636 by @ahoppen
  10. Address review comments to #1631 #1637 by @ahoppen

vscode-swift

  1. Handle PeekDocumentsRequest to show documents from sourcekit-lsp (like, Macro Expansions) in a "peeked" editor #945
  2. Add missing licence header to peekDocuments.ts #953 by @plemarquand
  3. Retrieve macro expansions for PeekDocumentsRequest through GetReferenceDocumentRequest instead of showing temporary files #971
  4. Allow VS Code to recognise files with "sourcekit-lsp" scheme to provide Semantic Functionality through SourceKitLSP #990
  5. Fix peeked editor closing without reopening with new contents when triggered again at the same position in the same file #1019 (Workaround for a bug in VS Code)

What should be merged?

  1. Work around Uri round-tripping issue in VS Code for sourcekit-lsp scheme #1026 by @ahoppen

What got closed?

sourcekit-lsp

  1. Add LSP extension request for retrieving macro expansions #892 by @fwcd :pray:t2:

  2. Add Semantic Functionality to Macro Expansion Reference Documents (including Nested Macro Expansion) :vertical_traffic_light: #1610 (Explored a variety of ideas everything took shape into #1631, last commit squashed previous ideas by the way) - Closed in favour of #1631

vscode-swift

  1. Add client-side support for macro expansions #621 by @fwcd :pray:t2:

  2. Fix semantic functionality not working for macro expansion reference documents due to URL encoding #1017 (Workaround for a bug in VS Code) - Closed in favour of #1026 by @ahoppen

What has to be done?

  1. Revert sourcekit-lsp#1636 when vscode-swift#1026 gets merged.
  2. Add test cases for Semantic Functionality in sourcekit-lsp.
  3. Add end-to-end test cases for the "Expand Macro" Code Action in vscode-swift.
  4. Merge sourcekit-lsp#1639 by @fwcd when LSP 3.18 gets finalised
  5. Merge vscode-swift#1027 by @fwcd when LSP 3.18 gets finalised.
52 Likes

@lokeshtr it's been a joy working with you. The quality of your work is superb. Your communication skills are great. The project wasn't easy. It involved learning two large code bases written in two different languages. This didn't hold you back though and you completed it along with the stretch goals in time.

As you note in your report, the work you have done has opened up a number of future avenues of development we didn't consider before the start of this project.

I look forward to your future contributions

7 Likes

@adam-fowler That means a lot to me. Thank you so much :smile:

1 Like

This was a fun read!

3 Likes

I couldn’t agree more. You did outstanding and very high-quality work. It’s been a pleasure working with you and if you want to continue contributing after GSoC is officially over, I look forward to continue collaborating! :heart:

3 Likes

Amazing! Projects like these are so important for making Swift a truly viable cross platform language :heart:

2 Likes

@lokeshtr This is amazing work and as a Linux, VSCode and Swift user I am looking forward to using this new feature.

I also think this should also be added as a blog post on Swift.org

4 Likes

@ahoppen That means a lot to me. Thank you! :heart:

I'm planning to do open source contributions on the side as an hobby whenever I get some free time. It's going to be fun for sure!

1 Like

Thanks :heart: !

I have been following Swift's growth for many years and it will be amazing to make Swift a truly viable cross platform language. As we saw in this year's WWDC, I expect Apple to push Swift into usage across various platforms and domains.

And, we should also ensure to spread the word to people, who had thought Swift to be an Apple only Language, that It is now a general purpose cross platform language.

3 Likes

Thank you! :heart:

And, please be sure to file an issue if you have any enhancement ideas or facing some bugs.

I'm not sure about making a blog post on Swift.org. What are your thoughts on this, @ahoppen?

I believe that there will be a general blog post about all GSoC Projects, I'm not sure if we will have specific ones.

2 Likes

We have had a blog post about the work done in the GSoC projects in the last couple of years and I think we will do the same this year. CC @ktoso, who is the GSoC admin for Swift and would organize the compilation for such a blog post.

4 Likes

Great work @lokeshtr! This is super cool. Thanks for the detailed write-up here too — you made it easy to follow along.

1 Like

Absolutely amazing work!

2 Likes

Ahh… interesting! Do you have any quick steps for an engineer to test this out for themselves from VS Code? I do not have much experience building Swift outside of Xcode. I need to point to main from where?

Hey @vanvoorden,
I love your interest for the project. I certainly don't think the steps to test it out are "quick" unless you are already working with the sourcekit-lsp and vscode-swift codebase. The steps are as follows:

Inorder for you to test this out right now, you will have to build sourcekit-lsp's "main" branch from scratch and also build & run the vscode-swift extension's "main" branch.

You can find steps on how to build and run here:

After you have successfully built sourcekit-lsp, you will have to configure the Swift Extension for VS Code to use the built sourcekit-lsp. You can find the steps for that here: sourcekit-lsp/CONTRIBUTING.md at main · swiftlang/sourcekit-lsp · GitHub

Now, you can run the vscode-swift extension in a host development window or build it into a .vsix file and install it into your VS Code directly as suggested here: vscode-swift/CONTRIBUTING.md at main · swiftlang/vscode-swift · GitHub

Note: You will have to ensure that the version of swift toolchain used to compile sourcekit-lsp is the same as the swift toolchain configured in the settings. Else, It will lead to unexpected behaviour.

I understand that the above steps are somewhat complicated to get started with (I felt it when I started the my work on the project), but yeah, I hope this helps :slight_smile:

If you ever test it out, I would love to have your feedback. If not, look forward to the official releases of sourcekit-lsp and vscode-swift (It will take a few months).

:sparkles:

2 Likes

Ahh… interesting! Totally outside the scope of your diff to support macro expansion… but I would love me a script (shell or python) that puts all that work in just one place for me to run with one command. I'm so lazy. :D

I am not used to writing scripts to automate tasks like this. I will give it a try when I get some time (I'm swamped right now). But, I can't guarantee that you will get the script soon. :slight_smile:

2 Likes

That's an impressive summer of code project and the design looks very well thought through! It's great that you got to the stretch functionality too. One small question for my curiosity: what's the eviction policy for the cache?

I think the custom url scheme would also lend itself to another feature I was interested in: go to definition for built-in Swift APIs which do not ship with a source file. We could have a url which represents decompiling a certain type definition from a swiftmodule/swiftinterface and resolve it in a peek window.

1 Like

I think everything required in VS Code required to support this feature has been released as of 1.11.1.

I believe you’ll still need to be using the latest sourcekit-lsp.

@lokeshtr do you still need to enable this functionality by passing arguments to sourcekit-lsp on startup?

That's an impressive summer of code project and the design looks very well thought through! It's great that you got to the stretch functionality too.

Thanks Tristan.

One small question for my curiosity: what's the eviction policy for the cache?

The current implementation cache is basically a queue of a constant size 10. It basically stores the most recent 10 macro expansions.