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
9
1
import urllib .parse
10
- from concurrent .futures import ProcessPoolExecutor
11
2
from 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
17
3
from typing import TYPE_CHECKING , Any , List , Optional , Tuple , Union , cast
18
- from urllib .parse import parse_qs , urlparse
19
4
20
5
from robot .parsing .lexer .tokens import Token
21
6
31
16
from robotcode .core .uri import Uri
32
17
from robotcode .core .utils .dataclasses import CamelSnakeMixin
33
18
from robotcode .core .utils .logging import LoggingDescriptor
34
- from robotcode .core .utils .net import find_free_port
35
19
from robotcode .jsonrpc2 .protocol import rpc_method
36
20
from 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
42
22
from robotcode .robot .diagnostics .model_helper import ModelHelper
43
23
from robotcode .robot .diagnostics .namespace import Namespace
44
24
from robotcode .robot .utils .ast import get_node_at_position , range_from_token
45
25
46
26
from ...common .decorators import code_action_kinds
47
- from ..configuration import DocumentationServerConfig
48
27
from .protocol_part import RobotLanguageServerProtocolPart
49
28
50
29
if TYPE_CHECKING :
@@ -56,190 +35,14 @@ class ConvertUriParams(CamelSnakeMixin):
56
35
uri : str
57
36
58
37
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
-
194
38
class RobotCodeActionDocumentationProtocolPart (RobotLanguageServerProtocolPart , ModelHelper ):
195
39
_logger = LoggingDescriptor ()
196
40
197
- def __init__ (self , parent : RobotLanguageServerProtocol ) -> None :
41
+ def __init__ (self , parent : " RobotLanguageServerProtocol" ) -> None :
198
42
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
-
208
43
self .parent .commands .register_all (self )
209
44
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 )
243
46
244
47
@language_id ("robotframework" )
245
48
@code_action_kinds ([CodeActionKind .SOURCE ])
@@ -406,7 +209,7 @@ def build_url(
406
209
407
210
url_args = "::" .join (args ) if args else ""
408
211
409
- base_url = f"http://localhost:{ self ._documentation_server_port } "
212
+ base_url = f"http://localhost:{ self .parent . http_server . port } "
410
213
params = urllib .parse .urlencode (
411
214
{
412
215
"name" : name ,
@@ -427,6 +230,6 @@ def _convert_uri(self, uri: str, *args: Any, **kwargs: Any) -> Optional[str]:
427
230
if folder :
428
231
path = real_uri .to_path ().relative_to (folder .uri .to_path ())
429
232
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 ()} "
431
234
432
235
return None
0 commit comments