Coverage for vibefpmgui/window/language.py: 80%

94 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-11-01 20:51 +0000

1"""The language server (fortls) for the editor: starting it, symbols, references, rename.""" 

2 

3from __future__ import annotations 

4 

5from pathlib import Path 

6 

7from qtpy.QtWidgets import QDialog, QMessageBox 

8 

9from .. import lsp 

10from ..editor import EditorTab 

11from ..rename_dialog import RenamePreviewDialog 

12from ..source_text import read_source 

13from ..symbol_search import SymbolSearchDialog 

14from ..widgets import run_dialog 

15 

16 

17class LanguageMixin: 

18 """The language server's features (a part of `vibefpmgui.main_window.MainWindow`).""" 

19 

20 def _update_language_server(self): 

21 """Run fortls for the open project (restarting it if the project or 

22 its folders changed), and connect the open Fortran tabs to it.""" 

23 if not self.fortls_action.isChecked() or lsp.executable() is None or self.manifest_path is None: 

24 self._stop_language_server() 

25 return 

26 root = self.manifest_path.parent.resolve() 

27 args = lsp.server_args(self.graph, root) 

28 client = self.lsp_client 

29 if client.is_running() and client.root == root and client.args == args: 29 ↛ 30line 29 didn't jump to line 30 because the condition on line 29 was never true

30 return 

31 for editor in self.editors(): 31 ↛ 32line 31 didn't jump to line 32 because the loop on line 31 never started

32 editor.detach_lsp() 

33 client.start(root, args) 

34 for editor in self.editors(): 34 ↛ 35line 34 didn't jump to line 35 because the loop on line 34 never started

35 editor.attach_lsp(client) 

36 self._update_editor_actions() 

37 

38 def _stop_language_server(self): 

39 for editor in self.editors(): 

40 editor.detach_lsp() 

41 self.lsp_client.stop() 

42 self._update_editor_actions() 

43 

44 def _language_server_stopped(self, why: str): 

45 for editor in self.editors(): 

46 editor.detach_lsp() 

47 self.statusBar().showMessage(f"{why} — hover, Go to Definition and the outline are off " 

48 "until the project is reloaded", 10000) 

49 self._update_editor_actions() 

50 

51 def set_fortls_enabled(self, on: bool): 

52 self.settings.setValue("fortls", on) 

53 self._update_language_server() 

54 

55 def go_to_symbol(self): 

56 if not self.lsp_client.is_running(): 56 ↛ 58line 56 didn't jump to line 58 because the condition on line 56 was always true

57 return 

58 dialog = SymbolSearchDialog(self.lsp_client, self.manifest_path.parent if self.manifest_path else None, 

59 self) 

60 dialog.chosen.connect(self.navigate_to) 

61 run_dialog(dialog) 

62 

63 def _open_editor_for(self, path: Path) -> EditorTab | None: 

64 path = path.resolve() 

65 return next((e for e in self.editors() if e.path == path), None) 

66 

67 def _file_text(self, path: Path) -> str: 

68 """A file's current text: the editor's (maybe unsaved) if it's open.""" 

69 editor = self._open_editor_for(path) 

70 if editor is not None: 

71 return editor.editor.toPlainText() 

72 return read_source(path).text 

73 

74 def _show_references(self, name: str, results: list): 

75 roots, _texts, project_dir = self._search_sources() 

76 self.references_panel.show_results(name, results, roots, project_dir) 

77 self.show_panel(self.references_panel) 

78 self.statusBar().showMessage(f"{len(results)} reference{'s' * (len(results) != 1)} to {name}", 5000) 

79 

80 def _preview_rename(self, old: str, new: str, edits: dict): 

81 edits = lsp.add_result_variable_edits(edits, old, new, self._file_text) 

82 open_files = {e.path for e in self.editors()} 

83 dialog = RenamePreviewDialog(old, new, edits, self._file_text, open_files, 

84 self.manifest_path.parent if self.manifest_path else None, 

85 self._mono_font(), self) 

86 if run_dialog(dialog) != QDialog.Accepted: 

87 return 

88 changed_open, written, failed, places = 0, 0, [], 0 

89 for path in dialog.selected(): # (deleted later: still there now) 

90 file_edits = edits.get(path, []) 

91 editor = self._open_editor_for(path) 

92 if editor is not None: 

93 editor.apply_text_edits(file_edits) # unsaved; Undo reverts it 

94 changed_open += 1 

95 else: 

96 try: 

97 text, encoding, newline, _mixed = read_source(path) 

98 text = lsp.apply_edits(text, file_edits) 

99 path.write_bytes(text.replace("\n", newline).encode(encoding, errors="replace")) 

100 except OSError as exc: 

101 failed.append(f"{path}: {exc}") 

102 continue 

103 self.lsp_client.file_changed_on_disk(path) 

104 written += 1 

105 places += len(file_edits) 

106 if failed: 106 ↛ 107line 106 didn't jump to line 107 because the condition on line 106 was never true

107 QMessageBox.warning(self, "Rename Symbol", "Some files could not be changed:\n\n" 

108 + "\n".join(failed)) 

109 parts = [] 

110 if written: 110 ↛ 112line 110 didn't jump to line 112 because the condition on line 110 was always true

111 parts.append(f"{written} file{'s' * (written != 1)} saved") 

112 if changed_open: 112 ↛ 114line 112 didn't jump to line 114 because the condition on line 112 was always true

113 parts.append(f"{changed_open} open file{'s' * (changed_open != 1)} changed (not saved yet)") 

114 self.statusBar().showMessage(f"Renamed {old} → {new} in {places} place{'s' * (places != 1)}" 

115 + (f": {', '.join(parts)}" if parts else ""), 10000) 

116 self.project_tree.refresh() 

117 

118 def _lsp_diagnostics(self, path: Path, issues: list): 

119 """fortls checked a file (opened or saved): show it in its tab.""" 

120 self._lsp_diagnostics_by_file[path] = issues 

121 editor = self._open_editor_for(path) 

122 if editor is not None: 

123 editor.show_lsp_diagnostics(issues)