Skip to content

Commit bcfadd4

Browse files
authored
Fix code completion in header files (#426)
Previously, header files would have their compiler flags inferred from source files which include them. However, code completion didn't work properly since the file path in the arguments was incorrect, pointing to the source file instead of the header file. This also includes support for source files which include other source files, although that's not a common use case.
1 parent 408789c commit bcfadd4

File tree

3 files changed

+88
-10
lines changed

3 files changed

+88
-10
lines changed

Sources/SKCore/BuildSystemManager.swift

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,17 +156,36 @@ extension BuildSystemManager: BuildSystem {
156156
self.watchedFiles[uri] = (mainFile, language)
157157
}
158158

159-
let newStatus = self.cachedStatusOrRegisterForSettings( for: mainFile, language: language)
159+
let newStatus = self.cachedStatusOrRegisterForSettings(for: mainFile, language: language)
160160

161-
if let change = newStatus.buildSettingsChange,
161+
if let mainChange = newStatus.buildSettingsChange,
162162
let delegate = self._delegate {
163+
let change = self.convert(change: mainChange, ofMainFile: mainFile, to: uri)
163164
self.notifyQueue.async {
164165
delegate.fileBuildSettingsChanged([uri: change])
165166
}
166167
}
167168
}
168169
}
169170

171+
/// Return settings for `file` based on the `change` settings for `mainFile`.
172+
///
173+
/// This is used when inferring arguments for header files (e.g. main file is a `.m` while file is a` .h`).
174+
func convert(
175+
change: FileBuildSettingsChange,
176+
ofMainFile mainFile: DocumentURI,
177+
to file: DocumentURI
178+
) -> FileBuildSettingsChange {
179+
guard mainFile != file else { return change }
180+
switch change {
181+
case .removedOrUnavailable: return .removedOrUnavailable
182+
case .fallback(let settings):
183+
return .fallback(settings.patching(newFile: file.pseudoPath, originalFile: mainFile.pseudoPath))
184+
case .modified(let settings):
185+
return .modified(settings.patching(newFile: file.pseudoPath, originalFile: mainFile.pseudoPath))
186+
}
187+
}
188+
170189
/// *Must be called on queue*. Handle a request for `FileBuildSettings` on
171190
/// `mainFile`. Updates and returns the new `MainFileStatus` for `mainFile`.
172191
func cachedStatusOrRegisterForSettings(
@@ -233,7 +252,9 @@ extension BuildSystemManager: BuildSystem {
233252
}
234253
if let change = status.buildSettingsChange {
235254
for watch in watches {
236-
changedWatchedFiles[watch.key] = change
255+
let newChange =
256+
self.convert(change: change, ofMainFile: mainFile, to: watch.key)
257+
changedWatchedFiles[watch.key] = newChange
237258
}
238259
}
239260
}
@@ -413,7 +434,10 @@ extension BuildSystemManager: MainFilesDelegate {
413434

414435
let newStatus = self.cachedStatusOrRegisterForSettings(
415436
for: newMainFile, language: language)
416-
buildSettingsChanges[uri] = newStatus.buildSettingsChange
437+
if let change = newStatus.buildSettingsChange {
438+
let newChange = self.convert(change: change, ofMainFile: newMainFile, to: uri)
439+
buildSettingsChanges[uri] = newChange
440+
}
417441
}
418442
}
419443

Sources/SKCore/FileBuildSettings.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import Foundation
1314
import LanguageServerProtocol
1415

