Skip to content

Commit d109f8f

Browse files
committed
Support formatting of entire documents
Since swift-syntax no longer depends on the C++ parser library and thus swift-format also doesn’t, we can use swift-format to format an entire document. Fixes swiftlang#576 rdar://96159694
1 parent 4ff6e6a commit d109f8f

File tree

8 files changed

+232
-2
lines changed

8 files changed

+232
-2
lines changed

Package.swift

+4-1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ let package = Package(
6363
.product(name: "SwiftSyntax", package: "swift-syntax"),
6464
.product(name: "SwiftParser", package: "swift-syntax"),
6565
.product(name: "SwiftIDEUtils", package: "swift-syntax"),
66+
.product(name: "SwiftFormat", package: "swift-format"),
6667
],
6768
exclude: ["CMakeLists.txt"]),
6869

@@ -254,13 +255,15 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil {
254255
.package(url: "https://github.com/apple/swift-tools-support-core.git", branch: "main"),
255256
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.2"),
256257
.package(url: "https://github.com/apple/swift-syntax.git", branch: "main"),
258+
.package(url: "https://github.com/apple/swift-format.git", branch: "main"),
257259
]
258260
} else {
259261
package.dependencies += [
260262
.package(path: "../indexstore-db"),
261263
.package(name: "swift-package-manager", path: "../swiftpm"),
262264
.package(path: "../swift-tools-support-core"),
263265
.package(path: "../swift-argument-parser"),
264-
.package(path: "../swift-syntax")
266+
.package(path: "../swift-syntax"),
267+
.package(path: "../swift-format"),
265268
]
266269
}

README.md

+1-1
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
| Rename || |
4848
| Local Refactoring || |
49-
| Formatting | | |
49+
| Formatting | | Whole file at once only. |
5050
| Folding || |
5151
| Syntax Highlighting || Both syntactic and semantic tokens |
5252
| Document Symbols || |

Sources/LanguageServerProtocol/Requests/FormattingRequests.swift

+19
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.
@@ -106,4 +111,18 @@ public struct FormattingOptions: Codable, Hashable {
106111

107112
/// Trim all newlines after the final newline at the end of the file.
108113
public var trimFinalNewlines: Bool?
114+
115+
public init(
116+
tabSize: Int,
117+
insertSpaces: Bool,
118+
trimTrailingWhitespace: Bool? = nil,
119+
insertFinalNewline: Bool? = nil,
120+
trimFinalNewlines: Bool? = nil
121+
) {
122+
self.tabSize = tabSize
123+
self.insertSpaces = insertSpaces
124+
self.trimTrailingWhitespace = trimTrailingWhitespace
125+
self.insertFinalNewline = insertFinalNewline
126+
self.trimFinalNewlines = trimFinalNewlines
127+
}
109128
}

Sources/SourceKitLSP/Clang/ClangLanguageServer.swift

+4
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,10 @@ extension ClangLanguageServerShim {
537537
func executeCommand(_ req: Request<ExecuteCommandRequest>) {
538538
forwardRequestToClangdOnQueue(req)
539539
}
540+
541+
func formatDocument(_ req: Request<DocumentFormattingRequest>) {
542+
forwardRequestToClangdOnQueue(req)
543+
}
540544
}
541545

542546
/// Clang build settings derived from a `FileBuildSettingsChange`.

Sources/SourceKitLSP/SourceKitServer.swift

+11
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ public final class SourceKitServer: LanguageServer {
204204
registerToolchainTextDocumentRequest(SourceKitServer.inlayHint, [])
205205
registerToolchainTextDocumentRequest(SourceKitServer.documentDiagnostic,
206206
.full(.init(items: [])))
207+
registerToolchainTextDocumentRequest(SourceKitServer.formatDocument, nil)
207208
}
208209

209210
/// Register a `TextDocumentRequest` that requires a valid `Workspace`, `ToolchainLanguageServer`,
@@ -665,6 +666,7 @@ extension SourceKitServer {
665666
codeActionOptions: CodeActionOptions(codeActionKinds: nil),
666667
supportsCodeActions: true
667668
)),
669+
documentFormattingProvider: .value(DocumentFormattingOptions(workDoneProgress: false)),
668670
colorProvider: .bool(true),
669671
foldingRangeProvider: .bool(!registry.clientHasDynamicFoldingRangeRegistration),
670672
declarationProvider: .bool(true),
@@ -1188,6 +1190,15 @@ extension SourceKitServer {
11881190
languageService.executeCommand(request)
11891191
}
11901192

