1- from __future__ import annotations
2-
3- import contextlib
4- import io
5- import multiprocessing as mp
6- import socket
7- import threading
8- import traceback
91import urllib .parse
10- from concurrent .futures import ProcessPoolExecutor
112from dataclasses import dataclass
12- from http import HTTPStatus
13- from http .server import SimpleHTTPRequestHandler , ThreadingHTTPServer
14- from os import PathLike
15- from string import Template
16- from threading import Thread
173from typing import TYPE_CHECKING , Any , List , Optional , Tuple , Union , cast
18- from urllib .parse import parse_qs , urlparse
194
205from robot .parsing .lexer .tokens import Token
216
3116from robotcode .core .uri import Uri
3217from robotcode .core .utils .dataclasses import CamelSnakeMixin
3318from robotcode .core .utils .logging import LoggingDescriptor
34- from robotcode .core .utils .net import find_free_port
3519from robotcode .jsonrpc2 .protocol import rpc_method
3620from robotcode .robot .diagnostics .entities import LibraryEntry
37- from robotcode .robot .diagnostics .library_doc import (
38- get_library_doc ,
39- get_robot_library_html_doc_str ,
40- resolve_robot_variables ,
41- )
21+ from robotcode .robot .diagnostics .library_doc import resolve_robot_variables
4222from robotcode .robot .diagnostics .model_helper import ModelHelper
4323from robotcode .robot .diagnostics .namespace import Namespace
4424from robotcode .robot .utils .ast import get_node_at_position , range_from_token
4525
4626from ...common .decorators import code_action_kinds
47- from ..configuration import DocumentationServerConfig
4827from .protocol_part import RobotLanguageServerProtocolPart
4928
5029if TYPE_CHECKING :
@@ -56,190 +35,14 @@ class ConvertUriParams(CamelSnakeMixin):
5635 uri : str
5736
5837
59- HTML_ERROR_TEMPLATE = Template (
60- """\n
61- <!doctype html>
62- <html>
63- <head>
64- <meta charset="utf-8"/>
65- <title>${type}: ${message}</title>
66- </head>
67- <body>
68- <div id="content">
69- <h3>
70- ${type}: ${message}
71- </h3>
72- <pre>
73- ${stacktrace}
74- </pre>
75- </div>
76-
77- </body>
78- </html>
79- """
80- )
81-
82- MARKDOWN_TEMPLATE = Template (
83- """\
84- <!doctype html>
85- <html>
86- <head>
87- <meta charset="utf-8"/>
88- <title>${name}</title>
89- </head>
90- <body>
91- <template type="markdown" id="markdown-content">${content}</template>
92- <div id="content"></div>
93- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
94- <script>
95- document.getElementById('content').innerHTML =
96- marked.parse(document.getElementById('markdown-content').content.textContent, {gfm: true});
97- </script>
98- </body>
99- </html>
100- """
101- )
102-
103-
104- class LibDocRequestHandler (SimpleHTTPRequestHandler ):
105- _logger = LoggingDescriptor ()
106-
107- def log_message (self , format : str , * args : Any ) -> None :
108- self ._logger .info (lambda : f"{ self .address_string ()} - { format % args } " )
109-
110- def log_error (self , format : str , * args : Any ) -> None :
111- self ._logger .error (lambda : f"{ self .address_string ()} - { format % args } " )
112-
113- def list_directory (self , _path : Union [str , PathLike [str ]]) -> io .BytesIO | None :
114- self .send_error (
115- HTTPStatus .FORBIDDEN ,
116- "You don't have permission to access this resource." ,
117- "Directory browsing is not allowed." ,
118- )
119- return None
120-
121- def do_GET (self ) -> None : # noqa: N802
122- query = parse_qs (urlparse (self .path ).query )
123- name = n [0 ] if (n := query .get ("name" , [])) else None
124- args = n [0 ] if (n := query .get ("args" , [])) else None
125- basedir = n [0 ] if (n := query .get ("basedir" , [])) else None
126- type_ = n [0 ] if (n := query .get ("type" , [])) else None
127- theme = n [0 ] if (n := query .get ("theme" , [])) else None
128-
129- if name :
130- try :
131- if type_ in ["md" , "markdown" ]:
132- libdoc = get_library_doc (
133- name ,
134- tuple (args .split ("::" ) if args else ()),
135- base_dir = basedir if basedir else "." ,
136- )
137-
138- def calc_md () -> str :
139- tt = str .maketrans ({"<" : "<" , ">" : ">" })
140- return libdoc .to_markdown (add_signature = False , only_doc = False , header_level = 0 ).translate (tt )
141-
142- data = MARKDOWN_TEMPLATE .substitute (content = calc_md (), name = name )
143-
144- self .send_response (200 )
145- self .send_header ("Content-type" , "text/html" )
146- self .end_headers ()
147-
148- self .wfile .write (bytes (data , "utf-8" ))
149- else :
150- with ProcessPoolExecutor (max_workers = 1 , mp_context = mp .get_context ("spawn" )) as executor :
151- result = executor .submit (
152- get_robot_library_html_doc_str ,
153- name ,
154- args ,
155- base_dir = basedir if basedir else "." ,
156- theme = theme ,
157- ).result (600 )
158-
159- self .send_response (200 )
160- self .send_header ("Content-type" , "text/html" )
161- self .end_headers ()
162-
163- self .wfile .write (bytes (result , "utf-8" ))
164- except (SystemExit , KeyboardInterrupt ):
165- raise
166- except BaseException as e :
167- self .send_response (404 )
168- self .send_header ("Content-type" , "text/html" )
169- self .end_headers ()
170-
171- self .wfile .write (
172- bytes (
173- HTML_ERROR_TEMPLATE .substitute (
174- type = type (e ).__qualname__ ,
175- message = str (e ),
176- stacktrace = "" .join (traceback .format_exc ()),
177- ),
178- "utf-8" ,
179- )
180- )
181-
182- else :
183- super ().do_GET ()
184-
185-
186- class DualStackServer (ThreadingHTTPServer ):
187- def server_bind (self ) -> None :
188- # suppress exception when protocol is IPv4
189- with contextlib .suppress (Exception ):
190- self .socket .setsockopt (socket .IPPROTO_IPV6 , socket .IPV6_V6ONLY , 0 )
191- return super ().server_bind ()
192-
193-
19438class RobotCodeActionDocumentationProtocolPart (RobotLanguageServerProtocolPart , ModelHelper ):
19539 _logger = LoggingDescriptor ()
19640
197- def __init__ (self , parent : RobotLanguageServerProtocol ) -> None :
41+ def __init__ (self , parent : " RobotLanguageServerProtocol" ) -> None :
19842 super ().__init__ (parent )
199-
200- parent .code_action .collect .add (self .collect )
201- self .parent .on_initialized .add (self .server_initialized )
202- self .parent .on_shutdown .add (self .server_shutdown )
203-
204- self ._documentation_server : Optional [ThreadingHTTPServer ] = None
205- self ._documentation_server_lock = threading .RLock ()
206- self ._documentation_server_port = 0
207-
20843 self .parent .commands .register_all (self )
20944
210- def server_initialized (self , sender : Any ) -> None :
211- self ._ensure_http_server_started ()
212-
213- def server_shutdown (self , sender : Any ) -> None :
214- with self ._documentation_server_lock :
215- if self ._documentation_server is not None :
216- self ._documentation_server .shutdown ()
217- self ._documentation_server = None
218-
219- def _run_server (self ) -> None :
220- config = self .parent .workspace .get_configuration (DocumentationServerConfig )
221-
222- self ._documentation_server_port = find_free_port (config .start_port , config .end_port )
223-
224- self ._logger .debug (lambda : f"Start documentation server on port { self ._documentation_server_port } " )
225-
226- with DualStackServer (("127.0.0.1" , self ._documentation_server_port ), LibDocRequestHandler ) as server :
227- self ._documentation_server = server
228- try :
229- server .serve_forever ()
230- except BaseException :
231- self ._documentation_server = None
232- raise
233-
234- def _ensure_http_server_started (self ) -> None :
235- with self ._documentation_server_lock :
236- if self ._documentation_server is None :
237- self ._server_thread = Thread (
238- name = "documentation_server" ,
239- target = self ._run_server ,
240- daemon = True ,
241- )
242- self ._server_thread .start ()
45+ parent .code_action .collect .add (self .collect )
24346
24447 @language_id ("robotframework" )
24548 @code_action_kinds ([CodeActionKind .SOURCE ])
@@ -406,7 +209,7 @@ def build_url(
406209
407210 url_args = "::" .join (args ) if args else ""
408211
409- base_url = f"http://localhost:{ self ._documentation_server_port } "
212+ base_url = f"http://localhost:{ self .parent . http_server . port } "
410213 params = urllib .parse .urlencode (
411214 {
412215 "name" : name ,
@@ -427,6 +230,6 @@ def _convert_uri(self, uri: str, *args: Any, **kwargs: Any) -> Optional[str]:
427230 if folder :
428231 path = real_uri .to_path ().relative_to (folder .uri .to_path ())
429232
430- return f"http://localhost:{ self ._documentation_server_port } /{ path .as_posix ()} "
233+ return f"http://localhost:{ self .parent . http_server . port } /{ path .as_posix ()} "
431234
432235 return None
0 commit comments