Skip to content

Commit 3b6ced8

Browse files
authored
Merge pull request #1815 from MahdiBM/mmbm-on-type-formatting
Handle on-type formatting requests
2 parents 6c5f5c9 + a2eb7b9 commit 3b6ced8

File tree

8 files changed

+190
-10
lines changed

8 files changed

+190
-10
lines changed

Diff for: Documentation/Configuration File.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,5 @@ The structure of the file is currently not guaranteed to be stable. Options may
5151
- `noLazy`: Prepare a target without generating object files but do not do lazy type checking and function body skipping
5252
- `enabled`: Prepare a target without generating object files and the like
5353
- `cancelTextDocumentRequestsOnEditAndClose: bool`: Whether sending a `textDocument/didChange` or `textDocument/didClose` notification for a document should cancel all pending requests for that document.
54-
- `experimentalFeatures: string[]`: Experimental features to enable
54+
- `experimentalFeatures: string[]`: Experimental features to enable. Available features: on-type-formatting
5555
- `swiftPublishDiagnosticsDebounceDuration: double`: The time that `SwiftLanguageService` should wait after an edit before starting to compute diagnostics and sending a `PublishDiagnosticsNotification`.

Diff for: Sources/SKOptions/ExperimentalFeatures.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13-
/// An experimental feature that can be enabled by passing `--experimental-feature` to `sourcekit-lsp` on the command
14-
/// line. The raw value of this feature is how it is named on the command line.
13+
/// An experimental feature that can be enabled by passing `--experimental-feature`
14+
/// to `sourcekit-lsp` on the command line or through the configuration file.
15+
/// The raw value of this feature is how it is named on the command line and in the configuration file.
1516
public enum ExperimentalFeature: String, Codable, Sendable, CaseIterable {
16-
/* This is here to silence the errors when the enum doesn't have any cases */
17-
case exampleCase = "example-case"
17+
case onTypeFormatting = "on-type-formatting"
1818
}

Diff for: Sources/SourceKitLSP/Clang/ClangLanguageService.swift

+4
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,10 @@ extension ClangLanguageService {
581581
return try await forwardRequestToClangd(req)
582582
}
583583

584+
func documentOnTypeFormatting(_ req: DocumentOnTypeFormattingRequest) async throws -> [TextEdit]? {
585+
return try await forwardRequestToClangd(req)
586+
}
587+
584588
func codeAction(_ req: CodeActionRequest) async throws -> CodeActionRequestResponse? {
585589
return try await forwardRequestToClangd(req)
586590
}

Diff for: Sources/SourceKitLSP/LanguageService.swift

