Skip to content

Commit 36ec508

Browse files
implement DocumentFormattingRequest
1 parent 2840c7d commit 36ec508

File tree

17 files changed

+275
-1
lines changed

17 files changed

+275
-1
lines changed

Package.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ let package = Package(
4747
"SourceKitD",
4848
"SKSwiftPMWorkspace",
4949
"SwiftToolsSupport-auto",
50+
"SwiftFormat",
5051
]
5152
),
5253

@@ -231,11 +232,13 @@ if getenv("SWIFTCI_USE_LOCAL_DEPS") == nil {
231232
.package(url: "https://github.com/apple/indexstore-db.git", .branch("master")),
232233
.package(url: "https://github.com/apple/swift-package-manager.git", .branch("master")),
233234
.package(url: "https://github.com/apple/swift-tools-support-core.git", .branch("master")),
235+
.package(url: "https://github.com/apple/swift-format.git", .branch("master")),
234236
]
235237
} else {
236238
package.dependencies += [
237239
.package(path: "../indexstore-db"),
238240
.package(path: "../swiftpm"),
239241
.package(path: "../swiftpm/swift-tools-support-core"),
242+
.package(path: "../swift-format"),
240243
]
241244
}

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ SourceKit-LSP is still in early development, so you may run into rough edges wit
4646
| Workspace Symbols || |
4747
| Global Rename || |
4848
| Local Refactoring || |
49-
| Formatting | | |
49+
| Formatting | | Whole file at once only. |
5050
| Folding || |
5151
| Syntax Highlighting || Not currently part of LSP. |
5252
| Document Symbols || |
@@ -59,3 +59,5 @@ SourceKit-LSP is still in early development, so you may run into rough edges wit
5959

