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
17import logging
28import os
39import re
410import traceback
11+ from pathlib import Path
512
613from fortls .intrinsics import (
714 get_intrinsic_keywords ,
3845
3946log = logging .getLogger (__name__ )
4047# 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 )
4249INT_STMNT_REGEX = re .compile (r"^[ ]*[a-z]*$" , re .I )
4350TYPE_DEF_REGEX = re .compile (r"[ ]*(TYPE|CLASS)[ ]*\([a-z0-9_ ]*$" , re .I )
4451SCOPE_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):
6673
6774
6875def 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"""
7077 if (curr_line is None ) or (iChar > len (curr_line )) or (curr_line .startswith ("#" )):
7178 return None
7279 prepend_string = "" .join (pre_lines )
@@ -87,6 +94,56 @@ def get_line_prefix(pre_lines, curr_line, iChar):
8794 return line_prefix
8895
8996
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+
90147class LangServer :
91148 def __init__ (self , conn , debug_log = False , settings = {}):
92149 self .conn = conn
@@ -97,8 +154,8 @@ def __init__(self, conn, debug_log=False, settings={}):
97154 self .workspace = {}
98155 self .obj_tree = {}
99156 self .link_version = 0
100- self .source_dirs = []
101- self .excl_paths = []
157+ self .source_dirs = set ()
158+ self .excl_paths = set ()
102159 self .excl_suffixes = []
103160 self .post_messages = []
104161 self .pp_suffixes = None
@@ -220,50 +277,46 @@ def serve_initialize(self, request):
220277 self .root_path = path_from_uri (
221278 params .get ("rootUri" ) or params .get ("rootPath" ) or ""
222279 )
223- self .source_dirs .append (self .root_path )
280+ self .source_dirs .add (self .root_path )
224281 # Check for config file
225282 config_path = os .path .join (self .root_path , ".fortls" )
226283 config_exists = os .path .isfile (config_path )
227284 if config_exists :
228285 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 )
230293
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+ )
266313 )
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+
267320 self .excl_suffixes = config_dict .get ("excl_suffixes" , [])
268321 self .lowercase_intrinsics = config_dict .get (
269322 "lowercase_intrinsics" , self .lowercase_intrinsics
@@ -274,7 +327,12 @@ def serve_initialize(self, request):
274327 )
275328 self .pp_suffixes = config_dict .get ("pp_suffixes" , None )
276329 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+ )
278336 self .max_line_length = config_dict .get (
279337 "max_line_length" , self .max_line_length
280338 )
@@ -285,14 +343,9 @@ def serve_initialize(self, request):
285343 self .pp_defs = {key : "" for key in self .pp_defs }
286344 except :
287345 self .post_messages .append (
288- [1 , ' Error while parsing " .fortls" settings file' ]
346+ [1 , " Error while parsing ' .fortls' settings file" ]
289347 )
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+
296349 # Setup logging
297350 if self .debug_log and (self .root_path != "" ):
298351 logging .basicConfig (
@@ -316,22 +369,15 @@ def serve_initialize(self, request):
316369 self .obj_tree [module .FQSN ] = [module , None ]
317370 # Set object settings
318371 set_keyword_ordering (self .sort_keywords )
319- # Recursively add sub-directories
372+ # Recursively add sub-directories that only match Fortran extensions
320373 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 )):
326378 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 ()))
335381 # Initialize workspace
336382 self .workspace_init ()
337383 #
@@ -799,6 +845,7 @@ def get_definition(self, def_file, def_line, def_char):
799845 pre_lines , curr_line , _ = def_file .get_code_line (
800846 def_line , forward = False , strip_comment = True
801847 )
848+ # Returns none for string literals, when the query is in the middle
802849 line_prefix = get_line_prefix (pre_lines , curr_line , def_char )
803850 if line_prefix is None :
804851 return None
@@ -1024,7 +1071,7 @@ def serve_references(self, request):
10241071 def_obj = self .get_definition (file_obj , def_line , def_char )
10251072 if def_obj is None :
10261073 return None
1027- # Determine global accesibility and type membership
1074+ # Determine global accessibility and type membership
10281075 restrict_file = None
10291076 type_mem = False
10301077 if def_obj .FQSN .count (":" ) > 2 :
@@ -1309,10 +1356,8 @@ def serve_onChange(self, request):
13091356 path = path_from_uri (uri )
13101357 file_obj = self .workspace .get (path )
13111358 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 )
13161361 return
13171362 else :
13181363 # Update file contents with changes
@@ -1327,11 +1372,11 @@ def serve_onChange(self, request):
13271372 reparse_req = reparse_req or reparse_flag
13281373 except :
13291374 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"
13321377 )
13331378 log .error (
1334- ' Change request failed for file "%s" : Could not apply change' ,
1379+ " Change request failed for file '%s' : Could not apply change" ,
13351380 path ,
13361381 exc_info = True ,
13371382 )
@@ -1340,9 +1385,7 @@ def serve_onChange(self, request):
13401385 if reparse_req :
13411386 _ , err_str = self .update_workspace_file (path , update_links = True )
13421387 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 } " )
13461389 return
13471390 # Update include statements linking to this file
13481391 for _ , tmp_file in self .workspace .items ():
@@ -1378,9 +1421,7 @@ def serve_onSave(self, request, did_open=False, did_close=False):
13781421 filepath , read_file = True , allow_empty = did_open
13791422 )
13801423 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 } " )
13841425 return
13851426 if did_change :
13861427 # Update include statements linking to this file
@@ -1446,20 +1487,22 @@ def update_workspace_file(
14461487 def workspace_init (self ):
14471488 # Get filenames
14481489 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 )
14631506 # Process files
14641507 from multiprocessing import Pool
14651508
@@ -1476,12 +1519,7 @@ def workspace_init(self):
14761519 result_obj = result .get ()
14771520 if result_obj [0 ] is None :
14781521 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 ]} " ]
14851523 )
14861524 continue
14871525 self .workspace [path ] = result_obj [0 ]
0 commit comments