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