Skip to content

[LSPAny] Add automatic encoding/decoding backed by Swift.Codable #41

Merged
rintaro merged 4 commits into
swiftlang:mainfrom
rintaro:lspany-codable
Mar 23, 2026
Merged

[LSPAny] Add automatic encoding/decoding backed by Swift.Codable #41
rintaro merged 4 commits into
swiftlang:mainfrom
rintaro:lspany-codable

Conversation

@rintaro

@rintaro rintaro commented Mar 19, 2026

Copy link
Copy Markdown
Member

Implement LSPAnyEncoder/LSPAnyDecoder and provide default init(fromLSPAny:)/encodeToLSPAny() for any LSPAnyCodable type that also conforms to Codable.

Conforming types can rely on auto-synthesized Codable implementations or provide custom init(from:)/encode(to:) methods.

@rintaro rintaro force-pushed the lspany-codable branch 2 times, most recently from 729f244 to 14e9e62 Compare March 19, 2026 23:10

@owenv owenv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Been a long time since I wrote an Encoder/Decoder but this looks correct to me

@rintaro rintaro force-pushed the lspany-codable branch 5 times, most recently from ce8ddb7 to b274a41 Compare March 20, 2026 05:20
self.textDocument = textDocument
self.color = color
self._range = CustomCodable<PositionRange>(wrappedValue: range)
self.range = range

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

These are drive-by changes. We don't need to initialize the underlying storage.

}

// TODO: Remove this extension after every clients migrated to `@CustomCodable<PositionRange>`.
extension Range: LSPAnyCodable where Bound == Position {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This extension is only used from sourcekit-lsp repo now. They should use @CustomCodable<PositionRange> for the properties instead.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we replace the TODO then?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'd like to keep this PR independent from sourcekit-lsp changes.
I will remove this after sourcekit-lsp PR is merged

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍🏽

rintaro added 4 commits March 20, 2026 09:55
Implement `LSPAnyEncoder`/`LSPAnyDecoder` and provide default
`init(fromLSPAny:)`/`encodeToLSPAny()` for any `LSPAnyCodable` type
that also conforms to `Codable`.

Conforming types can rely on auto-synthesized `Codable` implementations
or provide custom `init(from:)`/`encode(to:)` methods.
…cked defaults

Remove hand-written `init?(fromLSPDictionary:)` / `encodeToLSPAny()`
from LSP and BSP types, relying instead on the default implementations
introduced in the previous commit that delegate to `LSPAnyDecoder` /
`LSPAnyEncoder`.
When encoding an `LSPAny` value via `LSPAnyEncoder`, pass it through
directly instead of re-encoding through the generic `Encodable`
machinery. Likewise, when decoding into an `LSPAny` target type via
`LSPAnyDecoder`, return the stored value directly.

@ahoppen ahoppen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Very nice, getting rid of some annoying boilerplate

array.append(value)
storage = .unkeyed(array)
}
func count() -> Int {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should this be a property instead of a function?

Comment on lines +82 to +92
func prepareKeyed() {
storage = .keyed([:])
}
func set(key: String, value: LSPAnyReference) {
guard case .keyed(var dictionary)? = storage else {
preconditionFailure("set(key:value:) only available for .keyed")
}
storage = nil // Nil out first so `dictionary` is uniquely referenced (COW).
dictionary[key] = value
storage = .keyed(dictionary)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Instead of having these functions that are only applicable when the LSPAnyReference in in a certain mode, would it make sense to have separate types for the different cases (single, keyed, unkeyed) and then either unify them through a protocol they all conform to or a common superclass? That way we could ensure that the reference is of the expected type in the type system instead of having preconditions.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Maybe, but I don't want to spend much time for designing such setup for now.

reference.set(value: .string(value))
}
func encode<T: BinaryInteger & Encodable>(_ value: T) throws {
reference.set(value: .int(Int(truncatingIfNeeded: value)))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is truncatingIfNeeded really what we want here? Wouldn’t it be preferable to crash than to silently change the value of an encoded value?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I will address this in followups

Comment on lines +183 to +186
private func withValueReference<T>(
forKey key: Key,
encode: (LSPAnyReference, [any CodingKey]) throws -> T
) rethrows -> T {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does this have to be a with-style method? Since we’re operating on reference types anyway, wouldn’t it be easier if this just returned (LSPAnyReference, [any CodingKey])?

Same for UnkeyedContainer.withAppendingReference

@rintaro rintaro Mar 23, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

No, but I wanted to align the style of these methods with LSPAnyDecoder.UnkeyedContainer.withCurrentValueAndAdvance where I do want with style method

dictionary[key.stringValue] != nil
}

private func withValue<T>(forKey key: Key, decode: (LSPAny, [any CodingKey]) throws -> T) throws -> T {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Similar here: Does this have to be a with-style method instead of just returning a value?

}

// TODO: Remove this extension after every clients migrated to `@CustomCodable<PositionRange>`.
extension Range: LSPAnyCodable where Bound == Position {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we replace the TODO then?

@rintaro rintaro merged commit 87d8b26 into swiftlang:main Mar 23, 2026
51 checks passed
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.

3 participants