1516
/// Build settings for a single file.
@@ -29,3 +30,50 @@ public struct FileBuildSettings: Equatable {
2930
self.workingDirectory = workingDirectory
3031
}
3132
}
33+
34+
fileprivate let cExtensions = ["c"]
35+
fileprivate let cppExtensions = ["cpp", "cc"]
36+
fileprivate let objcExtensions = ["m"]
37+
fileprivate let objcppExtensions = ["mm"]
38+
39+
private extension String {
40+
var pathExtension: String {
41+
return (self as NSString).pathExtension
42+
}
43+
var pathBasename: String {
44+
return (self as NSString).lastPathComponent
45+
}
46+
}
47+
48+
public extension FileBuildSettings {
49+
/// Return arguments suitable for use by `newFile`.
50+
///
51+
/// This patches the arguments by searching for the argument corresponding to
52+
/// `originalFile` and replacing it.
53+
func patching(newFile: String, originalFile: String) -> FileBuildSettings {
54+
var arguments = self.compilerArguments
55+
let basename = originalFile.pathBasename
56+
let fileExtension = originalFile.pathExtension
57+
if let index = arguments.lastIndex(where: {
58+
// It's possible the arguments use relative paths while the `originalFile` given
59+
// is an absolute/real path value. We guess based on suffixes instead of hitting
60+
// the file system.
61+
$0.hasSuffix(basename) && originalFile.hasSuffix($0)
62+
}) {
63+
arguments[index] = newFile
64+
// The `-x<lang>` flag needs to be before the possible `-c <header file>`
65+
// argument in order for Clang to respect it. If there is a pre-existing `-x`
66+
// flag though, Clang will honor that one instead since it comes after.
67+
if cExtensions.contains(fileExtension) {
68+
arguments.insert("-xc", at: 0)
69+
} else if cppExtensions.contains(fileExtension) {
70+
arguments.insert("-xc++", at: 0)
71+
} else if objcExtensions.contains(fileExtension) {
72+
arguments.insert("-xobjective-c", at: 0)
73+
} else if (objcppExtensions.contains(fileExtension)) {
74+
arguments.insert("-xobjective-c++", at: 0)
75+
}
76+
}
77+
return FileBuildSettings(compilerArguments: arguments, workingDirectory: self.workingDirectory)
78+
}
79+
}

Tests/SKCoreTests/BuildSystemManagerTests.swift

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -325,13 +325,16 @@ final class BuildSystemManagerTests: XCTestCase {
325325
defer { withExtendedLifetime(bsm) {} } // Keep BSM alive for callbacks.
326326
let del = BSMDelegate(bsm)
327327

328-
bs.map[cpp] = FileBuildSettings(compilerArguments: ["C++ Main File"])
328+
let cppArg = "C++ Main File"
329+
bs.map[cpp] = FileBuildSettings(compilerArguments: [cppArg, cpp.pseudoPath])
329330

330331
let initial1 = expectation(description: "initial settings h1 via cpp")
331332
let initial2 = expectation(description: "initial settings h2 via cpp")
333+
let expectedArgsH1 = FileBuildSettings(compilerArguments: ["-xc++", cppArg, h1.pseudoPath])
334+
let expectedArgsH2 = FileBuildSettings(compilerArguments: ["-xc++", cppArg, h2.pseudoPath])
332335
del.expected = [
333-
(h1, bs.map[cpp]!, initial1, #file, #line),
334-
(h2, bs.map[cpp]!, initial2, #file, #line),
336+
(h1, expectedArgsH1, initial1, #file, #line),
337+
(h2, expectedArgsH2, initial2, #file, #line),
335338
]
336339

337340
bsm.registerForChangeNotifications(for: h1, language: .c)
@@ -341,12 +344,15 @@ final class BuildSystemManagerTests: XCTestCase {
341344
// since they are backed by the same underlying cpp file.
342345
wait(for: [initial1, initial2], timeout: 10, enforceOrder: false)
343346

344-
bs.map[cpp] = FileBuildSettings(compilerArguments: ["New C++ Main File"])
347+
let newCppArg = "New C++ Main File"
348+
bs.map[cpp] = FileBuildSettings(compilerArguments: [newCppArg, cpp.pseudoPath])
345349
let changed1 = expectation(description: "initial settings h1 via cpp")
346350
let changed2 = expectation(description: "initial settings h2 via cpp")
351+
let newArgsH1 = FileBuildSettings(compilerArguments: ["-xc++", newCppArg, h1.pseudoPath])
352+
let newArgsH2 = FileBuildSettings(compilerArguments: ["-xc++", newCppArg, h2.pseudoPath])
347353
del.expected = [
348-
(h1, bs.map[cpp]!, changed1, #file, #line),
349-
(h2, bs.map[cpp]!, changed2, #file, #line),
354+
(h1, newArgsH1, changed1, #file, #line),
355+
(h2, newArgsH2, changed2, #file, #line),
350356
]
351357
bsm.fileBuildSettingsChanged([cpp: .modified(bs.map[cpp]!)])
352358

0 commit comments

Comments
 (0)