@@ -120,6 +120,52 @@ def _idle_watcher():
120120 watcher .start ()
121121
122122
123+ def _analyze_node (node_id : str ) -> dict | None :
124+ """Look up *node_id* and return an analyzed dict, or None if not found."""
125+ if node_id not in _Handler .node_lookup :
126+ return None
127+ node = _Handler .node_lookup [node_id ]
128+ if node .summary is not None :
129+ return _node_to_dict (node , include_source = True )
130+ with _Handler .analyze_lock :
131+ if node .summary is None :
132+ from codedocent .analyzer import analyze_single_node # pylint: disable=import-outside-toplevel # noqa: E501
133+
134+ analyze_single_node (node , _Handler .model , _Handler .cache_dir )
135+ return _node_to_dict (node , include_source = True )
136+
137+
138+ def _execute_replace (node_id : str , body : dict ) -> tuple [int , dict ]:
139+ """Validate and execute a source-replacement request.
140+
141+ Returns ``(status_code, result_dict)``.
142+ """
143+ if node_id not in _Handler .node_lookup :
144+ return (404 , {"success" : False , "error" : "Unknown node ID" })
145+ node = _Handler .node_lookup [node_id ]
146+ if node .node_type in ("directory" , "file" ):
147+ return (
148+ 400 ,
149+ {"success" : False ,
150+ "error" : "Cannot replace directory/file blocks" },
151+ )
152+ new_source = body .get ("source" , "" )
153+ if not isinstance (new_source , str ):
154+ return (400 , {"success" : False , "error" : "source must be a string" })
155+ abs_path = _resolve_filepath (node , _Handler .cache_dir )
156+ from codedocent .editor import replace_block_source # pylint: disable=import-outside-toplevel # noqa: E501
157+
158+ with _Handler .analyze_lock :
159+ result = replace_block_source (
160+ abs_path , node .start_line , node .end_line , new_source ,
161+ )
162+ if result ["success" ]:
163+ _update_node_after_replace (
164+ node , new_source , result , _Handler .cache_dir ,
165+ )
166+ return (200 , result )
167+
168+
123169class _Handler (BaseHTTPRequestHandler ):
124170 """HTTP request handler for codedocent server."""
125171
@@ -175,101 +221,30 @@ def _serve_html(self):
175221 self .wfile .write (data )
176222
177223 def _serve_tree (self ):
178- tree_dict = _node_to_dict (_Handler .root )
179- data = json .dumps (tree_dict ).encode ("utf-8" )
180- self .send_response (200 )
181- self .send_header ("Content-Type" , "application/json" )
182- self .send_header ("Content-Length" , str (len (data )))
183- self .end_headers ()
184- self .wfile .write (data )
224+ self ._send_json (200 , _node_to_dict (_Handler .root ))
185225
186226 def _handle_source (self , node_id : str ):
187227 if node_id not in _Handler .node_lookup :
188228 self .send_error (404 , "Unknown node ID" )
189229 return
190230 node = _Handler .node_lookup [node_id ]
191- result = {"source" : node .source or "" }
192- data = json .dumps (result ).encode ("utf-8" )
193- self .send_response (200 )
194- self .send_header ("Content-Type" , "application/json" )
195- self .send_header ("Content-Length" , str (len (data )))
196- self .end_headers ()
197- self .wfile .write (data )
231+ self ._send_json (200 , {"source" : node .source or "" })
198232
199233 def _handle_analyze (self , node_id : str ):
200- if node_id not in _Handler .node_lookup :
234+ result = _analyze_node (node_id )
235+ if result is None :
201236 self .send_error (404 , "Unknown node ID" )
202237 return
203-
204- node = _Handler .node_lookup [node_id ]
205-
206- # Return cached result if already analyzed
207- if node .summary is not None :
208- result = _node_to_dict (node , include_source = True )
209- data = json .dumps (result ).encode ("utf-8" )
210- self .send_response (200 )
211- self .send_header ("Content-Type" , "application/json" )
212- self .send_header ("Content-Length" , str (len (data )))
213- self .end_headers ()
214- self .wfile .write (data )
215- return
216-
217- # Run analysis (thread-safe)
218- with _Handler .analyze_lock :
219- # Double-check after acquiring lock
220- if node .summary is None :
221- from codedocent .analyzer import analyze_single_node # pylint: disable=import-outside-toplevel # noqa: E501
222-
223- analyze_single_node (node , _Handler .model , _Handler .cache_dir )
224-
225- result = _node_to_dict (node , include_source = True )
226- data = json .dumps (result ).encode ("utf-8" )
227- self .send_response (200 )
228- self .send_header ("Content-Type" , "application/json" )
229- self .send_header ("Content-Length" , str (len (data )))
230- self .end_headers ()
231- self .wfile .write (data )
238+ self ._send_json (200 , result )
232239
233240 def _handle_replace (self , node_id : str ):
234- if node_id not in _Handler .node_lookup :
235- self .send_error (404 , "Unknown node ID" )
236- return
237-
238- node = _Handler .node_lookup [node_id ]
239-
240- if node .node_type in ("directory" , "file" ):
241- self ._send_json (
242- 400 ,
243- {"success" : False ,
244- "error" : "Cannot replace directory/file blocks" },
245- )
246- return
247-
248241 content_length = int (self .headers ["Content-Length" ])
249242 body = json .loads (self .rfile .read (content_length ))
250- new_source = body .get ("source" , "" )
251-
252- if not isinstance (new_source , str ):
253- self ._send_json (
254- 400 ,
255- {"success" : False , "error" : "source must be a string" },
256- )
243+ status , result = _execute_replace (node_id , body )
244+ if status == 404 :
245+ self .send_error (404 , result ["error" ])
257246 return
258-
259- abs_path = _resolve_filepath (node , _Handler .cache_dir )
260-
261- from codedocent .editor import replace_block_source # pylint: disable=import-outside-toplevel # noqa: E501
262-
263- with _Handler .analyze_lock :
264- result = replace_block_source (
265- abs_path , node .start_line , node .end_line , new_source ,
266- )
267- if result ["success" ]:
268- _update_node_after_replace (
269- node , new_source , result , _Handler .cache_dir ,
270- )
271-
272- self ._send_json (200 , result )
247+ self ._send_json (status , result )
273248
274249 def _send_json (self , status_code : int , obj : dict ):
275250 data = json .dumps (obj ).encode ("utf-8" )
0 commit comments