1193+
1194+
func formatDocument(
1195+
_ req: Request<DocumentFormattingRequest>,
1196+
workspace: Workspace,
1197+
languageService: ToolchainLanguageServer
1198+
) {
1199+
languageService.formatDocument(req)
1200+
}
1201+
11911202
func codeAction(
11921203
_ req: Request<CodeActionRequest>,
11931204
workspace: Workspace,

Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift

+65
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import SKSupport
1919
import SourceKitD
2020
import SwiftSyntax
2121
import SwiftParser
22+
import SwiftFormat
23+
import SwiftFormatConfiguration
2224

2325
#if os(Windows)
2426
import WinSDK
@@ -1461,6 +1463,69 @@ extension SwiftLanguageServer {
14611463
// FIXME: cancellation
14621464
_ = handle
14631465
}
1466+
1467+
public func formatDocument(_ req: Request<DocumentFormattingRequest>) {
1468+
self.queue.async {
1469+
do {
1470+
req.reply(try self.handleDocumentUpdate(req.params))
1471+
} catch let error as ResponseError {
1472+
req.reply(.failure(error))
1473+
} catch {
1474+
req.reply(.failure(ResponseError(code: .requestFailed, message: "Formatting failed: \(error.localizedDescription)")))
1475+
}
1476+
}
1477+
}
1478+
1479+
public func handleDocumentUpdate(_ req: DocumentFormattingRequest) throws -> [TextEdit]? {
1480+
guard let snapshot = documentManager.latestSnapshot(req.textDocument.uri) else {
1481+
return nil
1482+
}
1483+
1484+
var formatConfiguration: Configuration
1485+
if let fileURL = req.textDocument.uri.fileURL,
1486+
let configURL = Configuration.url(forConfigurationFileApplyingTo: fileURL),
1487+
let configuration = try? Configuration(contentsOf: configURL) {
1488+
formatConfiguration = configuration
1489+
} else {
1490+
formatConfiguration = Configuration()
1491+
}
1492+
formatConfiguration.tabWidth = req.options.tabSize
1493+
formatConfiguration.indentation = req.options.insertSpaces ? .spaces(req.options.tabSize) : .tabs(1)
1494+
1495+
// Unsupported options
1496+
if req.options.trimTrailingWhitespace == false {
1497+
throw ResponseError(code: .requestFailed, message: "swift-format does not support keeping trailing whitespace; set format option to trim trailing trivia to run the formatter")
1498+
}
1499+
if req.options.insertFinalNewline == false {
1500+
throw ResponseError(code: .requestFailed, message: "swift-format always inserts a final newline to the file; set option to insert a final newline to run the formatter")
1501+
}
1502+
if req.options.trimFinalNewlines == false {
1503+
throw ResponseError(code: .requestFailed, message: "swift-format always trims final newlines; set option to trim final newlines to run the formatter")
1504+
}
1505+
1506+
var outputBuffer = ""
1507+
let formatter = SwiftFormatter(configuration: formatConfiguration)
1508+
try formatter.format(source: snapshot.text, assumingFileURL: req.textDocument.uri.fileURL, to: &outputBuffer)
1509+
1510+
if snapshot.text == outputBuffer {
1511+
// No changes to the document.
1512+
return []
1513+
}
1514+
1515+
// It would be nice if swift-format could tell us which edits it performed.
1516+
// We also can’t compute the changes using `CollectionDifference` because it
1517+
// returns offset in the collection that were modified, but we need UTF-16
1518+
// indicies and it would be pretty hard to translate between the two.
1519+
// Thus, just return an edit that replaces the entire document.
1520+
let documentEndPos: Position
1521+
if snapshot.lineTable.isEmpty {
1522+
documentEndPos = Position(line: 0, utf16index: 0)
1523+
} else {
1524+
documentEndPos = Position(line: snapshot.lineTable.count - 1, utf16index: snapshot.lineTable.last!.utf16.count)
1525+
}
1526+
1527+
return [TextEdit(range: Position(line: 0, utf16index: 0)..<documentEndPos, newText: outputBuffer)]
1528+
}
14641529
}
14651530

