Skip to content

Implement color presentation #107

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

Merged
merged 4 commits into from
Jun 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions Sources/LanguageServerProtocol/ColorPresentationRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

/// The color presentation request is sent from the client to the server to obtain
/// a list of presentations for a color value at a given location. Clients can
/// use the result to modify a color reference, or show in a color picker
/// and let users pick one of the presentations
///
/// - Parameters:
/// - textDocument: The document to request presentations for.
/// - color: The color information to request presentations for.
/// - range: The range where the color would be inserted. Serves as a context.
///
/// - Returns: A list of color presentations for the given document.
public struct ColorPresentationRequest: TextDocumentRequest, Hashable {
public static let method: String = "textDocument/colorPresentation"
public typealias Response = [ColorPresentation]?

/// The document to request presentations for.
public var textDocument: TextDocumentIdentifier

/// The color information to request presentations for.
public var color: Color

/// The range where the color would be inserted. Serves as a context.
public var range: PositionRange

public init(
textDocument: TextDocumentIdentifier,
color: Color,
range: Range<Position>)
{
self.textDocument = textDocument
self.color = color
self.range = PositionRange(range)
}
}

public struct ColorPresentation: ResponseType, Hashable {
/// The label of this color presentation. It will be shown on the color
/// picker header. By default this is also the text that is inserted when
/// selecting this color presentation.
public var label: String

/// An edit which is applied to a document when selecting this
/// presentation for the color. When `falsy` the label is used.
public var textEdit: TextEdit?

/// An optional array of additional text edits that are applied when
/// selecting this color presentation. Edits must not overlap with
/// the main edit nor with themselves.
public var additionalTextEdits: [TextEdit]?

public init(label: String, textEdit: TextEdit?, additionalTextEdits: [TextEdit]?) {
self.label = label
self.textEdit = textEdit
self.additionalTextEdits = additionalTextEdits
}
}
64 changes: 64 additions & 0 deletions Sources/LanguageServerProtocol/DocumentColor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

/// The document color request is sent from the client to the server to list
/// all color references found in a given text document. Along with the range,
/// a color value in RGB is returned.
/// Clients can use the result to decorate color references in an editor.
///
/// - Parameters:
/// - textDocument: The document to search for color references.
///
/// - Returns: A list of color references for the given document.
public struct DocumentColorRequest: TextDocumentRequest, Hashable {
public static let method: String = "textDocument/documentColor"
public typealias Response = [ColorInformation]?

/// The document in which to search for color references.
public var textDocument: TextDocumentIdentifier

public init(textDocument: TextDocumentIdentifier) {
self.textDocument = textDocument
}
}

public struct ColorInformation: ResponseType, Hashable {
/// The range in the document where this color appears.
public var range: PositionRange

/// The actual color value for this color range.
public var color: Color

public init(range: Range<Position>, color: Color) {
self.range = PositionRange(range)
self.color = color
}
}

