Skip to content

Adopt swift-argument-parser for argument parsing #304

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 3 commits into from
Aug 28, 2020
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
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ find_package(TSC QUIET)
find_package(IndexStoreDB QUIET)
find_package(SwiftPM QUIET)
find_package(LLBuild QUIET)
find_package(ArgumentParser CONFIG REQUIRED)

include(SwiftSupport)

Expand Down
3 changes: 3 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ let package = Package(
.target(
name: "sourcekit-lsp",
dependencies: [
"ArgumentParser",
"LanguageServerProtocolJSONRPC",
"SourceKitLSP",
"SwiftToolsSupport-auto",
Expand Down Expand Up @@ -231,11 +232,13 @@ if getenv("SWIFTCI_USE_LOCAL_DEPS") == nil {
.package(url: "https://github.com/apple/indexstore-db.git", .branch("master")),
.package(url: "https://github.com/apple/swift-package-manager.git", .branch("master")),
.package(url: "https://github.com/apple/swift-tools-support-core.git", .branch("master")),
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "0.3.0")),
]
} else {
package.dependencies += [
.package(path: "../indexstore-db"),
.package(path: "../swiftpm"),
.package(path: "../swiftpm/swift-tools-support-core"),
.package(path: "../swift-argument-parser")
]
}
1 change: 1 addition & 0 deletions Sources/sourcekit-lsp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ endif()
add_executable(sourcekit-lsp
main.swift)
target_link_libraries(sourcekit-lsp PRIVATE
ArgumentParser
LanguageServerProtocolJSONRPC
SourceKitLSP
TSCUtility)
Expand Down
272 changes: 155 additions & 117 deletions Sources/sourcekit-lsp/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
//
//===----------------------------------------------------------------------===//

import ArgumentParser
import Csourcekitd // Not needed here, but fixes debugging...
import Dispatch
import Foundation
import LanguageServerProtocol
Expand All @@ -18,134 +20,170 @@ import LSPLogging
import SKCore
import SKSupport
import SourceKitLSP
import Csourcekitd // Not needed here, but fixes debugging...
import TSCBasic
import TSCLibc
import TSCUtility

extension LogLevel: ArgumentKind {
public static var completion: ShellCompletion {
return ShellCompletion.none
}
}

struct CommandLineOptions {
/// Options for the server.
var serverOptions: SourceKitServer.Options = SourceKitServer.Options()

/// Whether to wait for a response before handling the next request.
var syncRequests: Bool = false
}

