Skip to content

Commit a6d6897

Browse files
committed
feat: swift newfile change for server
1 parent 37f7dff commit a6d6897

File tree

5 files changed

+136
-55
lines changed

5 files changed

+136
-55
lines changed

compile_database.py

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ def findSwiftModuleRoot(filename):
165165

166166
return (directory, flagFile, compileFile)
167167

168+
def filekey(filename):
169+
return os.path.realpath(filename).lower()
168170

169171
class CompileFileInfo:
170172
def __init__(self, compileFile, store):
@@ -182,26 +184,24 @@ def __init__(self, compileFile, store):
182184
if not command:
183185
continue
184186
if files := i.get("files"): # batch files, eg: swift module
185-
self.file_info.update((self.key(f), command) for f in files)
187+
self.file_info.update((filekey(f), command) for f in files)
186188
if fileLists := i.get(
187189
"fileLists"
188190
): # file list store in a dedicated file
189191
self.file_info.update(
190-
(self.key(f), command)
192+
(filekey(f), command)
191193
for l in fileLists
192194
if os.path.isfile(l)
193195
for f in getFileArgs(l, store.setdefault("filelist", {}))
194196
)
195197
if file := i.get("file"): # single file info
196-
self.file_info[self.key(file)] = command
198+
self.file_info[filekey(file)] = command
197199

198200
def get(self, filename):
199201
if command := self.file_info.get(filename.lower()):
202+
# xcode 12 escape =, but not recognized...
200203
return command.replace("\\=", "=")
201204

202-
def key(self, filename):
203-
return os.path.realpath(filename).lower()
204-
205205
def groupby_dir(self) -> dict[str, set[str]]:
206206
if self.dir_info is None: # lazy index dir and cmd
207207
self.dir_info = defaultdict(set)
@@ -213,6 +213,7 @@ def groupby_dir(self) -> dict[str, set[str]]:
213213
return self.dir_info
214214

215215
# hack new file into current compile file
216+
# return: set of filekey for match for file. or None if new_file can't be infered
216217
def new_file(self, filename):
217218
# Currently only processing swift files
218219
if not filename.endswith(".swift"):
@@ -221,7 +222,7 @@ def new_file(self, filename):
221222
filename = os.path.realpath(filename)
222223
filename_key = filename.lower()
223224
if filename_key in self.file_info:
224-
return # already handled
225+
return {filename_key} # already handled
225226

226227
dir = os.path.dirname(filename_key)
227228
samefile = next(
@@ -234,7 +235,7 @@ def new_file(self, filename):
234235
cmd_match = next(cmd_split_pattern.finditer(command), None)
235236
if not cmd_match:
236237
return
237-
assert self.cmd_info
238+
assert self.cmd_info # init in groupby_dir
238239
module_files = self.cmd_info.pop(command)
239240
index = cmd_match.end()
240241
from shlex import quote
@@ -247,12 +248,22 @@ def new_file(self, filename):
247248
self.cmd_info[command] = module_files
248249
for v in module_files:
249250
self.file_info[v] = command
251+
return module_files
250252

253+
def newfileForCompileFile(filename, compileFile, store) -> set[str] | None:
254+
info = compileFileInfoFromStore(compileFile, store)
255+
return info.new_file(filename)
251256

252257
def commandForFile(filename, compileFile, store: Dict):
253258
"""
254259
command = store["compile"][<compileFile>][filename]
260+
> will lazy build compile_file info
255261
"""
262+
info = compileFileInfoFromStore(compileFile, store)
263+
return info.get(filename)
264+
265+
266+
def compileFileInfoFromStore(compileFile, store: Dict):
256267
compile_store = store.setdefault("compile", {})
257268
info: CompileFileInfo = compile_store.get(compileFile)
258269
if info is None: # load {filename.lower: command} dict
@@ -263,34 +274,29 @@ def commandForFile(filename, compileFile, store: Dict):
263274
# if has additional new_file, generate command for it
264275
for file in store.get("additional_files") or ():
265276
info.new_file(file)
266-
267-
# xcode 12 escape =, but not recognized...
268-
return info.get(filename)
277+
return info
269278

270279

271280
def GetFlagsInCompile(filename, compileFile, store):
272-
"""read flags from compileFile"""
281+
"""read flags from compileFile. filename should be realpath"""
273282
if compileFile:
274283
command = commandForFile(filename, compileFile, store)
275284
if command:
276285
flags = cmd_split(command)[1:] # ignore executable
277286
return list(filterFlags(flags, store.setdefault("filelist", {})))
278287

279288

280-
def GetFlags(filename: str, compileFile=None, **kwargs):
289+
def GetFlags(filename: str, compileFile=None, store=None):
281290
"""sourcekit entry function"""
282291
# NOTE: use store to ensure toplevel storage. child store should be other name
283292
# see store.setdefault to get all child attributes
284-
store = kwargs.get("store", globalStore)
285-
filename = os.path.realpath(filename)
293+
if not store:
294+
store = globalStore
286295

287296
if compileFile:
288297
if final_flags := GetFlagsInCompile(filename, compileFile, store):
289-
return {"flags": final_flags, "do_cache": True}
290-
291-
if filename.endswith(".swift"):
292-
return InferFlagsForSwift(filename, compileFile, store)
293-
return {"flags": [], "do_cache": False}
298+
return final_flags
299+
return None
294300

295301

296302
# TODO: c family infer flags #
@@ -330,4 +336,4 @@ def InferFlagsForSwift(filename, compileFile, store):
330336
"/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/",
331337
]
332338

