1
+ # TODO: make FORTRAN_EXT_REGEX be able to update from user input extensions
2
+ # TODO: enable jsonc C-style comments
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
1
7
import logging
2
8
import os
3
9
import re
4
10
import traceback
11
+ from pathlib import Path
5
12
6
13
from fortls .intrinsics import (
7
14
get_intrinsic_keywords ,
38
45
39
46
log = logging .getLogger (__name__ )
40
47
# Global regexes
41
- FORTRAN_EXT_REGEX = re .compile (r"^ \.F(77|90|95|03|08|OR|PP)?$" , re .I )
48
+ FORTRAN_EXT_REGEX = re .compile (r"\.F(77|90|95|03|08|OR|PP)?$" , re .I )
42
49
INT_STMNT_REGEX = re .compile (r"^[ ]*[a-z]*$" , re .I )
43
50
TYPE_DEF_REGEX = re .compile (r"[ ]*(TYPE|CLASS)[ ]*\([a-z0-9_ ]*$" , re .I )
44
51
SCOPE_DEF_REGEX = re .compile (r"[ ]*(MODULE|PROGRAM|SUBROUTINE|FUNCTION)[ ]+" , re .I )
@@ -66,7 +73,7 @@ def init_file(filepath, pp_defs, pp_suffixes, include_dirs):
66
73
67
74
68
75
def get_line_prefix (pre_lines , curr_line , iChar ):
69
- """Get code line prefix from current line and preceeding continuation lines"""
76
+ """Get code line prefix from current line and preceding continuation lines"""
70
77
if (curr_line is None ) or (iChar > len (curr_line )) or (curr_line .startswith ("#" )):
71
78
return None
72
79
prepend_string = "" .join (pre_lines )
@@ -87,6 +94,56 @@ def get_line_prefix(pre_lines, curr_line, iChar):
87
94
return line_prefix
88
95
89
96
97
+ def resolve_globs (glob_path : str , root_path : str = None ) -> list [str ]:
98
+ """Resolve glob patterns
99
+
100
+ Parameters
101
+ ----------
102
+ glob_path : str
103
+ Path containing the glob pattern follows
104
+ `fnmatch` glob pattern, can include relative paths, etc.
105
+ see fnmatch: https://docs.python.org/3/library/fnmatch.html#module-fnmatch
106
+
107
+ root_path : str, optional
108
+ root path to start glob search. If left empty the root_path will be
109
+ extracted from the glob_path, by default None
110
+
111
+ Returns
112
+ -------
113
+ list[str]
114
+ Expanded glob patterns with absolute paths.
115
+ Absolute paths are used to resolve any potential ambiguity
116
+ """
117
+ # Path.glob returns a generator, we then cast the Path obj to a str
118
+ # alternatively use p.as_posix()
119
+ if root_path :
120
+ return [str (p ) for p in Path (root_path ).resolve ().glob (glob_path )]
121
+ # Attempt to extract the root and glob pattern from the glob_path
122
+ # This is substantially less robust that then above
123
+ else :
124
+ p = Path (glob_path ).expanduser ()
125
+ parts = p .parts [p .is_absolute () :]
126
+ return [str (i ) for i in Path (p .root ).resolve ().glob (str (Path (* parts )))]
127
+
128
+
129
+ def only_dirs (paths : list [str ], err_msg : list = []) -> list [str ]:
130
+ dirs : list [str ] = []
131
+ for p in paths :
132
+ if os .path .isdir (p ):
133
+ dirs .append (p )
134
+ elif os .path .isfile (p ):
135
+ continue
136
+ else :
137
+ msg : str = (
138
+ f"Directory '{ p } ' specified in '.fortls' settings file does not exist"
139
+ )
140
+ if err_msg :
141
+ err_msg .append ([2 , msg ])
142
+ else :
143
+ print (f"WARNING: { msg } " )
144
+ return dirs
145
+
146
+
90
147
class LangServer :
91
148
def __init__ (self , conn , debug_log = False , settings = {}):
92
149
self .conn = conn
@@ -97,8 +154,8 @@ def __init__(self, conn, debug_log=False, settings={}):
97
154
self .workspace = {}
98
155
self .obj_tree = {}
99
156
self .link_version = 0
100
- self .source_dirs = []
101
- self .excl_paths = []
157
+ self .source_dirs = set ()
158
+ self .excl_paths = set ()
102
159
self .excl_suffixes = []
103
160
self .post_messages = []
104
161
self .pp_suffixes = None
@@ -220,50 +277,46 @@ def serve_initialize(self, request):
220
277
self .root_path = path_from_uri (
221
278
params .get ("rootUri" ) or params .get ("rootPath" ) or ""
222
279
)
223
- self .source_dirs .append (self .root_path )
280
+ self .source_dirs .add (self .root_path )
224
281
# Check for config file
225
282
config_path = os .path .join (self .root_path , ".fortls" )
226
283
config_exists = os .path .isfile (config_path )
227
284
if config_exists :
228
285
try :
229
- import json
286
+ with open (config_path , "r" ) as jsonfile :
287
+ # Allow for jsonc C-style commnets
288
+ # jsondata = "".join(
289
+ # line for line in jsonfile if not line.startswith("//")
290
+ # )
291
+ # config_dict = json.loads(jsondata)
292
+ config_dict = json .load (jsonfile )
230
293
231
- with open (config_path , "r" ) as fhandle :
232
- config_dict = json .load (fhandle )
233
- for excl_path in config_dict .get ("excl_paths" , []):
234
- self .excl_paths .append (os .path .join (self .root_path , excl_path ))
235
- source_dirs = config_dict .get ("source_dirs" , [])
236
- ext_source_dirs = config_dict .get ("ext_source_dirs" , [])
237
- # Legacy definition
238
- if len (source_dirs ) == 0 :
239
- source_dirs = config_dict .get ("mod_dirs" , [])
240
- for source_dir in source_dirs :
241
- dir_path = os .path .join (self .root_path , source_dir )
242
- if os .path .isdir (dir_path ):
243
- self .source_dirs .append (dir_path )
244
- else :
245
- self .post_messages .append (
246
- [
247
- 2 ,
248
- r'Source directory "{0}" specified in '
249
- r'".fortls" settings file does not exist' .format (
250
- dir_path
251
- ),
252
- ]
253
- )
254
- for ext_source_dir in ext_source_dirs :
255
- if os .path .isdir (ext_source_dir ):
256
- self .source_dirs .append (ext_source_dir )
257
- else :
258
- self .post_messages .append (
259
- [
260
- 2 ,
261
- r'External source directory "{0}" specified in '
262
- r'".fortls" settings file does not exist' .format (
263
- ext_source_dir
264
- ),
265
- ]
294
+ # Exclude paths (directories & files)
295
+ # with glob resolution
296
+ for path in config_dict .get ("excl_paths" , []):
297
+ self .excl_paths .update (set (resolve_globs (path , self .root_path )))
298
+
299
+ # Source directory paths (directories)
300
+ # with glob resolution
301
+ # XXX: Drop support for ext_source_dirs since they end up in
302
+ # source_dirs anyway
303
+ source_dirs = config_dict .get ("source_dirs" , []) + config_dict .get (
304
+ "ext_source_dirs" , []
305
+ )
306
+ for path in source_dirs :
307
+ self .source_dirs .update (
308
+ set (
309
+ only_dirs (
310
+ resolve_globs (path , self .root_path ),
311
+ self .post_messages ,
312
+ )
266
313
)
314
+ )
315
+ # Keep all directories present in source_dirs but not excl_paths
316
+ self .source_dirs = {
317
+ i for i in self .source_dirs if i not in self .excl_paths
318
+ }
319
+
267
320
self .excl_suffixes = config_dict .get ("excl_suffixes" , [])
268
321
self .lowercase_intrinsics = config_dict .get (
269
322
"lowercase_intrinsics" , self .lowercase_intrinsics
@@ -274,7 +327,12 @@ def serve_initialize(self, request):
274
327
)
275
328
self .pp_suffixes = config_dict .get ("pp_suffixes" , None )
276
329
self .pp_defs = config_dict .get ("pp_defs" , {})
277
- self .include_dirs = config_dict .get ("include_dirs" , [])
330
+ for path in config_dict .get ("include_dirs" , []):
331
+ self .include_dirs .extend (
332
+ only_dirs (
333
+ resolve_globs (path , self .root_path ), self .post_messages
334
+ )
335
+ )
278
336
self .max_line_length = config_dict .get (
279
337
"max_line_length" , self .max_line_length
280
338
)
@@ -285,14 +343,9 @@ def serve_initialize(self, request):
285
343
self .pp_defs = {key : "" for key in self .pp_defs }
286
344
except :
287
345
self .post_messages .append (
288
- [1 , ' Error while parsing " .fortls" settings file' ]
346
+ [1 , " Error while parsing ' .fortls' settings file" ]
289
347
)
290
- # Make relative include paths absolute
291
- for (i , include_dir ) in enumerate (self .include_dirs ):
292
- if not os .path .isabs (include_dir ):
293
- self .include_dirs [i ] = os .path .abspath (
294
- os .path .join (self .root_path , include_dir )
295
- )
348
+
296
349
# Setup logging
297
350
if self .debug_log and (self .root_path != "" ):
298
351
logging .basicConfig (
@@ -316,22 +369,15 @@ def serve_initialize(self, request):
316
369
self .obj_tree [module .FQSN ] = [module , None ]
317
370
# Set object settings
318
371
set_keyword_ordering (self .sort_keywords )
319
- # Recursively add sub-directories
372
+ # Recursively add sub-directories that only match Fortran extensions
320
373
if len (self .source_dirs ) == 1 :
321
- self .source_dirs = []
322
- for dirName , subdirList , fileList in os .walk (self .root_path ):
323
- if self .excl_paths .count (dirName ) > 0 :
324
- while len (subdirList ) > 0 :
325
- del subdirList [0 ]
374
+ self .source_dirs = set ()
375
+ for root , dirs , files in os .walk (self .root_path ):
376
+ # Match not found
377
+ if not list (filter (FORTRAN_EXT_REGEX .search , files )):
326
378
continue
327
- contains_source = False
328
- for filename in fileList :
329
- _ , ext = os .path .splitext (os .path .basename (filename ))
330
- if FORTRAN_EXT_REGEX .match (ext ):
331
- contains_source = True
332
- break
333
- if contains_source :
334
- self .source_dirs .append (dirName )
379
+ if root not in self .source_dirs and root not in self .excl_paths :
380
+ self .source_dirs .add (str (Path (root ).resolve ()))
335
381
# Initialize workspace
336
382
self .workspace_init ()
337
383
#
@@ -799,6 +845,7 @@ def get_definition(self, def_file, def_line, def_char):
799
845
pre_lines , curr_line , _ = def_file .get_code_line (
800
846
def_line , forward = False , strip_comment = True
801
847
)
848
+ # Returns none for string literals, when the query is in the middle
802
849
line_prefix = get_line_prefix (pre_lines , curr_line , def_char )
803
850
if line_prefix is None :
804
851
return None
@@ -1024,7 +1071,7 @@ def serve_references(self, request):
1024
1071
def_obj = self .get_definition (file_obj , def_line , def_char )
1025
1072
if def_obj is None :
1026
1073
return None
1027
- # Determine global accesibility and type membership
1074
+ # Determine global accessibility and type membership
1028
1075
restrict_file = None
1029
1076
type_mem = False
1030
1077
if def_obj .FQSN .count (":" ) > 2 :
@@ -1309,10 +1356,8 @@ def serve_onChange(self, request):
1309
1356
path = path_from_uri (uri )
1310
1357
file_obj = self .workspace .get (path )
1311
1358
if file_obj is None :
1312
- self .post_message (
1313
- 'Change request failed for unknown file "{0}"' .format (path )
1314
- )
1315
- log .error ('Change request failed for unknown file "%s"' , path )
1359
+ self .post_message (f"Change request failed for unknown file '{ path } '" )
1360
+ log .error ("Change request failed for unknown file '%s'" , path )
1316
1361
return
1317
1362
else :
1318
1363
# Update file contents with changes
@@ -1327,11 +1372,11 @@ def serve_onChange(self, request):
1327
1372
reparse_req = reparse_req or reparse_flag
1328
1373
except :
1329
1374
self .post_message (
1330
- ' Change request failed for file "{0}" : Could not apply change'
1331
- . format ( path )
1375
+ f" Change request failed for file ' { path } ' : Could not apply"
1376
+ " change"
1332
1377
)
1333
1378
log .error (
1334
- ' Change request failed for file "%s" : Could not apply change' ,
1379
+ " Change request failed for file '%s' : Could not apply change" ,
1335
1380
path ,
1336
1381
exc_info = True ,
1337
1382
)
@@ -1340,9 +1385,7 @@ def serve_onChange(self, request):
1340
1385
if reparse_req :
1341
1386
_ , err_str = self .update_workspace_file (path , update_links = True )
1342
1387
if err_str is not None :
1343
- self .post_message (
1344
- 'Change request failed for file "{0}": {1}' .format (path , err_str )
1345
- )
1388
+ self .post_message (f"Change request failed for file '{ path } ': { err_str } " )
1346
1389
return
1347
1390
# Update include statements linking to this file
1348
1391
for _ , tmp_file in self .workspace .items ():
@@ -1378,9 +1421,7 @@ def serve_onSave(self, request, did_open=False, did_close=False):
1378
1421
filepath , read_file = True , allow_empty = did_open
1379
1422
)
1380
1423
if err_str is not None :
1381
- self .post_message (
1382
- 'Save request failed for file "{0}": {1}' .format (filepath , err_str )
1383
- )
1424
+ self .post_message (f"Save request failed for file '{ filepath } ': { err_str } " )
1384
1425
return
1385
1426
if did_change :
1386
1427
# Update include statements linking to this file
@@ -1446,20 +1487,22 @@ def update_workspace_file(
1446
1487
def workspace_init (self ):
1447
1488
# Get filenames
1448
1489
file_list = []
1449
- for source_dir in self .source_dirs :
1450
- for filename in os .listdir (source_dir ):
1451
- _ , ext = os .path .splitext (os .path .basename (filename ))
1452
- if FORTRAN_EXT_REGEX .match (ext ):
1453
- filepath = os .path .normpath (os .path .join (source_dir , filename ))
1454
- if self .excl_paths .count (filepath ) > 0 :
1455
- continue
1456
- inc_file = True
1457
- for excl_suffix in self .excl_suffixes :
1458
- if filepath .endswith (excl_suffix ):
1459
- inc_file = False
1460
- break
1461
- if inc_file :
1462
- file_list .append (filepath )
1490
+ for src_dir in self .source_dirs :
1491
+ for f in os .listdir (src_dir ):
1492
+ p = os .path .join (src_dir , f )
1493
+ # Process only files
1494
+ if not os .path .isfile (p ):
1495
+ continue
1496
+ # File extension must match supported extensions
1497
+ if not FORTRAN_EXT_REGEX .search (f ):
1498
+ continue
1499
+ # File cannot be in excluded paths/files
1500
+ if p in self .excl_paths :
1501
+ continue
1502
+ # File cannot have an excluded extension
1503
+ if any (f .endswith (ext ) for ext in set (self .excl_suffixes )):
1504
+ continue
1505
+ file_list .append (p )
1463
1506
# Process files
1464
1507
from multiprocessing import Pool
1465
1508
@@ -1476,12 +1519,7 @@ def workspace_init(self):
1476
1519
result_obj = result .get ()
1477
1520
if result_obj [0 ] is None :
1478
1521
self .post_messages .append (
1479
- [
1480
- 1 ,
1481
- 'Initialization failed for file "{0}": {1}' .format (
1482
- path , result_obj [1 ]
1483
- ),
1484
- ]
1522
+ [1 , f"Initialization failed for file '{ path } ': { result_obj [1 ]} " ]
1485
1523
)
1486
1524
continue
1487
1525
self .workspace [path ] = result_obj [0 ]
0 commit comments