|
| 1 | +import logging |
| 2 | +import os |
| 3 | +from pathlib import Path |
| 4 | + |
| 5 | +from fortls.constants import KEYWORD_ID_DICT, KEYWORD_LIST, sort_keywords |
1 | 6 | from fortls.regex_patterns import (
|
2 | 7 | DQ_STRING_REGEX,
|
3 | 8 | FIXED_COMMENT_LINE_MATCH,
|
|
6 | 11 | LOGICAL_REGEX,
|
7 | 12 | NAT_VAR_REGEX,
|
8 | 13 | NUMBER_REGEX,
|
| 14 | + OBJBREAK_REGEX, |
9 | 15 | SQ_STRING_REGEX,
|
10 | 16 | WORD_REGEX,
|
11 | 17 | )
|
12 | 18 |
|
| 19 | +log = logging.getLogger(__name__) |
| 20 | + |
13 | 21 |
|
14 | 22 | def expand_name(line, char_poss):
|
15 | 23 | """Get full word containing given cursor position"""
|
@@ -124,3 +132,211 @@ def find_paren_match(test_str):
|
124 | 132 | if paren_count == 0:
|
125 | 133 | return i
|
126 | 134 | return ind
|
| 135 | + |
| 136 | + |
| 137 | +def get_line_prefix(pre_lines: list, curr_line: str, col: int, qs: bool = True) -> str: |
| 138 | + """Get code line prefix from current line and preceding continuation lines |
| 139 | +
|
| 140 | + Parameters |
| 141 | + ---------- |
| 142 | + pre_lines : list |
| 143 | + for multiline cases get all the previous, relevant lines |
| 144 | + curr_line : str |
| 145 | + the current line |
| 146 | + col : int |
| 147 | + column index of the current line |
| 148 | + qs : bool, optional |
| 149 | + strip quotes i.e. string literals from `curr_line` and `pre_lines`. |
| 150 | + Need this disable when hovering over string literals, by default True |
| 151 | +
|
| 152 | + Returns |
| 153 | + ------- |
| 154 | + str |
| 155 | + part of the line including any relevant line continuations before `col` |
| 156 | + """ |
| 157 | + if (curr_line is None) or (col > len(curr_line)) or (curr_line.startswith("#")): |
| 158 | + return None |
| 159 | + prepend_string = "".join(pre_lines) |
| 160 | + curr_line = prepend_string + curr_line |
| 161 | + col += len(prepend_string) |
| 162 | + line_prefix = curr_line[:col].lower() |
| 163 | + # Ignore string literals |
| 164 | + if qs: |
| 165 | + if (line_prefix.find("'") > -1) or (line_prefix.find('"') > -1): |
| 166 | + sq_count = 0 |
| 167 | + dq_count = 0 |
| 168 | + for char in line_prefix: |
| 169 | + if (char == "'") and (dq_count % 2 == 0): |
| 170 | + sq_count += 1 |
| 171 | + elif (char == '"') and (sq_count % 2 == 0): |
| 172 | + dq_count += 1 |
| 173 | + if (dq_count % 2 == 1) or (sq_count % 2 == 1): |
| 174 | + return None |
| 175 | + return line_prefix |
| 176 | + |
| 177 | + |
| 178 | +def resolve_globs(glob_path: str, root_path: str = None) -> list[str]: |
| 179 | + """Resolve glob patterns |
| 180 | +
|
| 181 | + Parameters |
| 182 | + ---------- |
| 183 | + glob_path : str |
| 184 | + Path containing the glob pattern follows |
| 185 | + `fnmatch` glob pattern, can include relative paths, etc. |
| 186 | + see fnmatch: https://docs.python.org/3/library/fnmatch.html#module-fnmatch |
| 187 | +
|
| 188 | + root_path : str, optional |
| 189 | + root path to start glob search. If left empty the root_path will be |
| 190 | + extracted from the glob_path, by default None |
| 191 | +
|
| 192 | + Returns |
| 193 | + ------- |
| 194 | + list[str] |
| 195 | + Expanded glob patterns with absolute paths. |
| 196 | + Absolute paths are used to resolve any potential ambiguity |
| 197 | + """ |
| 198 | + # Path.glob returns a generator, we then cast the Path obj to a str |
| 199 | + # alternatively use p.as_posix() |
| 200 | + if root_path: |
| 201 | + return [str(p) for p in Path(root_path).resolve().glob(glob_path)] |
| 202 | + # Attempt to extract the root and glob pattern from the glob_path |
| 203 | + # This is substantially less robust that then above |
| 204 | + else: |
| 205 | + p = Path(glob_path).expanduser() |
| 206 | + parts = p.parts[p.is_absolute() :] |
| 207 | + return [str(i) for i in Path(p.root).resolve().glob(str(Path(*parts)))] |
| 208 | + |
| 209 | + |
| 210 | +def only_dirs(paths: list[str], err_msg: list = []) -> list[str]: |
| 211 | + dirs: list[str] = [] |
| 212 | + for p in paths: |
| 213 | + if os.path.isdir(p): |
| 214 | + dirs.append(p) |
| 215 | + elif os.path.isfile(p): |
| 216 | + continue |
| 217 | + else: |
| 218 | + msg: str = ( |
| 219 | + f"Directory '{p}' specified in Configuration settings file does not" |
| 220 | + " exist" |
| 221 | + ) |
| 222 | + if err_msg: |
| 223 | + err_msg.append([2, msg]) |
| 224 | + else: |
| 225 | + log.warning(msg) |
| 226 | + return dirs |
| 227 | + |
| 228 | + |
| 229 | +def set_keyword_ordering(sorted): |
| 230 | + global sort_keywords |
| 231 | + sort_keywords = sorted |
| 232 | + |
| 233 | + |
| 234 | +def map_keywords(keywords): |
| 235 | + mapped_keywords = [] |
| 236 | + keyword_info = {} |
| 237 | + for keyword in keywords: |
| 238 | + keyword_prefix = keyword.split("(")[0].lower() |
| 239 | + keyword_ind = KEYWORD_ID_DICT.get(keyword_prefix) |
| 240 | + if keyword_ind is not None: |
| 241 | + mapped_keywords.append(keyword_ind) |
| 242 | + if keyword_prefix in ("intent", "dimension", "pass"): |
| 243 | + keyword_substring = get_paren_substring(keyword) |
| 244 | + if keyword_substring is not None: |
| 245 | + keyword_info[keyword_prefix] = keyword_substring |
| 246 | + if sort_keywords: |
| 247 | + mapped_keywords.sort() |
| 248 | + return mapped_keywords, keyword_info |
| 249 | + |
| 250 | + |
| 251 | +def get_keywords(keywords, keyword_info={}): |
| 252 | + keyword_strings = [] |
| 253 | + for keyword_id in keywords: |
| 254 | + string_rep = KEYWORD_LIST[keyword_id] |
| 255 | + addl_info = keyword_info.get(string_rep) |
| 256 | + string_rep = string_rep.upper() |
| 257 | + if addl_info is not None: |
| 258 | + string_rep += "({0})".format(addl_info) |
| 259 | + keyword_strings.append(string_rep) |
| 260 | + return keyword_strings |
| 261 | + |
| 262 | + |
| 263 | +def get_paren_substring(test_str): |
| 264 | + i1 = test_str.find("(") |
| 265 | + i2 = test_str.rfind(")") |
| 266 | + if i1 > -1 and i2 > i1: |
| 267 | + return test_str[i1 + 1 : i2] |
| 268 | + else: |
| 269 | + return None |
| 270 | + |
| 271 | + |
| 272 | +def get_paren_level(line): |
| 273 | + """Get sub-string corresponding to a single parenthesis level, |
| 274 | + via backward search up through the line. |
| 275 | +
|
| 276 | + Examples: |
| 277 | + "CALL sub1(arg1,arg2" -> ("arg1,arg2", [[10, 19]]) |
| 278 | + "CALL sub1(arg1(i),arg2" -> ("arg1,arg2", [[10, 14], [17, 22]]) |
| 279 | + """ |
| 280 | + if line == "": |
| 281 | + return "", [[0, 0]] |
| 282 | + level = 0 |
| 283 | + in_string = False |
| 284 | + string_char = "" |
| 285 | + i1 = len(line) |
| 286 | + sections = [] |
| 287 | + for i in range(len(line) - 1, -1, -1): |
| 288 | + char = line[i] |
| 289 | + if in_string: |
| 290 | + if char == string_char: |
| 291 | + in_string = False |
| 292 | + continue |
| 293 | + if (char == "(") or (char == "["): |
| 294 | + level -= 1 |
| 295 | + if level == 0: |
| 296 | + i1 = i |
| 297 | + elif level < 0: |
| 298 | + sections.append([i + 1, i1]) |
| 299 | + break |
| 300 | + elif (char == ")") or (char == "]"): |
| 301 | + level += 1 |
| 302 | + if level == 1: |
| 303 | + sections.append([i + 1, i1]) |
| 304 | + elif (char == "'") or (char == '"'): |
| 305 | + in_string = True |
| 306 | + string_char = char |
| 307 | + if level == 0: |
| 308 | + sections.append([i, i1]) |
| 309 | + sections.reverse() |
| 310 | + out_string = "" |
| 311 | + for section in sections: |
| 312 | + out_string += line[section[0] : section[1]] |
| 313 | + return out_string, sections |
| 314 | + |
| 315 | + |
| 316 | +def get_var_stack(line): |
| 317 | + """Get user-defined type field sequence terminating the given line |
| 318 | +
|
| 319 | + Examples: |
| 320 | + "myvar%foo%bar" -> ["myvar", "foo", "bar"] |
| 321 | + "myarray(i)%foo%bar" -> ["myarray", "foo", "bar"] |
| 322 | + "CALL self%method(this%foo" -> ["this", "foo"] |
| 323 | + """ |
| 324 | + if len(line) == 0: |
| 325 | + return [""] |
| 326 | + final_var, sections = get_paren_level(line) |
| 327 | + if final_var == "": |
| 328 | + return [""] |
| 329 | + # Continuation of variable after paren requires '%' character |
| 330 | + iLast = 0 |
| 331 | + for (i, section) in enumerate(sections): |
| 332 | + if not line[section[0] : section[1]].startswith("%"): |
| 333 | + iLast = i |
| 334 | + final_var = "" |
| 335 | + for section in sections[iLast:]: |
| 336 | + final_var += line[section[0] : section[1]] |
| 337 | + # |
| 338 | + if final_var is not None: |
| 339 | + final_op_split = OBJBREAK_REGEX.split(final_var) |
| 340 | + return final_op_split[-1].split("%") |
| 341 | + else: |
| 342 | + return None |
0 commit comments