333-
return {"flags": final_flags, "do_cache": True}
339+
return final_flags

config/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from .config import ServerConfig
2+
from .env import env

config/env.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# use to get environ config
2+
from functools import cache
3+
import os
4+
5+
class Env:
6+
def on(self, value: str):
7+
try:
8+
c = value[0]
9+
if c.isdigit(): return bool(int(c))
10+
return c in ["t", "T", "y", "Y"]
11+
except Exception:
12+
return False
13+
14+
@property
15+
@cache
16+
def new_file(self):
17+
if newfile := os.environ.get("XBS_FEAT_NEWFILE"):
18+
return self.on(newfile)
19+
return False
20+
21+
env = Env()
22+

server.py

Lines changed: 85 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,18 @@
88
from typing import Optional
99
import urllib.parse
1010

11-
from compile_database import GetFlags
12-
from config import ServerConfig
11+
from compile_database import (
12+
GetFlags,
13+
InferFlagsForSwift,
14+
filekey,
15+
newfileForCompileFile,
16+
)
17+
from config import ServerConfig, env
1318
from misc import force_remove, get_mtime
1419

1520
logger = logging.getLogger(__name__)
1621

22+
1723
def send(data):
1824
data_str = json.dumps(data)
1925
logger.debug("Res <-- %s", data_str)
@@ -32,6 +38,11 @@ def uri2filepath(uri):
3238
return urllib.parse.unquote(result.path)
3339

3440

41+
def uri2realpath(uri):
42+
path = uri2filepath(uri)
43+
return os.path.realpath(path)
44+
45+
3546
def uptodate(target: str, srcs: list[str]):
3647
target_mtime = get_mtime(target)
3748
srcs_mtime = (get_mtime(src) for src in srcs)
@@ -53,12 +64,14 @@ def __init__(self, root_path: str, cache_path):
5364
self.observed_uri = set()
5465
# background thread to observe changes
5566
self.observed_thread: Optional[Thread] = None
67+
5668
# {path: mtime} cache. use to find changes
5769
self.observed_info = {self.config.path: get_mtime(self.config.path)}
5870

5971
self.reinit_compile_info()
60-
# NOTE:thread-safety: for state shared by main and background thread,
61-
# can only changed in sync_compile_file
72+
# NOTE:thread-safety: for state shared by main and background watch thread,
73+
# can only changed in sync_compile_file, which block all thread and no one access it.
74+
# other time, the shared state is readonly and safe..
6275

6376
def get_compile_file(self, config):
6477
# isolate xcode generate compile file and manual compile_file
@@ -75,7 +88,7 @@ def reinit_compile_info(self):
7588
"""all the compile information may change in background"""
7689

7790
# store use to save compile_datainfo. it will be reload when config changes.
78-
self.store = {} # main-thread
91+
self.store = {} # main-thread
7992
self._compile_file = self.get_compile_file(self.config)
8093
if os.path.exists(self._compile_file):
8194
self.compile_file = self._compile_file
@@ -101,11 +114,40 @@ def compile_lock_path(self):
101114

102115
return output_lock_path(self._compile_file)
103116

