Skip to content

Commit 972a318

Browse files
committed
implement evaluate in debug server
1 parent 8a3b77e commit 972a318

File tree

3 files changed

+174
-45
lines changed

3 files changed

+174
-45
lines changed

robotcode/debug_adapter/launcher/debugger.py

Lines changed: 117 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
from __future__ import annotations
22

33
import itertools
4+
import re
45
import threading
56
import weakref
67
from collections import deque
78
from enum import Enum
89
from pathlib import Path
9-
from typing import Any, Deque, Dict, List, Literal, NamedTuple, Optional
10+
from typing import Any, Deque, Dict, List, Literal, NamedTuple, Optional, Union
1011

1112
from ...utils.event import event
1213
from ...utils.logging import LoggingDescriptor
1314
from ..types import (
1415
Breakpoint,
1516
ContinuedEvent,
1617
ContinuedEventBody,
18+
EvaluateArgumentContext,
1719
Event,
1820
OutputCategory,
1921
OutputEvent,
@@ -31,9 +33,20 @@
3133
Thread,
3234
ValueFormat,
3335
Variable,
36+
VariablePresentationHint,
3437
)
3538

3639

40+
class EvaluateResult(NamedTuple):
41+
result: str
42+
type: Optional[str] = None
43+
presentation_hint: Optional[VariablePresentationHint] = None
44+
variables_reference: int = 0
45+
named_variables: Optional[int] = None
46+
indexed_variables: Optional[int] = None
47+
memory_reference: Optional[str] = None
48+
49+
3750
class State(Enum):
3851
Stopped = 0
3952
Running = 1
@@ -61,7 +74,7 @@ class StackTraceResult(NamedTuple):
6174

6275
class StackFrameEntry:
6376
def __init__(
64-
self, context: weakref.ref[Any], name: str, type: str, source: str, line: int, column: int = 1
77+
self, context: weakref.ref[Any], name: str, type: str, source: Optional[str], line: int, column: int = 1
6578
) -> None:
6679
self.context = context
6780
self.name = name
@@ -281,7 +294,7 @@ def set_breakpoints(
281294

282295
return []
283296

284-
def process_state(self, source: str, line_no: int, type: str) -> None:
297+
def process_state(self, source: Optional[str], line_no: Optional[int], type: Optional[str]) -> None:
285298
if self.state == State.Stopped:
286299
return
287300

@@ -336,21 +349,22 @@ def process_state(self, source: str, line_no: int, type: str) -> None:
336349
)
337350
self.requested_state = RequestedState.Nothing
338351

339-
source = str(Path(source).resolve())
340-
if source in self.breakpoints:
341-
breakpoints = [v for v in self.breakpoints[source].breakpoints if v.line == line_no]
342-
if len(breakpoints) > 0:
343-
self.state = State.Paused
344-
self.send_event(
345-
self,
346-
StoppedEvent(
347-
body=StoppedEventBody(
348-
reason=StoppedReason.BREAKPOINT,
349-
thread_id=threading.current_thread().ident,
350-
hit_breakpoint_ids=[id(v) for v in breakpoints],
351-
)
352-
),
353-
)
352+
if source is not None:
353+
source = str(Path(source).resolve())
354+
if source in self.breakpoints:
355+
breakpoints = [v for v in self.breakpoints[source].breakpoints if v.line == line_no]
356+
if len(breakpoints) > 0:
357+
self.state = State.Paused
358+
self.send_event(
359+
self,
360+
StoppedEvent(
361+
body=StoppedEventBody(
362+
reason=StoppedReason.BREAKPOINT,
363+
thread_id=threading.current_thread().ident,
364+
hit_breakpoint_ids=[id(v) for v in breakpoints],
365+
)
366+
),
367+
)
354368