14661531
extension SwiftLanguageServer: SKDNotificationHandler {

Sources/SourceKitLSP/ToolchainLanguageServer.swift

+1
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ public protocol ToolchainLanguageServer: AnyObject {
101101
// MARK: - Other
102102

103103
func executeCommand(_ req: Request<ExecuteCommandRequest>)
104+
func formatDocument(_ req: Request<DocumentFormattingRequest>)
104105

105106
/// Crash the language server. Should be used for crash recovery testing only.
106107
func _crash()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2018 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 LSPLogging
15+
import LSPTestSupport
16+
import SKTestSupport
17+
import SourceKitLSP
18+
import XCTest
19+
20+
final class FormattingTests: XCTestCase {
21+
22+
/// Connection and lifetime management for the service.
23+
var connection: TestSourceKitServer! = nil
24+
25+
/// The primary interface to make requests to the SourceKitServer.
26+
var sk: TestClient! = nil
27+
28+
var documentManager: DocumentManager! {
29+
connection.server!._documentManager
30+
}
31+
32+
override func setUp() {
33+
connection = TestSourceKitServer()
34+
sk = connection.client
35+
_ = try! sk.sendSync(
36+
InitializeRequest(
37+
processId: nil,
38+
rootPath: nil,
39+
rootURI: nil,
40+
initializationOptions: nil,
41+
capabilities: ClientCapabilities(
42+
workspace: nil,
43+
textDocument: TextDocumentClientCapabilities(
44+
codeAction: .init(
45+
codeActionLiteralSupport: .init(
46+
codeActionKind: .init(valueSet: [.quickFix])
47+
)
48+
),
49+
publishDiagnostics: .init(codeDescriptionSupport: true)
50+
)
51+
),
52+
trace: .off,
53+
workspaceFolders: nil
54+
)
55+
)
56+
}
57+
58+
override func tearDown() {
59+
sk = nil
60+
connection = nil
61+
}
62+
63+
func testFormatting() throws {
64+
let url = URL(fileURLWithPath: "/\(UUID())/a.swift")
65+
let uri = DocumentURI(url)
66+
67+
sk.send(DidOpenTextDocumentNotification(textDocument: TextDocumentItem(
68+
uri: uri,
69+
language: .swift,
70+
version: 1,
71+
text: """
72+
struct S {
73+
var foo: Int
74+
}
75+
""")))
76+
77+
let resp = try sk.sendSync(
78+
DocumentFormattingRequest(
79+
textDocument: TextDocumentIdentifier(url),
80+
options: FormattingOptions(
81+
tabSize: 2,
82+
insertSpaces: true
83+
)
84+
)
85+
)
86+
87+
let edits = try XCTUnwrap(resp)
88+
XCTAssertEqual(edits.count, 1)
89+
let edit = try XCTUnwrap(edits.first)
90+
XCTAssertEqual(edit.range, Position(line: 0, utf16index: 0)..<Position(line: 2, utf16index: 1))
91+
XCTAssertEqual(edit.newText, """
92+
struct S {
93+
var foo: Int
94+
}
95+
96+
""")
97+
}
98+
99+
func testFormattingNoEdits() throws {
100+
let url = URL(fileURLWithPath: "/\(UUID())/a.swift")
101+
let uri = DocumentURI(url)
102+
103+
sk.send(DidOpenTextDocumentNotification(textDocument: TextDocumentItem(
104+
uri: uri,
105+
language: .swift,
106+
version: 1,
107+
text: """
108+
struct S {
109+
var foo: Int
110+
}
111+
112+
""")))
113+
114+
let resp = try sk.sendSync(
115+
DocumentFormattingRequest(
116+
textDocument: TextDocumentIdentifier(url),
117+
options: FormattingOptions(
118+
tabSize: 2,
119+
insertSpaces: true
120+
)
121+
)
122+
)
123+
124+
let edits = try XCTUnwrap(resp)
125+
XCTAssertEqual(edits.count, 0)
126+
}
127+
}

0 commit comments

Comments
 (0)