117+
def register_uri(self, uri):
118+
self.observed_uri.add(uri)
119+
120+
file_path = uri2realpath(uri)
121+
flags = GetFlags(file_path, self.compile_file, store=self.store)
122+
123+
if not flags and env.new_file:
124+
if filekeys := newfileForCompileFile(
125+
file_path, self.compile_file, store=self.store
126+
):
127+
# add new file success, update options for module files
128+
for v in self.observed_uri:
129+
if filekey(uri2filepath(v)) in filekeys:
130+
self.notify_option_changed(v)
131+
return
132+
133+
if not flags and file_path.endswith(".swift"):
134+
flags = InferFlagsForSwift(file_path, self.compile_file, store=self.store)
135+
136+
self._notify_option_changed(uri, self.optionsForFlags(flags))
137+
138+
def unregister_uri(self, uri):
139+
self.observed_uri.remove(uri)
140+
104141
def optionsForFile(self, uri):
105-
file_path = uri2filepath(uri)
106-
flags = GetFlags(file_path, self.compile_file, store=self.store)[
107-
"flags"
108-
] # type: list
142+
file_path = uri2realpath(uri)
143+
flags = GetFlags(file_path, self.compile_file, store=self.store)
144+
if not flags and file_path.endswith(".swift"):
145+
flags = InferFlagsForSwift(file_path, self.compile_file, store=self.store)
146+
return self.optionsForFlags(flags)
147+
148+
def optionsForFlags(self, flags):
149+
if flags is None:
150+
return None
109151
try:
110152
workdir = flags[flags.index("-working-directory") + 1]
111153
except (IndexError, ValueError):
@@ -116,19 +158,22 @@ def optionsForFile(self, uri):
116158
}
117159

118160
def notify_option_changed(self, uri):
119-
try:
120-
notification = {
121-
"jsonrpc": "2.0",
122-
"method": "build/sourceKitOptionsChanged",
123-
"params": {
124-
"uri": uri,
125-
"updatedOptions": self.optionsForFile(uri),
126-
},
127-
}
128-
send(notification)
129-
return True
130-
except ValueError as e: # may have other type change register, like target
131-
logger.debug(e)
161+
# no clear options?
162+
self._notify_option_changed(uri, self.optionsForFile(uri))
163+
164+
def _notify_option_changed(self, uri, options):
165+
# empty options is nouse and lsp will stop working.., at least there should has a infer flags..
166+
if options is None:
167+
return
168+
notification = {
169+
"jsonrpc": "2.0",
170+
"method": "build/sourceKitOptionsChanged",
171+
"params": {
172+
"uri": uri,
173+
"updatedOptions": options,
174+
},
175+
}
176+
send(notification)
132177

133178
def shutdown(self):
134179
self.observed_thread = None # release to end in subthread
@@ -241,6 +286,7 @@ def update_check_time(path):
241286
def trigger_parse(self, xcpath):
242287
# FIXME: ensure index_store_path from buildServer.json consistent with parsed .compile file..
243288
import xclog_parser
289+
244290
xclog_parser.hooks_echo_to_log = True
245291

246292
from xclog_parser import parse, OutputLockedError
@@ -397,10 +443,9 @@ def textDocument_registerForChanges(message):
397443
action = message["params"]["action"]
398444
uri = message["params"]["uri"]
399445
if action == "register":
400-
if shared_state.notify_option_changed(uri):
401-
shared_state.observed_uri.add(uri)
446+
shared_state.register_uri(uri)
402447
elif action == "unregister":
403-
shared_state.observed_uri.remove(uri)
448+
shared_state.unregister_uri(uri)
404449

405450
def textDocument_sourceKitOptions(message):
406451
return {
@@ -438,14 +483,9 @@ def serve():
438483
message = json.loads(raw)
439484
logger.debug("Req --> " + raw)
440485

441-
with lock:
442-
response = None
443-
handler = dispatch.get(message["method"].replace("/", "_"))
444-
if handler:
445-
response = handler(message)
446-
# ignore other notifications
447-
elif "id" in message:
448-
response = {
486+
def default_response():
487+
if "id" in message:
488+
return {
449489
"jsonrpc": "2.0",
450490
"id": message["id"],
451491
"error": {
@@ -454,5 +494,17 @@ def serve():
454494
},
455495
}
456496

497+
with lock:
498+
response = None
499+
handler = dispatch.get(message["method"].replace("/", "_"))
500+
if handler:
501+
try:
502+
response = handler(message)
503+
except Exception as e:
504+
logger.exception(f"handle message error: {e}")
505+
response = default_response()
506+
else:
507+
# ignore other notifications
508+
response = default_response()
457509
if response:
458510
send(response)

xcode-build-server

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def show_help():
1616
1717
SPECIAL ENVIRONMENT VARIABLE:
1818
SOURCEKIT_LOGGING=3: enable detail debug log
19-
XBS_LOGPATH: set log path. default is :stderr. use :null to disable log
19+
XBS_LOGPATH: set log path. default is `:stderr`. use `:null` to disable log
2020
"""
2121
)
2222
exit(0)

0 commit comments

Comments
 (0)