-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Incorporate changes made in Helix highlight queries
In helix-editor/helix#9586, the Helix editor added highlighting for the `any` keyword. However, while reviewing that, I noticed that both `some` and `any` should be more specific: these keywords are legal as identifiers, but should not be highlighted as identifiers when they are keywords. For instance, I can write: ``` let any: any Protocol = AnyImplementation() ``` In this example, only the second `any` should be highlighted. See discussion on #351
- Loading branch information
1 parent
bd81fff
commit 1aa6284
Showing
3 changed files
with
305 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
// The MIT License (MIT) | ||
// | ||
// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). | ||
|
||
import Foundation | ||
// ^ @include | ||
|
||
extension ImagePipeline { | ||
/// The pipeline configuration. | ||
public struct Configuration: @unchecked Sendable { | ||
/// Image cache used by the pipeline. | ||
public var imageCache: (any ImageCaching)? { | ||
// ^ @keyword | ||
// ^ @type | ||
// This exists simply to ensure we don't init ImageCache.shared if the | ||
// user provides their own instance. | ||
get { isCustomImageCacheProvided ? customImageCache : ImageCache.shared } | ||
set { | ||
customImageCache = newValue | ||
isCustomImageCacheProvided = true | ||
} | ||
} | ||
|
||
/// Default implementation uses shared ``ImageDecoderRegistry`` to create | ||
/// a decoder that matches the context. | ||
public var makeImageDecoder: @Sendable (ImageDecodingContext) -> (any ImageDecoding)? = { | ||
// ^ @keyword | ||
// ^ @type | ||
ImageDecoderRegistry.shared.decoder(for: $0) | ||
} | ||
|
||
/// Instantiates a pipeline configuration. | ||
/// | ||
/// - parameter dataLoader: `DataLoader()` by default. | ||
// NOTE: Surgical change on next line: renamed `dataLoader` to `any` to show it is a contextual keyword | ||
public init(any: any DataLoading = DataLoader()) { | ||
// ^ @parameter | ||
// ^ @keyword | ||
// ^ @type | ||
self.dataLoader = any | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,260 @@ | ||
// The MIT License (MIT) | ||
// | ||
// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). | ||
|
||
import Foundation | ||
import Nuke | ||
import SwiftUI | ||
import Combine | ||
|
||
public typealias ImageRequest = Nuke.ImageRequest | ||
// ^ @keyword | ||
// // ^ @type | ||
|
||
/// A view that asynchronously loads and displays an image. | ||
/// | ||
/// ``LazyImage`` is designed to be similar to the native [`AsyncImage`](https://developer.apple.com/documentation/SwiftUI/AsyncImage), | ||
/// but it uses [Nuke](https://github.com/kean/Nuke) for loading images. You | ||
/// can take advantage of all of its features, such as caching, prefetching, | ||
/// task coalescing, smart background decompression, request priorities, and more. | ||
@MainActor | ||
@available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 10.16, *) | ||
public struct LazyImage<Content: View>: View { | ||
@StateObject private var viewModel = FetchImage() | ||
|
||
private var context: LazyImageContext? | ||
private var makeContent: ((LazyImageState) -> Content)? | ||
private var transaction: Transaction | ||
private var pipeline: ImagePipeline = .shared | ||
private var onStart: ((ImageTask) -> Void)? | ||
private var onDisappearBehavior: DisappearBehavior? = .cancel | ||
private var onCompletion: ((Result<ImageResponse, Error>) -> Void)? | ||
|
||
// MARK: Initializers | ||
|
||
/// Loads and displays an image using `SwiftUI.Image`. | ||
/// | ||
/// - Parameters: | ||
/// - url: The image URL. | ||
public init(url: URL?) where Content == Image { | ||
// ^ @parameter | ||
// ^ @type | ||
self.init(request: url.map { ImageRequest(url: $0) }) | ||
} | ||
|
||
/// Loads and displays an image using `SwiftUI.Image`. | ||
/// | ||
/// - Parameters: | ||
/// - request: The image request. | ||
public init(request: ImageRequest?) where Content == Image { | ||
self.context = request.map(LazyImageContext.init) | ||
self.transaction = Transaction(animation: nil) | ||
} | ||
|
||
/// Loads an images and displays custom content for each state. | ||
/// | ||
/// See also ``init(request:transaction:content:)`` | ||
public init(url: URL?, | ||
transaction: Transaction = Transaction(animation: nil), | ||
@ViewBuilder content: @escaping (LazyImageState) -> Content) { | ||
self.init(request: url.map { ImageRequest(url: $0) }, transaction: transaction, content: content) | ||
} | ||
|
||
/// Loads an images and displays custom content for each state. | ||
/// | ||
/// - Parameters: | ||
/// - request: The image request. | ||
/// - content: The view to show for each of the image loading states. | ||
/// | ||
/// ```swift | ||
/// LazyImage(request: $0) { state in | ||
/// if let image = state.image { | ||
/// image // Displays the loaded image. | ||
/// } else if state.error != nil { | ||
/// Color.red // Indicates an error. | ||
/// } else { | ||
/// Color.blue // Acts as a placeholder. | ||
/// } | ||
/// } | ||
/// ``` | ||
public init(request: ImageRequest?, | ||
transaction: Transaction = Transaction(animation: nil), | ||
@ViewBuilder content: @escaping (LazyImageState) -> Content) { | ||
self.context = request.map { LazyImageContext(request: $0) } | ||
self.transaction = transaction | ||
self.makeContent = content | ||
} | ||
|
||
// MARK: Options | ||
|
||
/// Sets processors to be applied to the image. | ||
/// | ||
/// If you pass an image requests with a non-empty list of processors as | ||
/// a source, your processors will be applied instead. | ||
public func processors(_ processors: [any ImageProcessing]?) -> Self { | ||
map { $0.context?.request.processors = processors ?? [] } | ||
} | ||
|
||
/// Sets the priority of the requests. | ||
public func priority(_ priority: ImageRequest.Priority?) -> Self { | ||
map { $0.context?.request.priority = priority ?? .normal } | ||
} | ||
|
||
/// Changes the underlying pipeline used for image loading. | ||
public func pipeline(_ pipeline: ImagePipeline) -> Self { | ||
map { $0.pipeline = pipeline } | ||
} | ||
|
||
public enum DisappearBehavior { | ||
/// Cancels the current request but keeps the presentation state of | ||
/// the already displayed image. | ||
case cancel | ||
/// Lowers the request's priority to very low | ||
case lowerPriority | ||
} | ||
|
||
/// Gets called when the request is started. | ||
public func onStart(_ closure: @escaping (ImageTask) -> Void) -> Self { | ||
map { $0.viewModel.onStart = closure } | ||
} | ||
|
||
/// Override the behavior on disappear. By default, the view is reset. | ||
public func onDisappear(_ behavior: DisappearBehavior?) -> Self { | ||
map { $0.onDisappearBehavior = behavior } | ||
} | ||
|
||
/// Gets called when the current request is completed. | ||
public func onCompletion(_ closure: @escaping (Result<ImageResponse, Error>) -> Void) -> Self { | ||
map { $0.onCompletion = closure } | ||
} | ||
|
||
private func map(_ closure: (inout LazyImage) -> Void) -> Self { | ||
var copy = self | ||
closure(©) | ||
return copy | ||
} | ||
|
||
// MARK: Body | ||
|
||
public var body: some View { | ||
// ^ @keyword | ||
// ^ @type | ||
ZStack { | ||
if let makeContent = makeContent { | ||
makeContent(viewModel) | ||
} else { | ||
makeDefaultContent(for: viewModel) | ||
} | ||
} | ||
.onAppear { onAppear() } | ||
.onDisappear { onDisappear() } | ||
.onChange(of: context) { | ||
viewModel.load($0?.request) | ||
} | ||
} | ||
|
||
// NOTE: surgical change on next line: renamed `state` to `some` to test that `some` is a contextual keyword. | ||
@ViewBuilder | ||
private func makeDefaultContent(for some: LazyImageState) -> some View { | ||
// ^ @parameter | ||
// ^ @keyword | ||
// ^ @type | ||
if let image = some.image { | ||
image | ||
} else { | ||
Color(.secondarySystemBackground) | ||
} | ||
} | ||
|
||
private func onAppear() { | ||
viewModel.transaction = transaction | ||
viewModel.pipeline = pipeline | ||
viewModel.onStart = onStart | ||
viewModel.onCompletion = onCompletion | ||
viewModel.load(context?.request) | ||
} | ||
|
||
private func onDisappear() { | ||
guard let behavior = onDisappearBehavior else { return } | ||
switch behavior { | ||
case .cancel: | ||
viewModel.cancel() | ||
case .lowerPriority: | ||
viewModel.priority = .veryLow | ||
} | ||
} | ||
} | ||
|
||
private struct LazyImageContext: Equatable { | ||
var request: ImageRequest | ||
|
||
static func == (lhs: LazyImageContext, rhs: LazyImageContext) -> Bool { | ||
let lhs = lhs.request | ||
let rhs = rhs.request | ||
return lhs.preferredImageId == rhs.preferredImageId && | ||
lhs.priority == rhs.priority && | ||
lhs.processors == rhs.processors && | ||
lhs.priority == rhs.priority && | ||
lhs.options == rhs.options | ||
} | ||
} | ||
|
||
#if DEBUG | ||
@available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) | ||
struct LazyImage_Previews: PreviewProvider { | ||
static var previews: some View { | ||
// ^ @keyword | ||
Group { | ||
LazyImageDemoView() | ||
.previewDisplayName("LazyImage") | ||
|
||
LazyImage(url: URL(string: "https://kean.blog/images/pulse/01.png")) | ||
.previewDisplayName("LazyImage (Default)") | ||
|
||
AsyncImage(url: URL(string: "https://kean.blog/images/pulse/01.png")) | ||
.previewDisplayName("AsyncImage") | ||
} | ||
} | ||
} | ||
|
||
// This demonstrates that the view reacts correctly to the URL changes. | ||
@available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) | ||
private struct LazyImageDemoView: View { | ||
@State var url = URL(string: "https://kean.blog/images/pulse/01.png") | ||
@State var isBlured = false | ||
@State var imageViewId = UUID() | ||
|
||
var body: some View { | ||
VStack { | ||
Spacer() | ||
|
||
LazyImage(url: url) { state in | ||
if let image = state.image { | ||
image.resizable().aspectRatio(contentMode: .fit) | ||
} | ||
} | ||
#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS) | ||
.processors(isBlured ? [ImageProcessors.GaussianBlur()] : []) | ||
#endif | ||
.id(imageViewId) // Example of how to implement retry | ||
|
||
Spacer() | ||
VStack(alignment: .leading, spacing: 16) { | ||
Button("Change Image") { | ||
if url == URL(string: "https://kean.blog/images/pulse/01.png") { | ||
url = URL(string: "https://kean.blog/images/pulse/02.png") | ||
} else { | ||
url = URL(string: "https://kean.blog/images/pulse/01.png") | ||
} | ||
} | ||
Button("Retry") { imageViewId = UUID() } | ||
Toggle("Apply Blur", isOn: $isBlured) | ||
} | ||
.padding() | ||
#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS) | ||
.background(Material.ultraThick) | ||
#endif | ||
} | ||
} | ||
} | ||
#endif |