func parseArguments() throws -> CommandLineOptions {
let arguments = Array(ProcessInfo.processInfo.arguments.dropFirst())
let parser = ArgumentParser(usage: "[options]", overview: "Language Server Protocol implementation for Swift and C-based languages")
let loggingOption = parser.add(option: "--log-level", kind: LogLevel.self, usage: "Set the logging level (debug|info|warning|error) [default: \(LogLevel.default)]")
let syncOption = parser.add(option: "--sync", kind: Bool.self) // For testing.
let buildConfigurationOption = parser.add(option: "--configuration", shortName: "-c", kind: BuildConfiguration.self, usage: "Build with configuration (debug|release) [default: debug]")
let buildPathOption = parser.add(option: "--build-path", kind: PathArgument.self, usage: "Specify build/cache directory")
let buildFlagsCc = parser.add(option: "-Xcc", kind: [String].self, strategy: .oneByOne, usage: "Pass flag through to all C compiler invocations")
let buildFlagsCxx = parser.add(option: "-Xcxx", kind: [String].self, strategy: .oneByOne, usage: "Pass flag through to all C++ compiler invocations")
let buildFlagsLinker = parser.add(option: "-Xlinker", kind: [String].self, strategy: .oneByOne, usage: "Pass flag through to all linker invocations")
let buildFlagsSwift = parser.add(option: "-Xswiftc", kind: [String].self, strategy: .oneByOne, usage: "Pass flag through to all Swift compiler invocations")
let clangdOptions = parser.add(option: "-Xclangd", kind: [String].self, strategy: .oneByOne, usage: "Pass options to clangd command-line")
let indexStorePath = parser.add(option: "-index-store-path", kind: PathArgument.self, usage: "Override index-store-path from the build system")
let indexDatabasePath = parser.add(option: "-index-db-path", kind: PathArgument.self, usage: "Override index-database-path from the build system")
let completionServerSideFiltering = parser.add(option: "-completion-server-side-filtering", kind: Bool.self, usage: "Whether to enable server-side filtering in code-completion")
let completionMaxResults = parser.add(option: "-completion-max-results", kind: Int.self, usage: "When server-side filtering is enabled, the maximum number of results to return")

let parsedArguments = try parser.parse(arguments)

var result = CommandLineOptions()

if let config = parsedArguments.get(buildConfigurationOption) {
result.serverOptions.buildSetup.configuration = config
}
if let buildPath = parsedArguments.get(buildPathOption)?.path {
result.serverOptions.buildSetup.path = buildPath
}
if let flags = parsedArguments.get(buildFlagsCc) {
result.serverOptions.buildSetup.flags.cCompilerFlags = flags
}
if let flags = parsedArguments.get(buildFlagsCxx) {
result.serverOptions.buildSetup.flags.cxxCompilerFlags = flags
}
if let flags = parsedArguments.get(buildFlagsLinker) {
result.serverOptions.buildSetup.flags.linkerFlags = flags
}
if let flags = parsedArguments.get(buildFlagsSwift) {
result.serverOptions.buildSetup.flags.swiftCompilerFlags = flags
extension AbsolutePath: ExpressibleByArgument {
public init?(argument: String) {
if let cwd = localFileSystem.currentWorkingDirectory {
self.init(argument, relativeTo: cwd)
} else {
guard let path = try? AbsolutePath(validating: argument) else {
return nil
}
self = path
}
}

if let options = parsedArguments.get(clangdOptions) {
result.serverOptions.clangdOptions = options
public static var defaultCompletionKind: CompletionKind {
// This type is most commonly used to select a directory, not a file.
// Specify '.file()' in an argument declaration when necessary.
.directory
}
}

if let path = parsedArguments.get(indexStorePath)?.path {
result.serverOptions.indexOptions.indexStorePath = path
}
if let path = parsedArguments.get(indexDatabasePath)?.path {
result.serverOptions.indexOptions.indexDatabasePath = path
}
extension LogLevel: ExpressibleByArgument {}
extension BuildConfiguration: ExpressibleByArgument {}

if let logLevel = parsedArguments.get(loggingOption) {
Logger.shared.currentLevel = logLevel
} else {
Logger.shared.setLogLevel(environmentVariable: "SOURCEKIT_LOGGING")
}
struct Main: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Language Server Protocol implementation for Swift and C-based languages"
)

if let sync = parsedArguments.get(syncOption) {
result.syncRequests = sync
/// Whether to wait for a response before handling the next request.
/// Used for testing.
@Flag(name: .customLong("sync"))
var syncRequests = false

@Option(help: "Set the logging level [debug|info|warning|error] (default: \(LogLevel.default))")
var logLevel: LogLevel?

@Option(
name: [.customLong("configuration"), .customShort("c")],
help: "Build with configuration [debug|release]"
)
var buildConfiguration = BuildConfiguration.debug

@Option(help: "Specify build/cache directory")
var buildPath: AbsolutePath?

@Option(
name: .customLong("Xcc", withSingleDash: true),
parsing: .unconditionalSingleValue,
help: "Pass flag through to all C compiler invocations"
)
var buildFlagsCc = [String]()

@Option(
name: .customLong("Xcxx", withSingleDash: true),
parsing: .unconditionalSingleValue,
help: "Pass flag through to all C++ compiler invocations"
)
var buildFlagsCxx = [String]()

@Option(
name: .customLong("Xlinker", withSingleDash: true),
parsing: .unconditionalSingleValue,
help: "Pass flag through to all linker invocations"
)
var buildFlagsLinker = [String]()

@Option(
name: .customLong("Xswiftc", withSingleDash: true),
parsing: .unconditionalSingleValue,
help: "Pass flag through to all Swift compiler invocations"
)
var buildFlagsSwift = [String]()

@Option(
name: .customLong("Xclangd", withSingleDash: true),
parsing: .unconditionalSingleValue,
help: "Pass options to clangd command-line"
)
var clangdOptions = [String]()

@Option(
name: .customLong("index-store-path", withSingleDash: true),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest accepting both single- and double-dash for this option if possible

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@natecook1000 please correct me if I'm wrong, but I don't think that accepting both is currently possible.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 It looks like this is prohibited by the command validation, but doesn't necessarily need to be. Opened an issue to allow it: apple/swift-argument-parser#231

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MaxDesiatov This fix is included in version 0.3.1! 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great, thank you!

help: "Override index-store-path from the build system"
)
var indexStorePath: AbsolutePath?

@Option(
name: .customLong("index-db-path", withSingleDash: true),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest accepting both single- and double-dash for this option if possible.

help: "Override index-database-path from the build system"
)
var indexDatabasePath: AbsolutePath?

@Option(
help: "Whether to enable server-side filtering in code-completion"
)
var completionServerSideFiltering = true

@Option(
help: "When server-side filtering is enabled, the maximum number of results to return"
)
var completionMaxResults = 200

func mapOptions() -> SourceKitServer.Options {
var serverOptions = SourceKitServer.Options()

serverOptions.buildSetup.configuration = buildConfiguration
serverOptions.buildSetup.path = buildPath
serverOptions.buildSetup.flags.cCompilerFlags = buildFlagsCc
serverOptions.buildSetup.flags.cxxCompilerFlags = buildFlagsCxx
serverOptions.buildSetup.flags.linkerFlags = buildFlagsLinker
serverOptions.buildSetup.flags.swiftCompilerFlags = buildFlagsSwift
serverOptions.clangdOptions = clangdOptions
serverOptions.indexOptions.indexStorePath = indexStorePath
serverOptions.indexOptions.indexDatabasePath = indexDatabasePath
serverOptions.completionOptions.serverSideFiltering = completionServerSideFiltering
serverOptions.completionOptions.maxResults = completionMaxResults

return serverOptions
}

if let serverSideFiltering = parsedArguments.get(completionServerSideFiltering) {
result.serverOptions.completionOptions.serverSideFiltering = serverSideFiltering
}
if let maxResults = parsedArguments.get(completionMaxResults) {
result.serverOptions.completionOptions.maxResults = maxResults
func run() throws {
if let logLevel = logLevel {
Logger.shared.currentLevel = logLevel
} else {
Logger.shared.setLogLevel(environmentVariable: "SOURCEKIT_LOGGING")
}

// Dup stdout and redirect the fd to stderr so that a careless print()
// will not break our connection stream.
let realStdout = dup(STDOUT_FILENO)
if realStdout == -1 {
fatalError("failed to dup stdout: \(strerror(errno)!)")
}
if dup2(STDERR_FILENO, STDOUT_FILENO) == -1 {
fatalError("failed to redirect stdout -> stderr: \(strerror(errno)!)")
}

let clientConnection = JSONRPCConnection(
protocol: MessageRegistry.lspProtocol,
inFD: STDIN_FILENO,
outFD: realStdout,
syncRequests: syncRequests
)

let installPath = AbsolutePath(Bundle.main.bundlePath)
ToolchainRegistry.shared = ToolchainRegistry(installPath: installPath, localFileSystem)

let server = SourceKitServer(client: clientConnection, options: mapOptions(), onExit: {
clientConnection.close()
})
clientConnection.start(receiveHandler: server, closeHandler: {
server.prepareForExit()
// Use _Exit to avoid running static destructors due to SR-12668.
_Exit(0)
})

Logger.shared.addLogHandler { message, _ in
clientConnection.send(LogMessageNotification(type: .log, message: message))
}

dispatchMain()
}

return result
}

let options: CommandLineOptions
do {
options = try parseArguments()
} catch {
fputs("error: \(error)\n", TSCLibc.stderr)
exit(1)
}

// Dup stdout and redirect the fd to stderr so that a careless print()
// will not break our connection stream.
let realStdout = dup(STDOUT_FILENO)
if realStdout == -1 {
fatalError("failed to dup stdout: \(strerror(errno)!)")
}
if dup2(STDERR_FILENO, STDOUT_FILENO) == -1 {
fatalError("failed to redirect stdout -> stderr: \(strerror(errno)!)")
}

let clientConnection = JSONRPCConnection(
protocol: MessageRegistry.lspProtocol,
inFD: STDIN_FILENO,
outFD: realStdout,
syncRequests: options.syncRequests)

let installPath = AbsolutePath(Bundle.main.bundlePath)
ToolchainRegistry.shared = ToolchainRegistry(installPath: installPath, localFileSystem)

let server = SourceKitServer(client: clientConnection, options: options.serverOptions, onExit: {
clientConnection.close()
})
clientConnection.start(receiveHandler: server, closeHandler: {
server.prepareForExit()
// Use _Exit to avoid running static destructors due to SR-12668.
_Exit(0)
})

Logger.shared.addLogHandler { message, _ in
clientConnection.send(LogMessageNotification(type: .log, message: message))
}

dispatchMain()
Main.main()