/// Represents a color in RGBA space.
public struct Color: Hashable, Codable {
/// The red component of this color in the range [0-1].
public var red: Double
/// The green component of this color in the range [0-1].
public var green: Double
/// The blue component of this color in the range [0-1].
public var blue: Double
/// The alpha component of this color in the range [0-1].
public var alpha: Double

public init(red: Double, green: Double, blue: Double, alpha: Double) {
self.red = red
self.green = green
self.blue = blue
self.alpha = alpha
}
}
2 changes: 2 additions & 0 deletions Sources/LanguageServerProtocol/Messages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public let builtinRequests: [_RequestType.Type] = [
DocumentOnTypeFormatting.self,
FoldingRangeRequest.self,
DocumentSymbolRequest.self,
DocumentColorRequest.self,
ColorPresentationRequest.self,

// MARK: LSP Extension Requests

Expand Down
8 changes: 7 additions & 1 deletion Sources/LanguageServerProtocol/ServerCapabilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ public struct ServerCapabilities: Codable, Hashable {
/// Whether the server provides "textDocument/documentSymbol"
public var documentSymbolProvider: Bool?

/// Whether the server provides "textDocument/documentColor" and "textDocument/colorPresentation".
public var colorProvider: Bool?

// TODO: fill-in the rest.

public init(
Expand All @@ -58,7 +61,8 @@ public struct ServerCapabilities: Codable, Hashable {
documentRangeFormattingProvider: Bool? = nil,
documentOnTypeFormattingProvider: DocumentOnTypeFormattingOptions? = nil,
foldingRangeProvider: Bool? = nil,
documentSymbolProvider: Bool? = nil
documentSymbolProvider: Bool? = nil,
colorProvider: Bool? = nil
)
{
self.textDocumentSync = textDocumentSync
Expand All @@ -72,6 +76,7 @@ public struct ServerCapabilities: Codable, Hashable {
self.documentOnTypeFormattingProvider = documentOnTypeFormattingProvider
self.foldingRangeProvider = foldingRangeProvider
self.documentSymbolProvider = documentSymbolProvider
self.colorProvider = colorProvider
}

public init(from decoder: Decoder) throws {
Expand All @@ -81,6 +86,7 @@ public struct ServerCapabilities: Codable, Hashable {
self.definitionProvider = try container.decodeIfPresent(Bool.self, forKey: .definitionProvider)
self.foldingRangeProvider = try container.decodeIfPresent(Bool.self, forKey: .foldingRangeProvider)
self.documentSymbolProvider = try container.decodeIfPresent(Bool.self, forKey: .documentSymbolProvider)
self.colorProvider = try container.decodeIfPresent(Bool.self, forKey: .colorProvider)

if let textDocumentSync = try? container.decode(TextDocumentSyncOptions.self, forKey: .textDocumentSync) {
self.textDocumentSync = textDocumentSync
Expand Down
13 changes: 12 additions & 1 deletion Sources/SourceKit/SourceKitServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ public final class SourceKitServer: LanguageServer {
registerWorkspaceRequest(SourceKitServer.foldingRange)
registerWorkspaceRequest(SourceKitServer.symbolInfo)
registerWorkspaceRequest(SourceKitServer.documentSymbol)
registerWorkspaceRequest(SourceKitServer.documentColor)
registerWorkspaceRequest(SourceKitServer.colorPresentation)
}

func registerWorkspaceRequest<R>(
Expand Down Expand Up @@ -260,7 +262,8 @@ extension SourceKitServer {
referencesProvider: true,
documentHighlightProvider: true,
foldingRangeProvider: true,
documentSymbolProvider: true
documentSymbolProvider: true,
colorProvider: true
)))
}

Expand Down Expand Up @@ -346,6 +349,14 @@ extension SourceKitServer {
toolchainTextDocumentRequest(req, workspace: workspace, fallback: nil)
}

func documentColor(_ req: Request<DocumentColorRequest>, workspace: Workspace) {
toolchainTextDocumentRequest(req, workspace: workspace, fallback: nil)
}

func colorPresentation(_ req: Request<ColorPresentationRequest>, workspace: Workspace) {
toolchainTextDocumentRequest(req, workspace: workspace, fallback: nil)
}

func definition(_ req: Request<DefinitionRequest>, workspace: Workspace) {
// FIXME: sending yourself a request isn't very convenient

Expand Down
106 changes: 105 additions & 1 deletion Sources/SourceKit/sourcekitd/SwiftLanguageServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ public final class SwiftLanguageServer: LanguageServer {
_register(SwiftLanguageServer.foldingRange)
_register(SwiftLanguageServer.symbolInfo)
_register(SwiftLanguageServer.documentSymbol)
_register(SwiftLanguageServer.documentColor)
_register(SwiftLanguageServer.colorPresentation)
}

func getDiagnostic(_ diag: SKResponseDictionary, for snapshot: DocumentSnapshot) -> Diagnostic? {
Expand Down Expand Up @@ -193,7 +195,8 @@ extension SwiftLanguageServer {
referencesProvider: nil,
documentHighlightProvider: true,
foldingRangeProvider: true,
documentSymbolProvider: true
documentSymbolProvider: true,
colorProvider: true
)))
}

Expand Down Expand Up @@ -553,6 +556,107 @@ extension SwiftLanguageServer {
return
}

func documentColor(_ req: Request<DocumentColorRequest>) {
guard let snapshot = documentManager.latestSnapshot(req.params.textDocument.url) else {
log("failed to find snapshot for url \(req.params.textDocument.url)")
req.reply(nil)
return
}

let skreq = SKRequestDictionary(sourcekitd: sourcekitd)
skreq[keys.request] = requests.editor_open
skreq[keys.name] = "DocumentColor:" + snapshot.document.url.path
skreq[keys.sourcetext] = snapshot.text
skreq[keys.syntactic_only] = 1

let handle = sourcekitd.send(skreq) { [weak self] result in
guard let self = self else { return }
guard let dict = result.success else {
req.reply(.failure(result.failure!))
return
}

guard let results: SKResponseArray = dict[self.keys.substructure] else {
return req.reply([])
}

func colorInformation(dict: SKResponseDictionary) -> ColorInformation? {
guard let kind: sourcekitd_uid_t = dict[self.keys.kind],
kind == self.values.expr_object_literal,
let name: String = dict[self.keys.name],
name == "colorLiteral",
let offset: Int = dict[self.keys.offset],
let start: Position = snapshot.positionOf(utf8Offset: offset),
let length: Int = dict[self.keys.length],
let end: Position = snapshot.positionOf(utf8Offset: offset + length),
let substructure: SKResponseArray = dict[self.keys.substructure] else {
return nil
}
var red, green, blue, alpha: Double?
substructure.forEach{ (i: Int, value: SKResponseDictionary) in
guard let name: String = value[self.keys.name],
let bodyoffset: Int = value[self.keys.bodyoffset],
let bodylength: Int = value[self.keys.bodylength] else {
return true
}
let view = snapshot.text.utf8
let bodyStart = view.index(view.startIndex, offsetBy: bodyoffset)
let bodyEnd = view.index(view.startIndex, offsetBy: bodyoffset+bodylength)
let value = String(view[bodyStart..<bodyEnd]).flatMap(Double.init)
switch name {
case "red":
red = value
case "green":
green = value
case "blue":
blue = value
case "alpha":
alpha = value
default:
break
}
return true
}
if let red = red,
let green = green,
let blue = blue,
let alpha = alpha {
let color = Color(red: red, green: green, blue: blue, alpha: alpha)
return ColorInformation(range: start..<end, color: color)
} else {
return nil
}
}

func colorInformation(array: SKResponseArray) -> [ColorInformation] {
var result: [ColorInformation] = []
array.forEach { (i: Int, value: SKResponseDictionary) in
if let documentSymbol = colorInformation(dict: value) {
result.append(documentSymbol)
} else if let substructure: SKResponseArray = value[self.keys.substructure] {
result += colorInformation(array: substructure)
}
return true
}
return result
}

req.reply(colorInformation(array: results))
}
// FIXME: cancellation
_ = handle
}

func colorPresentation(_ req: Request<ColorPresentationRequest>) {
let color = req.params.color
// Empty string as a label breaks VSCode color picker
let label = "Color Literal"
let newText = "#colorLiteral(red: \(color.red), green: \(color.green), blue: \(color.blue), alpha: \(color.alpha))"
let textEdit = TextEdit(range: req.params.range.asRange, newText: newText)
let presentation = ColorPresentation(label: label, textEdit: textEdit, additionalTextEdits: nil)
req.reply([presentation])
}

func documentSymbolHighlight(_ req: Request<DocumentHighlightRequest>) {

guard let snapshot = documentManager.latestSnapshot(req.params.textDocument.url) else {
Expand Down
2 changes: 2 additions & 0 deletions Sources/SourceKit/sourcekitd/SwiftSourceKitFramework.swift
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ struct sourcekitd_values {
let syntaxtype_comment_url: sourcekitd_uid_t
let syntaxtype_doccomment: sourcekitd_uid_t
let syntaxtype_doccomment_field: sourcekitd_uid_t
let expr_object_literal: sourcekitd_uid_t

let kind_keyword: sourcekitd_uid_t

Expand Down Expand Up @@ -448,6 +449,7 @@ struct sourcekitd_values {
syntaxtype_comment_url = api.uid_get_from_cstr("source.lang.swift.syntaxtype.comment.url")!
syntaxtype_doccomment = api.uid_get_from_cstr("source.lang.swift.syntaxtype.doccomment")!
syntaxtype_doccomment_field = api.uid_get_from_cstr("source.lang.swift.syntaxtype.doccomment.field")!
expr_object_literal = api.uid_get_from_cstr("source.lang.swift.expr.object_literal")!

kind_keyword = api.uid_get_from_cstr("source.lang.swift.keyword")!
}
Expand Down
Loading