add first proc doc
This commit is contained in:
+47
-14
@@ -44,6 +44,7 @@ from tools.semantic_tokens import _Highlighter, TOKEN_TYPES, TokenModifier
|
|||||||
from tools.completion_items import completion, remove_existing_items, remove_shared_keys
|
from tools.completion_items import completion, remove_existing_items, remove_shared_keys
|
||||||
from tools.inlay_hint import InlayHintGenerator
|
from tools.inlay_hint import InlayHintGenerator
|
||||||
from tools.file_sourcing import get_all_psc_files, read_psc_file
|
from tools.file_sourcing import get_all_psc_files, read_psc_file
|
||||||
|
from tools.proc_docs import ProcDocCollector
|
||||||
from lsp_tclserver import TclLanguageServer
|
from lsp_tclserver import TclLanguageServer
|
||||||
|
|
||||||
|
|
||||||
@@ -209,29 +210,19 @@ def hover(params: lsp.HoverParams) -> lsp.Hover:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# 1) Built-in MOM procs / variables
|
||||||
command = token
|
command = token
|
||||||
data = standard_items.json_data
|
data = standard_items.json_data
|
||||||
|
|
||||||
all_items = data.get("MOM_procs", []) + data.get("mom_variables", [])
|
all_items = data.get("MOM_procs", []) + data.get("mom_variables", [])
|
||||||
|
match = next((item for item in all_items if item.get("label") == command), None)
|
||||||
match = next((item for item in all_items if item["label"] == command), None)
|
if match and match.get("kind") == "function":
|
||||||
if not match:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not match.get("kind") == "function":
|
|
||||||
return None
|
|
||||||
|
|
||||||
label = match.get("label", "")
|
label = match.get("label", "")
|
||||||
|
|
||||||
parameters = match.get("parameters", [])
|
parameters = match.get("parameters", [])
|
||||||
param_lines = "\n".join(f"- `{p['name']}`: {p['desc']}" for p in parameters) or "_None_"
|
param_lines = "\n".join(f"- `{p['name']}`: {p['desc']}" for p in parameters) or "_None_"
|
||||||
|
|
||||||
example_data = match.get("example", [])
|
example_data = match.get("example", [])
|
||||||
example_md = "\n".join(f"{line}" for line in example_data)
|
example_md = "\n".join(f"{line}" for line in example_data)
|
||||||
|
|
||||||
returns_data = match.get("returns", ["None"])
|
returns_data = match.get("returns", ["None"])
|
||||||
returns_md = "\n".join(f"- {line}" for line in returns_data)
|
returns_md = "\n".join(f"- {line}" for line in returns_data)
|
||||||
|
|
||||||
doc_md = f"""\
|
doc_md = f"""\
|
||||||
### 📘 {label}
|
### 📘 {label}
|
||||||
|
|
||||||
@@ -250,7 +241,39 @@ def hover(params: lsp.HoverParams) -> lsp.Hover:
|
|||||||
**Example**
|
**Example**
|
||||||
```tcl
|
```tcl
|
||||||
{example_md}"""
|
{example_md}"""
|
||||||
|
return lsp.Hover(lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=doc_md))
|
||||||
|
|
||||||
|
# 2) Custom procs collected from the workspace
|
||||||
|
# Find params
|
||||||
|
params_for_proc = None
|
||||||
|
for _file, mapping in LSP_SERVER.proc_signatures.items():
|
||||||
|
if command in mapping:
|
||||||
|
params_for_proc = mapping[command]
|
||||||
|
break
|
||||||
|
|
||||||
|
# Find docs
|
||||||
|
doc_text = None
|
||||||
|
if hasattr(LSP_SERVER, "proc_docs"):
|
||||||
|
for _file, mapping in getattr(LSP_SERVER, "proc_docs", {}).items():
|
||||||
|
if command in mapping:
|
||||||
|
doc_text = mapping[command]
|
||||||
|
break
|
||||||
|
|
||||||
|
if params_for_proc is None and not doc_text:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Render hover for custom proc
|
||||||
|
param_lines = "\n".join(f"- `{p}`" for p in (params_for_proc or [])) or "_None_"
|
||||||
|
# Keep the doc block as-is; it's already normalized by the collector
|
||||||
|
body = doc_text or ""
|
||||||
|
doc_md = f"""\
|
||||||
|
### 🛠️ {command}
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
{param_lines}
|
||||||
|
|
||||||
|
{body}
|
||||||
|
"""
|
||||||
return lsp.Hover(lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=doc_md))
|
return lsp.Hover(lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=doc_md))
|
||||||
|
|
||||||
|
|
||||||
@@ -318,6 +341,7 @@ def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
|
|||||||
capabilities=lsp.ServerCapabilities(
|
capabilities=lsp.ServerCapabilities(
|
||||||
document_formatting_provider=GLOBAL_SETTINGS.get("formatter", True),
|
document_formatting_provider=GLOBAL_SETTINGS.get("formatter", True),
|
||||||
semantic_tokens_provider=lsp.SemanticTokensOptions(legend=semantic_tokens_legend, full=True, range=False),
|
semantic_tokens_provider=lsp.SemanticTokensOptions(legend=semantic_tokens_legend, full=True, range=False),
|
||||||
|
hover_provider=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -337,13 +361,22 @@ def initialized(params: lsp.InitializedParams):
|
|||||||
if not filepath.exists():
|
if not filepath.exists():
|
||||||
continue
|
continue
|
||||||
completion.reset()
|
completion.reset()
|
||||||
document = LSP_SERVER.workspace.get_text_document(filepath.as_uri()) # filepath.read_text(encoding="utf-8")
|
document = LSP_SERVER.workspace.get_text_document(filepath.as_uri())
|
||||||
tree = LSP_SERVER.parser.parse(document.source)
|
tree = LSP_SERVER.parser.parse(document.source)
|
||||||
|
# completions and signatures
|
||||||
tree.accept(completion, recurse=True)
|
tree.accept(completion, recurse=True)
|
||||||
remove_existing_items(completion.custom_functions, LSP_SERVER.poco_completion)
|
remove_existing_items(completion.custom_functions, LSP_SERVER.poco_completion)
|
||||||
LSP_SERVER.poco_completion[str(filepath)] = completion.custom_functions
|
LSP_SERVER.poco_completion[str(filepath)] = completion.custom_functions
|
||||||
remove_shared_keys(LSP_SERVER.proc_signatures, completion.proc_signatures)
|
remove_shared_keys(LSP_SERVER.proc_signatures, completion.proc_signatures)
|
||||||
LSP_SERVER.proc_signatures[str(filepath)] = completion.proc_signatures
|
LSP_SERVER.proc_signatures[str(filepath)] = completion.proc_signatures
|
||||||
|
# docs
|
||||||
|
from tools.proc_docs import ProcDocCollector
|
||||||
|
|
||||||
|
doc_collector = ProcDocCollector()
|
||||||
|
tree.accept(doc_collector, recurse=True)
|
||||||
|
if not hasattr(LSP_SERVER, "proc_docs"):
|
||||||
|
LSP_SERVER.proc_docs = {}
|
||||||
|
LSP_SERVER.proc_docs[str(filepath)] = doc_collector.docs
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_to_output(f"Fehler beim Parsen von {filepath}: {e}")
|
log_to_output(f"Fehler beim Parsen von {filepath}: {e}")
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from plugins.poco_plugin import commands
|
|||||||
from tools import checks, parser
|
from tools import checks, parser
|
||||||
from pygls import server, uris
|
from pygls import server, uris
|
||||||
from tools.completion_items import completion, remove_existing_items, remove_shared_keys
|
from tools.completion_items import completion, remove_existing_items, remove_shared_keys
|
||||||
|
from tools.proc_docs import ProcDocCollector
|
||||||
|
|
||||||
|
|
||||||
DIAGNOSTIC_SOURCE = "nx-post-support"
|
DIAGNOSTIC_SOURCE = "nx-post-support"
|
||||||
@@ -24,6 +25,7 @@ class TclLanguageServer(server.LanguageServer):
|
|||||||
self.diagnostics = {}
|
self.diagnostics = {}
|
||||||
self.poco_completion: dict = {}
|
self.poco_completion: dict = {}
|
||||||
self.proc_signatures: dict = {}
|
self.proc_signatures: dict = {}
|
||||||
|
self.proc_docs: dict = {}
|
||||||
|
|
||||||
def update_poco_completion_for_file(self, document: TextDocument):
|
def update_poco_completion_for_file(self, document: TextDocument):
|
||||||
"""Update poco_completion for a specific file when it changes"""
|
"""Update poco_completion for a specific file when it changes"""
|
||||||
@@ -34,16 +36,24 @@ class TclLanguageServer(server.LanguageServer):
|
|||||||
del self.poco_completion[filepath]
|
del self.poco_completion[filepath]
|
||||||
if filepath in self.proc_signatures:
|
if filepath in self.proc_signatures:
|
||||||
del self.proc_signatures[filepath]
|
del self.proc_signatures[filepath]
|
||||||
|
if filepath in self.proc_docs:
|
||||||
|
del self.proc_docs[filepath]
|
||||||
|
|
||||||
# Parse and extract new completion items
|
# Parse and extract new completion items and docs
|
||||||
completion.reset()
|
completion.reset()
|
||||||
try:
|
try:
|
||||||
tree = self.parser.parse(document.source)
|
tree = self.parser.parse(document.source)
|
||||||
|
# completions and signatures
|
||||||
tree.accept(completion, recurse=True)
|
tree.accept(completion, recurse=True)
|
||||||
remove_existing_items(completion.custom_functions, self.poco_completion)
|
remove_existing_items(completion.custom_functions, self.poco_completion)
|
||||||
self.poco_completion[filepath] = completion.custom_functions
|
self.poco_completion[filepath] = completion.custom_functions
|
||||||
remove_shared_keys(self.proc_signatures, completion.proc_signatures)
|
remove_shared_keys(self.proc_signatures, completion.proc_signatures)
|
||||||
self.proc_signatures[filepath] = completion.proc_signatures
|
self.proc_signatures[filepath] = completion.proc_signatures
|
||||||
|
|
||||||
|
# proc docs
|
||||||
|
doc_collector = ProcDocCollector()
|
||||||
|
tree.accept(doc_collector, recurse=True)
|
||||||
|
self.proc_docs[filepath] = doc_collector.docs
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.debug(f"Error parsing {filepath}: {e}")
|
logging.debug(f"Error parsing {filepath}: {e}")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
from tclint.syntax_tree import Visitor, Command, BareWord
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class ProcDocCollector(Visitor):
|
||||||
|
"""
|
||||||
|
Collects documentation blocks preceding `proc` declarations.
|
||||||
|
|
||||||
|
Heuristic:
|
||||||
|
- Accumulate consecutive comment lines into a pending block.
|
||||||
|
- When a `proc <name> { ... }` command is visited, if there is a pending
|
||||||
|
block whose last line ends immediately above the proc line (same line or
|
||||||
|
previous line), attach the block to that proc name.
|
||||||
|
- Clear the pending block whenever we see a non-comment top-level node before
|
||||||
|
a matching `proc` to avoid associating stale docs.
|
||||||
|
|
||||||
|
The collected docs are plain text (comment markers removed). The consumer can
|
||||||
|
render them as Markdown.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.docs: Dict[str, str] = {}
|
||||||
|
self._pending_lines: List[str] = []
|
||||||
|
self._pending_end_line: Optional[int] = None
|
||||||
|
self._last_visited_node_line: Optional[int] = None
|
||||||
|
|
||||||
|
# Comments are leaf nodes; visit order is sequential across the script
|
||||||
|
def visit_comment(self, comment): # type: ignore[override]
|
||||||
|
# comment.value is the text after '#', trimmed of trailing whitespace by parser
|
||||||
|
line_no = comment.pos[0] if comment.pos else None
|
||||||
|
if self._pending_lines and self._pending_end_line is not None:
|
||||||
|
# If the current comment is directly after the previous one, keep the block,
|
||||||
|
# otherwise start a new block.
|
||||||
|
if line_no is not None and self._pending_end_line is not None and line_no <= self._pending_end_line + 1:
|
||||||
|
pass # continue current block
|
||||||
|
else:
|
||||||
|
# gap; start a new block
|
||||||
|
self._pending_lines = []
|
||||||
|
# Append this line to the pending block
|
||||||
|
value = comment.value.lstrip() if isinstance(comment.value, str) else ""
|
||||||
|
self._pending_lines.append(value)
|
||||||
|
self._pending_end_line = comment.end_pos[0] if comment.end_pos else line_no
|
||||||
|
self._last_visited_node_line = line_no
|
||||||
|
|
||||||
|
def visit_command(self, command: Command): # type: ignore[override]
|
||||||
|
# Any command other than `proc` breaks the association with a pending
|
||||||
|
# doc block; we only attach to immediate `proc`s.
|
||||||
|
routine = command.routine
|
||||||
|
proc_line = command.pos[0] if command.pos else None
|
||||||
|
|
||||||
|
if getattr(routine, "contents", None) == "proc":
|
||||||
|
# Extract proc name (first arg)
|
||||||
|
if not command.args:
|
||||||
|
self._clear_pending()
|
||||||
|
return
|
||||||
|
first = command.args[0]
|
||||||
|
name = getattr(first, "value", None) or getattr(first, "contents", None)
|
||||||
|
if not name or not isinstance(first, BareWord):
|
||||||
|
self._clear_pending()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Attach pending block if it's immediately above the proc (allow a single blank line)
|
||||||
|
attach = False
|
||||||
|
if self._pending_lines and self._pending_end_line is not None and proc_line is not None:
|
||||||
|
if self._pending_end_line in {proc_line, proc_line - 1}:
|
||||||
|
attach = True
|
||||||
|
|
||||||
|
if attach:
|
||||||
|
# Normalize text to Markdown: join lines and trim surrounding divider lines
|
||||||
|
lines = self._normalize_lines(self._pending_lines)
|
||||||
|
self.docs[name] = "\n".join(lines).strip()
|
||||||
|
# Whether attached or not, clear pending to avoid reuse
|
||||||
|
self._clear_pending()
|
||||||
|
else:
|
||||||
|
# Non-proc command before a proc clears any pending doc
|
||||||
|
self._clear_pending()
|
||||||
|
|
||||||
|
self._last_visited_node_line = proc_line
|
||||||
|
|
||||||
|
def _clear_pending(self):
|
||||||
|
self._pending_lines = []
|
||||||
|
self._pending_end_line = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_lines(lines: List[str]) -> List[str]:
|
||||||
|
# Drop long underscore divider lines commonly used
|
||||||
|
def is_divider(s: str) -> bool:
|
||||||
|
s_stripped = s.strip()
|
||||||
|
return len(s_stripped) >= 5 and set(s_stripped) in ({"_"}, {"-"}, {"="})
|
||||||
|
|
||||||
|
filtered = [ln for ln in lines if not is_divider(ln)]
|
||||||
|
# If the block contains pseudo-tags like <Documentation>, keep them; the
|
||||||
|
# hover can render them as-is or the consumer can improve formatting later.
|
||||||
|
return filtered
|
||||||
|
|
||||||
Reference in New Issue
Block a user