6060
* SourceKit-LSP does not update its global index in the background, but instead relies on indexing-while-building to provide data. This only affects global queries like find-references and jump-to-definition.
6161
* **Workaround**: build the project to update the index
62+
63+
* Formatting uses swift-format, which requires a specific toolchain version. You can learn more at the [swift-format readme.](https://github.com/apple/swift-format#matching-swift-format-to-your-swift-version)

Sources/LanguageServerProtocol/Requests/FormattingRequests.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ public struct DocumentFormattingRequest: TextDocumentRequest, Hashable {
2828

2929
/// Options to customize the formatting.
3030
public var options: FormattingOptions
31+
32+
public init(textDocument: TextDocumentIdentifier, options: FormattingOptions) {
33+
self.textDocument = textDocument
34+
self.options = options
35+
}
3136
}
3237

3338
/// Request to format a specified range within a document.
@@ -97,4 +102,9 @@ public struct FormattingOptions: Codable, Hashable {
97102

98103
/// Whether to use spaces instead of tabs.
99104
public var insertSpaces: Bool
105+
106+
public init(tabSize: Int, insertSpaces: Bool) {
107+
self.tabSize = tabSize
108+
self.insertSpaces = insertSpaces
109+
}
100110
}

Sources/SourceKitLSP/Clang/ClangLanguageServer.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,10 @@ extension ClangLanguageServerShim {
243243
forwardRequest(req, to: clangd)
244244
}
245245

246+
func documentFormatting(_ req: Request<DocumentFormattingRequest>) {
247+
forwardRequest(req, to: clangd)
248+
}
249+
246250
func documentColor(_ req: Request<DocumentColorRequest>) {
247251
if capabilities?.colorProvider?.isSupported == true {
248252
forwardRequest(req, to: clangd)

Sources/SourceKitLSP/SourceKitServer.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ public final class SourceKitServer: LanguageServer {
9393
registerToolchainTextDocumentRequest(SourceKitServer.documentSymbolHighlight, nil)
9494
registerToolchainTextDocumentRequest(SourceKitServer.foldingRange, nil)
9595
registerToolchainTextDocumentRequest(SourceKitServer.documentSymbol, nil)
96+
registerToolchainTextDocumentRequest(SourceKitServer.documentFormatting, nil)
9697
registerToolchainTextDocumentRequest(SourceKitServer.documentColor, [])
9798
registerToolchainTextDocumentRequest(SourceKitServer.colorPresentation, [])
9899
registerToolchainTextDocumentRequest(SourceKitServer.codeAction, nil)
@@ -475,6 +476,7 @@ extension SourceKitServer {
475476
codeActionOptions: CodeActionOptions(codeActionKinds: nil),
476477
supportsCodeActions: true
477478
)),
479+
documentFormattingProvider: true,
478480
colorProvider: .bool(true),
479481
foldingRangeProvider: .bool(true),
480482
executeCommandProvider: ExecuteCommandOptions(
@@ -717,6 +719,14 @@ extension SourceKitServer {
717719
languageService.documentSymbol(req)
718720
}
719721

722+
func documentFormatting(
723+
_ req: Request<DocumentFormattingRequest>,
724+
workspace: Workspace,
725+
languageService: ToolchainLanguageServer
726+
) {
727+
languageService.documentFormatting(req)
728+
}
729+
720730
func documentColor(
721731
_ req: Request<DocumentColorRequest>,
722732
workspace: Workspace,

Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import SKCore
1818
import SKSupport
1919
import SourceKitD
2020
import TSCBasic
21+
import SwiftFormat
22+
import SwiftFormatConfiguration
2123

2224
fileprivate extension Range {
2325
/// Checks if this range overlaps with the other range, counting an overlap with an empty range as a valid overlap.
@@ -209,6 +211,7 @@ extension SwiftLanguageServer {
209211
clientCapabilities: initialize.capabilities.textDocument?.codeAction,
210212
codeActionOptions: CodeActionOptions(codeActionKinds: [.quickFix, .refactor]),
211213
supportsCodeActions: true)),
214+
documentFormattingProvider: true,
212215
colorProvider: .bool(true),
213216
foldingRangeProvider: .bool(true),
214217
executeCommandProvider: ExecuteCommandOptions(
@@ -742,6 +745,48 @@ extension SwiftLanguageServer {
742745
}
743746
}
744747

748+
public func documentFormatting(_ req: Request<DocumentFormattingRequest>) {
749+
guard let file = req.params.textDocument.uri.fileURL else {
750+
req.reply(nil)
751+
return
752+
}
753+
guard let snapshot = self.documentManager.latestSnapshot(req.params.textDocument.uri) else {
754+
log("failed to find snapshot for url \(req.params.textDocument.uri)")
755+
req.reply(nil)
756+
return
757+
}
758+
let options = req.params.options
759+
self.queue.async {
760+
let configuration: SwiftFormatConfiguration.Configuration
761+
// try to load swift-format configuration from a ".swift-format" file
762+
// if it fails, use values provided by the lsp
763+
if let configUrl = Configuration.url(forConfigurationFileApplyingTo: file),
764+
let config = try? Configuration(contentsOf: configUrl) {
765+
configuration = config
766+
} else {
767+
var config = SwiftFormatConfiguration.Configuration()
768+
config.indentation = options.insertSpaces ? .spaces(options.tabSize) : .tabs(1)
769+
config.tabWidth = options.tabSize
770+
configuration = config
771+
}
772+
773+
let formatter = SwiftFormat.SwiftFormatter(configuration: configuration)
774+
do {
775+
guard let lastLine = snapshot.lineTable.last else {
776+
req.reply(nil)
777+
return
778+
}
779+
let lastPosition = Position(line: snapshot.lineTable.count-1, utf16index: lastLine.utf16.count)
780+
var edit = TextEdit(range: Position(line: 0, utf16index: 0)..<lastPosition, newText: "")
781+
try formatter.format(source: snapshot.text, assumingFileURL: file, to: &edit.newText)
782+
req.reply([edit])
783+
} catch {
784+
log("failed to format document: \(error)", level: .error)
785+
req.reply(nil)
786+
}
787+
}
788+
}
789+
745790
public func documentColor(_ req: Request<DocumentColorRequest>) {
746791
let keys = self.keys
747792

Sources/SourceKitLSP/ToolchainLanguageServer.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public protocol ToolchainLanguageServer: AnyObject {
5959
func documentSymbolHighlight(_ req: Request<DocumentHighlightRequest>)
6060
func foldingRange(_ req: Request<FoldingRangeRequest>)
6161
func documentSymbol(_ req: Request<DocumentSymbolRequest>)
62+
func documentFormatting(_ req: Request<DocumentFormattingRequest>)
6263
func documentColor(_ req: Request<DocumentColorRequest>)
6364
func colorPresentation(_ req: Request<ColorPresentationRequest>)
6465
func codeAction(_ req: Request<CodeActionRequest>)

Tests/INPUTS/Formatting/.swift-format

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
This file is intentionally an invalid swift-format configuration file. It is used to test
2+
the behavior when program cannot find a valid file. We cannot just left this directory blank,
3+
because swift-format searches for the configuration in parent directories, and it's possible
4+
that user has another configuration file somewhere outside the Tests.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"version": 1,
3+
"indentation": {
4+
"spaces": 1
5+
},
6+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/*Directory*/
2+
struct Directory {
3+
var bar = 123
4+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"version": 1,
3+
"indentation": {
4+
"spaces": 4
5+
},
6+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/*NestedWithConfig*/
2+
struct NestedWithConfig {
3+
var bar = 123
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/*NestedWithoutConfig*/
2+
struct NestedWithoutConfig {
3+
var bar = 123
4+
}

Tests/INPUTS/Formatting/Root.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/*Root*/
2+
struct Root {
3+
var bar = 123
4+
}

Tests/INPUTS/Formatting/project.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"sources": [
3+
"Root.swift",
4+
"Directory/Directory.swift",
5+
"Directory/NestedWithConfig/NestedWithConfig.swift",
6+
"Directory/NestedWithoutConfig/NestedWithoutConfig.swift"
7+
]
8+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import LanguageServerProtocol
14+
import SKTestSupport
15+
import XCTest
16+
import ISDBTestSupport
17+
18+
19+
// Note that none of the indentation values choosen are equal to the default
20+
// indentation level, which is two spaces.
21+
final class FormattingTests: XCTestCase {
22+
var workspace: SKTibsTestWorkspace! = nil
23+
24+
func initialize() throws {
25+
workspace = try XCTUnwrap(staticSourceKitTibsWorkspace(name: "Formatting"))
26+
try workspace.buildAndIndex()
27+
try workspace.openDocument(workspace.testLoc("Root").url, language: .swift)
28+
try workspace.openDocument(workspace.testLoc("Directory").url, language: .swift)
29+
try workspace.openDocument(workspace.testLoc("NestedWithConfig").url, language: .swift)
30+
try workspace.openDocument(workspace.testLoc("NestedWithoutConfig").url, language: .swift)
31+
32+
sleep(1) // FIXME: openDocument is asynchronous, wait for it to finish
33+
}
34+
override func tearDown() {
35+
workspace = nil
36+
}
37+
38+
func performFormattingRequest(file url: URL, options: FormattingOptions) throws -> [TextEdit]? {
39+
let request = DocumentFormattingRequest(
40+
textDocument: TextDocumentIdentifier(url),
41+
options: options
42+
)
43+
return try workspace.sk.sendSync(request)
44+
}
45+
46+
func testSpaces() throws {
47+
XCTAssertNoThrow(try initialize())
48+
let url = workspace.testLoc("Root").url
49+
let options = FormattingOptions(tabSize: 3, insertSpaces: true)
50+
let edits = try XCTUnwrap(performFormattingRequest(file: url, options: options))
51+
XCTAssertEqual(edits.count, 1)
52+
let firstEdit = try XCTUnwrap(edits.first)
53+
XCTAssertEqual(firstEdit.range.lowerBound, Position(line: 0, utf16index: 0))
54+
XCTAssertEqual(firstEdit.range.upperBound, Position(line: 3, utf16index: 1))
55+
// var bar needs to be indented with three spaces
56+
// which is the value from lsp
57+
XCTAssertEqual(firstEdit.newText, """
58+
/*Root*/
59+
struct Root {
60+
var bar = 123
61+
}
62+
63+
""")
64+
}
65+
66+
func testTabs() throws {
67+
try initialize()
68+
let url = workspace.testLoc("Root").url
69+
let options = FormattingOptions(tabSize: 3, insertSpaces: false)
70+
let edits = try XCTUnwrap(performFormattingRequest(file: url, options: options))
71+
XCTAssertEqual(edits.count, 1)
72+
let firstEdit = try XCTUnwrap(edits.first)
73+
XCTAssertEqual(firstEdit.range.lowerBound, Position(line: 0, utf16index: 0))
74+
XCTAssertEqual(firstEdit.range.upperBound, Position(line: 3, utf16index: 1))
75+
// var bar needs to be indented with a tab
76+
// which is the value from lsp
77+
XCTAssertEqual(firstEdit.newText, """
78+
/*Root*/
79+
struct Root {
80+
\tvar bar = 123
81+
}
82+
83+
""")
84+
}
85+
86+
func testConfigFile() throws {
87+
XCTAssertNoThrow(try initialize())
88+
let url = workspace.testLoc("Directory").url
89+
let options = FormattingOptions(tabSize: 3, insertSpaces: true)
90+
let edits = try XCTUnwrap(performFormattingRequest(file: url, options: options))
91+
XCTAssertEqual(edits.count, 1)
92+
let firstEdit = try XCTUnwrap(edits.first)
93+
XCTAssertEqual(firstEdit.range.lowerBound, Position(line: 0, utf16index: 0))
94+
XCTAssertEqual(firstEdit.range.upperBound, Position(line: 3, utf16index: 1))
95+
// var bar needs to be indented with one space
96+
// which is the value from ".swift-format" in "Directory"
97+
XCTAssertEqual(firstEdit.newText, """
98+
/*Directory*/
99+
struct Directory {
100+
var bar = 123
101+
}
102+
103+
""")
104+
}
105+
106+
func testConfigFileInParentDirectory() throws {
107+
XCTAssertNoThrow(try initialize())
108+
let url = workspace.testLoc("NestedWithoutConfig").url
109+
let options = FormattingOptions(tabSize: 3, insertSpaces: true)
110+
let edits = try XCTUnwrap(performFormattingRequest(file: url, options: options))
111+
XCTAssertEqual(edits.count, 1)
112+
let firstEdit = try XCTUnwrap(edits.first)
113+
XCTAssertEqual(firstEdit.range.lowerBound, Position(line: 0, utf16index: 0))
114+
XCTAssertEqual(firstEdit.range.upperBound, Position(line: 3, utf16index: 1))
115+
// var bar needs to be indented with one space
116+
// which is the value from ".swift-format" in "Directory"
117+
XCTAssertEqual(firstEdit.newText, """
118+
/*NestedWithoutConfig*/
119+
struct NestedWithoutConfig {
120+
var bar = 123
121+
}
122+
123+
""")
124+
}
125+
126+
func testConfigFileInNestedDirectory() throws {
127+
XCTAssertNoThrow(try initialize())
128+
let url = workspace.testLoc("NestedWithConfig").url
129+
let options = FormattingOptions(tabSize: 3, insertSpaces: true)
130+
let edits = try XCTUnwrap(performFormattingRequest(file: url, options: options))
131+
XCTAssertEqual(edits.count, 1)
132+
let firstEdit = try XCTUnwrap(edits.first)
133+
XCTAssertEqual(firstEdit.range.lowerBound, Position(line: 0, utf16index: 0))
134+
XCTAssertEqual(firstEdit.range.upperBound, Position(line: 3, utf16index: 1))
135+
// var bar needs to be indented with four spaces
136+
// which is the value from ".swift-format" in "NestedWithConfig"
137+
XCTAssertEqual(firstEdit.newText, """
138+
/*NestedWithConfig*/
139+
struct NestedWithConfig {
140+
var bar = 123
141+
}
142+
143+
""")
144+
}
145+
}

0 commit comments

Comments
 (0)