355369
@_logger.call
356370
def wait_for_running(self) -> None:
@@ -367,7 +381,8 @@ def start_output_group(self, name: str, attributes: Dict[str, Any], type: Option
367381
OutputEvent(
368382
body=OutputEventBody(
369383
output=f"{(type +' ') if type else ''}{name}\n",
370-
category=OutputCategory.CONSOLE,
384+
# category=OutputCategory.CONSOLE,
385+
category="log",
371386
group=OutputGroup.STARTCOLLAPSED,
372387
source=Source(name=name, path=source) if source else None,
373388
line=line_no,
@@ -385,27 +400,52 @@ def end_output_group(self, name: str, attributes: Dict[str, Any]) -> None:
385400
OutputEvent(
386401
body=OutputEventBody(
387402
output="",
388-
category=OutputCategory.CONSOLE,
403+
# category=OutputCategory.CONSOLE,
404+
category="log",
389405
group=OutputGroup.END,
390406
source=Source(name=name, path=source) if source else None,
391407
line=line_no,
392408
)
393409
),
394410
)
395411

396-
def start_suite(self, name: str, attributes: Dict[str, Any]) -> None:
412+
def add_stackframe_entry(
413+
self, name: str, type: str, source: Optional[str], line: Optional[int], column: Optional[int] = 1
414+
) -> StackFrameEntry:
397415
from robot.running.context import EXECUTION_CONTEXTS
398416

417+
if source is None or line is None or column is None:
418+
for v in self.stack_frames:
419+
if source is None:
420+
source = v.source
421+
if line is None:
422+
line = v.line
423+
if column is None:
424+
column = v.column
425+
if source is not None and line is not None and column is not None:
426+
break
427+
428+
result = StackFrameEntry(
429+
weakref.ref(EXECUTION_CONTEXTS.current),
430+
name,
431+
type,
432+
source,
433+
line if line is not None else 0,
434+
column if column is not None else 0,
435+
)
436+
self.stack_frames.appendleft(result)
437+
438+
return result
439+
440+
def start_suite(self, name: str, attributes: Dict[str, Any]) -> None:
399441
source = attributes.get("source", None)
400442
line_no = attributes.get("lineno", 1)
401443
longname = attributes.get("longname", "")
402444
type = "SUITE"
403445

404-
self.stack_frames.appendleft(
405-
StackFrameEntry(weakref.ref(EXECUTION_CONTEXTS.current), longname, type, source, line_no)
406-
)
446+
entry = self.add_stackframe_entry(longname, type, source, line_no)
407447

408-
self.process_state(source, line_no, type)
448+
self.process_state(entry.source, entry.line, entry.type)
409449

410450
self.wait_for_running()
411451

@@ -414,18 +454,14 @@ def end_suite(self, name: str, attributes: Dict[str, Any]) -> None:
414454
self.stack_frames.popleft()
415455

416456
def start_test(self, name: str, attributes: Dict[str, Any]) -> None:
417-
from robot.running.context import EXECUTION_CONTEXTS
418-
419457
source = attributes.get("source", None)
420458
line_no = attributes.get("lineno", 1)
421459
longname = attributes.get("longname", "")
422460
type = "TEST"
423461

424-
self.stack_frames.appendleft(
425-
StackFrameEntry(weakref.ref(EXECUTION_CONTEXTS.current), longname, type, source, line_no)
426-
)
462+
entry = self.add_stackframe_entry(longname, type, source, line_no)
427463

428-
self.process_state(source, line_no, type)
464+
self.process_state(entry.source, entry.line, entry.type)
429465

430466
self.wait_for_running()
431467

@@ -434,8 +470,6 @@ def end_test(self, name: str, attributes: Dict[str, Any]) -> None:
434470
self.stack_frames.popleft()
435471

436472
def start_keyword(self, name: str, attributes: Dict[str, Any]) -> None:
437-
from robot.running.context import EXECUTION_CONTEXTS
438-
439473
status = attributes.get("status", "")
440474

441475
if status == "NOT RUN":
@@ -446,11 +480,9 @@ def start_keyword(self, name: str, attributes: Dict[str, Any]) -> None:
446480
kwname = attributes.get("kwname", "")
447481
type = attributes.get("type", "KEYWORD")
448482

449-
self.stack_frames.appendleft(
450-
StackFrameEntry(weakref.ref(EXECUTION_CONTEXTS.current), kwname, type, source, line_no)
451-
)
483+
entry = self.add_stackframe_entry(kwname, type, source, line_no)
452484

453-
self.process_state(source, line_no, type)
485+
self.process_state(entry.source, entry.line, entry.type)
454486

455487
self.wait_for_running()
456488

@@ -480,13 +512,19 @@ def get_stack_trace(
480512
) -> StackTraceResult:
481513
start_frame = start_frame or 0
482514
levels = start_frame + 1 + (levels or len(self.stack_frames))
483-
return StackTraceResult(
484-
[
485-
StackFrame(id=v.id, name=v.name, line=v.line, column=v.column, source=Source(path=v.source))
486-
for v in itertools.islice(self.stack_frames, start_frame, levels)
487-
],
488-
len(self.stack_frames),
489-
)
515+
516+
frames = [
517+
StackFrame(
518+
id=v.id,
519+
name=v.name,
520+
line=v.line,
521+
column=v.column,
522+
source=Source(path=v.source) if v.source is not None else None,
523+
)
524+
for v in itertools.islice(self.stack_frames, start_frame, levels)
525+
]
526+
527+
return StackTraceResult(frames, len(frames))
490528

491529
def log_message(self, message: Dict[str, Any]) -> None:
492530
if self.output_log:
@@ -609,3 +647,38 @@ def get_variables(
609647
]
610648

611649
return result
650+
651+
VARS_RE = re.compile(r"^[$@&%]\{.*\}$")
652+
653+
def evaluate(
654+
self,
655+
expression: str,
656+
frame_id: Optional[int] = None,
657+
context: Union[EvaluateArgumentContext, str, None] = None,
658+
format: Optional[ValueFormat] = None,
659+
) -> EvaluateResult:
660+
from robot.running.context import EXECUTION_CONTEXTS
661+
from robot.variables.evaluation import evaluate_expression
662+
663+
evaluate_context: Any = None
664+
665+
if frame_id is not None:
666+
evaluate_context = (
667+
next((v.context() for v in self.stack_frames if v.id == frame_id), None)
668+
if frame_id is not None
669+
else None
670+
)
671+
672+
if evaluate_context is None:
673+
evaluate_context = EXECUTION_CONTEXTS.current
674+
675+
try:
676+
vars = evaluate_context.variables.current if frame_id is not None else evaluate_context.variables._global
677+
if self.VARS_RE.match(expression.strip()):
678+
result = vars.replace_string(expression)
679+
else:
680+
result = evaluate_expression(vars.replace_string(expression), vars.store)
681+
except BaseException as e:
682+
result = e
683+
684+
return EvaluateResult(repr(result), repr(type(result)))

robotcode/debug_adapter/launcher/server.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import asyncio
22
import os
3-
from typing import Any, Literal, Optional
3+
from typing import Any, Literal, Optional, Union
44

55
from ...jsonrpc2.protocol import rpc_method
66
from ...jsonrpc2.server import JsonRPCServer, JsonRpcServerMode, TcpParams
@@ -11,6 +11,9 @@
1111
ContinueArguments,
1212
ContinueResponseBody,
1313
DisconnectArguments,
14+
EvaluateArgumentContext,
15+
EvaluateArguments,
16+
EvaluateResponseBody,
1417
Event,
1518
ExitedEvent,
1619
ExitedEventBody,
@@ -206,6 +209,26 @@ async def _variables(
206209
variables=Debugger.instance().get_variables(variables_reference, filter, start, count, format)
207210
)
208211

212+
@rpc_method(name="evaluate", param_type=EvaluateArguments)
213+
async def _evaluate(
214+
self,
215+
arguments: ScopesArguments,
216+
expression: str,
217+
frame_id: Optional[int] = None,
218+
context: Union[EvaluateArgumentContext, str, None] = None,
219+
format: Optional[ValueFormat] = None,
220+
) -> EvaluateResponseBody:
221+
result = Debugger.instance().evaluate(expression, frame_id, context, format)
222+
return EvaluateResponseBody(
223+
result=result.result,
224+
type=result.type,
225+
presentation_hint=result.presentation_hint,
226+
variables_reference=result.variables_reference,
227+
named_variables=result.named_variables,
228+
indexed_variables=result.indexed_variables,
229+
memory_reference=result.memory_reference,
230+
)
231+
209232

210233
class LaucherServer(JsonRPCServer[LauncherServerProtocol]):
211234
def __init__(

robotcode/debug_adapter/types.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,3 +675,36 @@ class VariablesResponseBody(Model):
675675

676676
class VariablesResponse(Response):
677677
body: VariablesResponseBody
678+
679+
680+
class EvaluateArgumentContext(Enum):
681+
WATCH = "watch"
682+
REPL = "repl"
683+
HOVER = "hover"
684+
CLIPBOARD = "clipboard"
685+
686+
687+
class EvaluateArguments(Model):
688+
expression: str
689+
frame_id: Optional[int] = None
690+
context: Union[EvaluateArgumentContext, str, None] = None
691+
format: Optional[ValueFormat] = None
692+
693+
694+
class EvaluateRequest(Request):
695+
command: str = Field("evaluate", const=True)
696+
arguments: EvaluateArguments
697+
698+
699+
class EvaluateResponseBody(Model):
700+
result: str
701+
type: Optional[str] = None
702+
presentation_hint: Optional[VariablePresentationHint] = None
703+
variables_reference: int = 0
704+
named_variables: Optional[int] = None
705+
indexed_variables: Optional[int] = None
706+
memory_reference: Optional[str] = None
707+
708+
709+
class EvaluateResponse(Response):
710+
body: VariablesResponseBody

0 commit comments

Comments
 (0)