+1
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ package protocol LanguageService: AnyObject, Sendable {
210210
func documentDiagnostic(_ req: DocumentDiagnosticsRequest) async throws -> DocumentDiagnosticReport
211211
func documentFormatting(_ req: DocumentFormattingRequest) async throws -> [TextEdit]?
212212
func documentRangeFormatting(_ req: DocumentRangeFormattingRequest) async throws -> [TextEdit]?
213+
func documentOnTypeFormatting(_ req: DocumentOnTypeFormattingRequest) async throws -> [TextEdit]?
213214

214215
// MARK: - Rename
215216

Diff for: Sources/SourceKitLSP/SourceKitLSPServer.swift

+20-2
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,8 @@ extension SourceKitLSPServer: QueueBasedMessageHandler {
720720
await self.handleRequest(for: request, requestHandler: self.documentFormatting)
721721
case let request as RequestAndReply<DocumentRangeFormattingRequest>:
722722
await self.handleRequest(for: request, requestHandler: self.documentRangeFormatting)
723+
case let request as RequestAndReply<DocumentOnTypeFormattingRequest>:
724+
await self.handleRequest(for: request, requestHandler: self.documentOnTypeFormatting)
723725
case let request as RequestAndReply<DocumentHighlightRequest>:
724726
await self.handleRequest(for: request, requestHandler: self.documentSymbolHighlight)
725727
case let request as RequestAndReply<DocumentSemanticTokensDeltaRequest>:
@@ -971,7 +973,8 @@ extension SourceKitLSPServer {
971973
let result = InitializeResult(
972974
capabilities: await self.serverCapabilities(
973975
for: req.capabilities,
974-
registry: self.capabilityRegistry!
976+
registry: self.capabilityRegistry!,
977+
options: options
975978
)
976979
)
977980
logger.logFullObjectInMultipleLogMessages(header: "Initialize response", AnyRequestType(request: req))
@@ -980,7 +983,8 @@ extension SourceKitLSPServer {
980983

981984
func serverCapabilities(
982985
for client: ClientCapabilities,
983-
registry: CapabilityRegistry
986+
registry: CapabilityRegistry,
987+
options: SourceKitLSPOptions
984988
) async -> ServerCapabilities {
985989
let completionOptions =
986990
await registry.clientHasDynamicCompletionRegistration
@@ -990,6 +994,11 @@ extension SourceKitLSPServer {
990994
triggerCharacters: [".", "("]
991995
)
992996

997+
let onTypeFormattingOptions =
998+
options.hasExperimentalFeature(.onTypeFormatting)
999+
? DocumentOnTypeFormattingOptions(triggerCharacters: ["\n", "\r\n", "\r", "{", "}", ";", ".", ":", "#"])
1000+
: nil
1001+
9931002
let foldingRangeOptions =
9941003
await registry.clientHasDynamicFoldingRangeRegistration
9951004
? nil
@@ -1039,6 +1048,7 @@ extension SourceKitLSPServer {
10391048
codeLensProvider: CodeLensOptions(),
10401049
documentFormattingProvider: .value(DocumentFormattingOptions(workDoneProgress: false)),
10411050
documentRangeFormattingProvider: .value(DocumentRangeFormattingOptions(workDoneProgress: false)),
1051+
documentOnTypeFormattingProvider: onTypeFormattingOptions,
10421052
renameProvider: .value(RenameOptions(prepareProvider: true)),
10431053
colorProvider: .bool(true),
10441054
foldingRangeProvider: foldingRangeOptions,
@@ -1537,6 +1547,14 @@ extension SourceKitLSPServer {
15371547
return try await languageService.documentRangeFormatting(req)
15381548
}
15391549

1550+
func documentOnTypeFormatting(
1551+
_ req: DocumentOnTypeFormattingRequest,
1552+
workspace: Workspace,
1553+
languageService: LanguageService
1554+
) async throws -> [TextEdit]? {
1555+
return try await languageService.documentOnTypeFormatting(req)
1556+
}
1557+
15401558
func colorPresentation(
15411559
_ req: ColorPresentationRequest,
15421560
workspace: Workspace,

Diff for: Sources/SourceKitLSP/Swift/DocumentFormatting.swift

+27-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import Foundation
1515
package import LanguageServerProtocol
1616
import LanguageServerProtocolExtensions
1717
import SKLogging
18+
import SKUtilities
1819
import SwiftExtensions
1920
import SwiftParser
2021
import SwiftSyntax
@@ -28,6 +29,7 @@ import Foundation
2829
import LanguageServerProtocol
2930
import LanguageServerProtocolExtensions
3031
import SKLogging
32+
import SKUtilities
3133
import SwiftExtensions
3234
import SwiftParser
3335
import SwiftSyntax
@@ -143,26 +145,44 @@ private func edits(from original: DocumentSnapshot, to edited: String) -> [TextE
143145
extension SwiftLanguageService {
144146
package func documentFormatting(_ req: DocumentFormattingRequest) async throws -> [TextEdit]? {
145147
return try await format(
148+
snapshot: documentManager.latestSnapshot(req.textDocument.uri),
146149
textDocument: req.textDocument,
147150
options: req.options
148151
)
149152
}
150153

151154
package func documentRangeFormatting(_ req: DocumentRangeFormattingRequest) async throws -> [TextEdit]? {
152155
return try await format(
156+
snapshot: documentManager.latestSnapshot(req.textDocument.uri),
153157
textDocument: req.textDocument,
154158
options: req.options,
155159
range: req.range
156160
)
157161
}
158162

163+
package func documentOnTypeFormatting(_ req: DocumentOnTypeFormattingRequest) async throws -> [TextEdit]? {
164+
let snapshot = try documentManager.latestSnapshot(req.textDocument.uri)
165+
guard let line = snapshot.lineTable.line(at: req.position.line) else {
166+
return nil
167+
}
168+
169+
let lineStartPosition = snapshot.position(of: line.startIndex, fromLine: req.position.line)
170+
let lineEndPosition = snapshot.position(of: line.endIndex, fromLine: req.position.line)
171+
172+
return try await format(
173+
snapshot: snapshot,
174+
textDocument: req.textDocument,
175+
options: req.options,
176+
range: lineStartPosition..<lineEndPosition
177+
)
178+
}
179+
159180
private func format(
181+
snapshot: DocumentSnapshot,
160182
textDocument: TextDocumentIdentifier,
161183
options: FormattingOptions,
162184
range: Range<Position>? = nil
163185
) async throws -> [TextEdit]? {
164-
let snapshot = try documentManager.latestSnapshot(textDocument.uri)
165-
166186
guard let swiftFormat else {
167187
throw ResponseError.unknown(
168188
"Formatting not supported because the toolchain is missing the swift-format executable"
@@ -176,9 +196,13 @@ extension SwiftLanguageService {
176196
swiftFormatConfiguration(for: textDocument.uri, options: options),
177197
]
178198
if let range {
199+
let utf8Range = snapshot.utf8OffsetRange(of: range)
200+
// swift-format takes an inclusive range, but Swift's `Range.upperBound` is exclusive.
201+
// Also make sure `upperBound` does not go less than `lowerBound`.
202+
let utf8UpperBound = max(utf8Range.lowerBound, utf8Range.upperBound - 1)
179203
args += [
180204
"--offsets",
181-
"\(snapshot.utf8Offset(of: range.lowerBound)):\(snapshot.utf8Offset(of: range.upperBound))",
205+
"\(utf8Range.lowerBound):\(utf8UpperBound)",
182206
]
183207
}
184208
let process = TSCBasic.Process(arguments: args)

Diff for: Sources/SourceKitLSP/Swift/SwiftLanguageService.swift

+6
Original file line numberDiff line numberDiff line change
@@ -1226,6 +1226,12 @@ extension DocumentSnapshot {
12261226
return Position(line: zeroBasedLine, utf16index: utf16Column)
12271227
}
12281228

1229+
/// Converts the given `String.Index` to a UTF-16-based line:column position.
1230+
func position(of index: String.Index, fromLine: Int = 0) -> Position {
1231+
let (line, utf16Column) = lineTable.lineAndUTF16ColumnOf(index, fromLine: fromLine)
1232+
return Position(line: line, utf16index: utf16Column)
1233+
}
1234+
12291235
// MARK: Position <-> AbsolutePosition
12301236

12311237
/// Converts the given UTF-8-offset-based `AbsolutePosition` to a UTF-16-based line:column.

Diff for: Tests/SourceKitLSPTests/OnTypeFormattingTests.swift

+127
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 - 2023 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 SKLogging
15+
import SKTestSupport
16+
import SourceKitLSP
17+
import XCTest
18+
19+
final class OnTypeFormattingTests: XCTestCase {
20+
func testOnlyFormatsSpecifiedLine() async throws {
21+
try await SkipUnless.toolchainContainsSwiftFormat()
22+
let testClient = try await TestSourceKitLSPClient()
23+
let uri = DocumentURI(for: .swift)
24+
25+
let positions = testClient.openDocument(
26+
"""
27+
func foo() {
28+
if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() {
29+
1️⃣// do stuff
30+
}
31+
}
32+
""",
33+
uri: uri
34+
)
35+
36+
let response = try await testClient.send(
37+
DocumentOnTypeFormattingRequest(
38+
textDocument: TextDocumentIdentifier(uri),
39+
position: positions["1️⃣"],
40+
ch: "\n",
41+
options: FormattingOptions(tabSize: 2, insertSpaces: true)
42+
)
43+
)
44+
45+
let edits = try XCTUnwrap(response)
46+
XCTAssertEqual(
47+
edits,
48+
[
49+
TextEdit(range: Range(positions["1️⃣"]), newText: " ")
50+
]
51+
)
52+
}
53+
54+
func testFormatsFullLineAndDoesNotFormatNextLine() async throws {
55+
try await SkipUnless.toolchainContainsSwiftFormat()
56+
let testClient = try await TestSourceKitLSPClient()
57+
let uri = DocumentURI(for: .swift)
58+
59+
let positions = testClient.openDocument(
60+
"""
61+
func foo() {
62+
1️⃣if let SomeReallyLongVar = 2️⃣ 3️⃣Some.More.Stuff(), let a = 4️⃣ 5️⃣myfunc() 6️⃣{
63+
}
64+
}
65+
""",
66+
uri: uri
67+
)
68+
69+
let response = try await testClient.send(
70+
DocumentOnTypeFormattingRequest(
71+
textDocument: TextDocumentIdentifier(uri),
72+
position: positions["6️⃣"],
73+
ch: "{",
74+
options: FormattingOptions(tabSize: 4, insertSpaces: true)
75+
)
76+
)
77+
78+
let edits = try XCTUnwrap(response)
79+
XCTAssertEqual(
80+
edits,
81+
[
82+
TextEdit(range: Range(positions["1️⃣"]), newText: " "),
83+
TextEdit(range: positions["2️⃣"]..<positions["3️⃣"], newText: ""),
84+
TextEdit(range: positions["4️⃣"]..<positions["5️⃣"], newText: ""),
85+
]
86+
)
87+
}
88+
89+
/// Should not remove empty lines when formatting is triggered on a new empty line.
90+
/// Otherwise could mess up writing code. You'd write {} and try to go into the braces to write more code,
91+
/// only for on-type formatting to immediately close the braces again.
92+
func testDoesNothingWhenInAnEmptyLine() async throws {
93+
try await SkipUnless.toolchainContainsSwiftFormat()
94+
let testClient = try await TestSourceKitLSPClient()
95+
let uri = DocumentURI(for: .swift)
96+
97+
let positions = testClient.openDocument(
98+
"""
99+
func foo() {
100+
if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() {
101+
102+
103+
1️⃣
104+
105+
106+
}
107+
}
108+
""",
109+
uri: uri
110+
)
111+
112+
let response = try await testClient.send(
113+
DocumentOnTypeFormattingRequest(
114+
textDocument: TextDocumentIdentifier(uri),
115+
position: positions["1️⃣"],
116+
ch: "\n",
117+
options: FormattingOptions(tabSize: 2, insertSpaces: true)
118+
)
119+
)
120+
121+
let edits = try XCTUnwrap(response)
122+
XCTAssertEqual(
123+
edits,
124+
[]
125+
)
126+
}
127+
}

0 commit comments

Comments
 (0)