mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-06 18:24:36 +01:00
Instead of logging errors in position translation ad-hoc at the caller’s side (and ofter forgetting to do so), log these errors in `LineTable`. To be able to debug where the position conversion error is coming from, also log the file name and line number of the caller. rdar://125545620
190 lines
6.4 KiB
Swift
190 lines
6.4 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2014 - 2020 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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import Foundation
|
|
import LSPLogging
|
|
import LanguageServerProtocol
|
|
import SwiftParser
|
|
import SwiftSyntax
|
|
|
|
import struct TSCBasic.AbsolutePath
|
|
import class TSCBasic.Process
|
|
import func TSCBasic.withTemporaryFile
|
|
|
|
fileprivate extension String {
|
|
init?(bytes: [UInt8], encoding: Encoding) {
|
|
let data = bytes.withUnsafeBytes { buffer in
|
|
guard let baseAddress = buffer.baseAddress else {
|
|
return Data()
|
|
}
|
|
return Data(bytes: baseAddress, count: buffer.count)
|
|
}
|
|
self.init(data: data, encoding: encoding)
|
|
}
|
|
}
|
|
|
|
/// If a parent directory of `fileURI` contains a `.swift-format` file, return the path to that file.
|
|
/// Otherwise, return `nil`.
|
|
private func swiftFormatFile(for fileURI: DocumentURI) -> AbsolutePath? {
|
|
guard var path = try? AbsolutePath(validating: fileURI.pseudoPath) else {
|
|
return nil
|
|
}
|
|
repeat {
|
|
path = path.parentDirectory
|
|
let configFile = path.appending(component: ".swift-format")
|
|
if FileManager.default.isReadableFile(atPath: configFile.pathString) {
|
|
return configFile
|
|
}
|
|
} while !path.isRoot
|
|
return nil
|
|
}
|
|
|
|
/// If a `.swift-format` file is discovered that applies to `fileURI`, return the path to that file.
|
|
/// Otherwise, return a JSON object containing the configuration parameters from `options`.
|
|
///
|
|
/// The result of this function can be passed to the `--configuration` parameter of swift-format.
|
|
private func swiftFormatConfiguration(
|
|
for fileURI: DocumentURI,
|
|
options: FormattingOptions
|
|
) throws -> String {
|
|
if let configFile = swiftFormatFile(for: fileURI) {
|
|
// If we find a .swift-format file, we ignore the options passed to us by the editor.
|
|
// Most likely, the editor inferred them from the current document and thus the options
|
|
// passed by the editor are most likely less correct than those in .swift-format.
|
|
return configFile.pathString
|
|
}
|
|
|
|
// The following options are not supported by swift-format and ignored:
|
|
// - trimTrailingWhitespace: swift-format always trims trailing whitespace
|
|
// - insertFinalNewline: swift-format always inserts a final newline to the file
|
|
// - trimFinalNewlines: swift-format always trims final newlines
|
|
|
|
if options.insertSpaces {
|
|
return """
|
|
{
|
|
"version": 1,
|
|
"tabWidth": \(options.tabSize),
|
|
"indentation": { "spaces": \(options.tabSize) }
|
|
}
|
|
"""
|
|
} else {
|
|
return """
|
|
{
|
|
"version": 1,
|
|
"tabWidth": \(options.tabSize),
|
|
"indentation": { "tabs": 1 }
|
|
}
|
|
"""
|
|
}
|
|
}
|
|
|
|
extension CollectionDifference.Change {
|
|
var offset: Int {
|
|
switch self {
|
|
case .insert(offset: let offset, element: _, associatedWith: _):
|
|
return offset
|
|
case .remove(offset: let offset, element: _, associatedWith: _):
|
|
return offset
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Compute the text edits that need to be made to transform `original` into `edited`.
|
|
private func edits(from original: DocumentSnapshot, to edited: String) -> [TextEdit] {
|
|
let difference = edited.utf8.difference(from: original.text.utf8)
|
|
|
|
let sequentialEdits = difference.map { change in
|
|
switch change {
|
|
case .insert(offset: let offset, element: let element, associatedWith: _):
|
|
IncrementalEdit(offset: offset, length: 0, replacement: [element])
|
|
case .remove(offset: let offset, element: _, associatedWith: _):
|
|
IncrementalEdit(offset: offset, length: 1, replacement: [])
|
|
}
|
|
}
|
|
|
|
let concurrentEdits = ConcurrentEdits(fromSequential: sequentialEdits)
|
|
|
|
// Map the offset-based edits to line-column based edits to be consumed by LSP
|
|
|
|
return concurrentEdits.edits.compactMap { (edit) -> TextEdit? in
|
|
guard let (startLine, startColumn) = original.lineTable.lineAndUTF16ColumnOf(utf8Offset: edit.offset) else {
|
|
return nil
|
|
}
|
|
guard let (endLine, endColumn) = original.lineTable.lineAndUTF16ColumnOf(utf8Offset: edit.endOffset) else {
|
|
return nil
|
|
}
|
|
guard let newText = String(bytes: edit.replacement, encoding: .utf8) else {
|
|
logger.fault("Failed to get String from UTF-8 bytes \(edit.replacement)")
|
|
return nil
|
|
}
|
|
|
|
return TextEdit(
|
|
range: Position(line: startLine, utf16index: startColumn)..<Position(line: endLine, utf16index: endColumn),
|
|
newText: newText
|
|
)
|
|
}
|
|
}
|
|
|
|
extension SwiftLanguageService {
|
|
public func documentFormatting(_ req: DocumentFormattingRequest) async throws -> [TextEdit]? {
|
|
let snapshot = try documentManager.latestSnapshot(req.textDocument.uri)
|
|
|
|
guard let swiftFormat else {
|
|
throw ResponseError.unknown(
|
|
"Formatting not supported because the toolchain is missing the swift-format executable"
|
|
)
|
|
}
|
|
|
|
let process = TSCBasic.Process(
|
|
args: swiftFormat.pathString,
|
|
"format",
|
|
"--configuration",
|
|
try swiftFormatConfiguration(for: req.textDocument.uri, options: req.options)
|
|
)
|
|
let writeStream = try process.launch()
|
|
|
|
// Send the file to format to swift-format's stdin. That way we don't have to write it to a file.
|
|
writeStream.send(snapshot.text)
|
|
try writeStream.close()
|
|
|
|
let result = try await process.waitUntilExit()
|
|
guard result.exitStatus == .terminated(code: 0) else {
|
|
let swiftFormatErrorMessage: String
|
|
switch result.stderrOutput {
|
|
case .success(let stderrBytes):
|
|
swiftFormatErrorMessage = String(bytes: stderrBytes, encoding: .utf8) ?? "unknown error"
|
|
case .failure(let error):
|
|
swiftFormatErrorMessage = String(describing: error)
|
|
}
|
|
throw ResponseError.unknown(
|
|
"""
|
|
Running swift-format failed
|
|
\(swiftFormatErrorMessage)
|
|
"""
|
|
)
|
|
}
|
|
let formattedBytes: [UInt8]
|
|
switch result.output {
|
|
case .success(let bytes):
|
|
formattedBytes = bytes
|
|
case .failure(let error):
|
|
throw error
|
|
}
|
|
|
|
guard let formattedString = String(bytes: formattedBytes, encoding: .utf8) else {
|
|
throw ResponseError.unknown("Failed to decode response from swift-format as UTF-8")
|
|
}
|
|
|
|
return edits(from: snapshot, to: formattedString)
|
|
}
|
|
}
|