Skip to content

Commit 3c5c706

Browse files
better markdown support for lsp message logging
1 parent 2422e9a commit 3c5c706

File tree

11 files changed

+207
-94
lines changed

11 files changed

+207
-94
lines changed

codeflash/api/aiservice.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,6 @@ def optimize_python_code( # noqa: D417
133133
"repo_name": git_repo_name,
134134
}
135135

136-
logger.info("Generating optimized candidates…")
137136
console.rule()
138137
try:
139138
response = self.make_ai_service_request("/optimize", payload=payload, timeout=600)
@@ -144,8 +143,6 @@ def optimize_python_code( # noqa: D417
144143

145144
if response.status_code == 200:
146145
optimizations_json = response.json()["optimizations"]
147-
logger.info(f"Generated `{len(optimizations_json)}` candidate optimizations.")
148-
console.rule()
149146
end_time = time.perf_counter()
150147
logger.debug(f"Generating optimizations took {end_time - start_time:.2f} seconds.")
151148
return self._get_valid_candidates(optimizations_json)
@@ -194,7 +191,7 @@ def optimize_python_code_line_profiler( # noqa: D417
194191
"lsp_mode": is_LSP_enabled(),
195192
}
196193

197-
logger.info("Generating optimized candidates…")
194+
logger.info("loading|tags|Generating optimized candidates using line profiler information…")
198195
console.rule()
199196
if line_profiler_results == "":
200197
logger.info("No LineProfiler results were provided, Skipping optimization.")
@@ -209,7 +206,6 @@ def optimize_python_code_line_profiler( # noqa: D417
209206

210207
if response.status_code == 200:
211208
optimizations_json = response.json()["optimizations"]
212-
logger.info(f"Generated {len(optimizations_json)} candidate optimizations using line profiler information.")
213209
console.rule()
214210
return self._get_valid_candidates(optimizations_json)
215211
try:

codeflash/cli_cmds/console.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,13 @@
4949
# override the logger to reformat the messages for the lsp
5050
for level in ("info", "debug", "warning", "error"):
5151
real_fn = getattr(logger, level)
52-
setattr(logger, level, lambda msg, *args, _real_fn=real_fn, **kwargs: enhanced_log(msg, _real_fn, *args, **kwargs))
52+
setattr(
53+
logger,
54+
level,
55+
lambda msg, _real_fn=real_fn, _level=level, *args, **kwargs: enhanced_log(
56+
msg, _real_fn, _level, *args, **kwargs
57+
),
58+
)
5359

5460

5561
def lsp_log(message: LspMessage) -> None:

codeflash/lsp/beta.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -158,20 +158,6 @@ def initialize_function_optimization(
158158
return {"functionName": params.functionName, "status": "success"}
159159

160160

161-
@server.feature("discoverFunctionTests")
162-
def discover_function_tests(server: CodeflashLanguageServer, params: FunctionOptimizationParams) -> dict[str, str]:
163-
fto = server.optimizer.current_function_being_optimized
164-
optimizable_funcs = {fto.file_path: [fto]}
165-
166-
devnull_writer = open(os.devnull, "w") # noqa
167-
with contextlib.redirect_stdout(devnull_writer):
168-
function_to_tests, num_discovered_tests = server.optimizer.discover_tests(optimizable_funcs)
169-
170-
server.optimizer.discovered_tests = function_to_tests
171-
172-
return {"functionName": params.functionName, "status": "success", "discovered_tests": num_discovered_tests}
173-
174-
175161
@server.feature("validateProject")
176162
def validate_project(server: CodeflashLanguageServer, _params: FunctionOptimizationParams) -> dict[str, str]:
177163
from codeflash.cli_cmds.cmd_init import is_valid_pyproject_toml
@@ -301,6 +287,12 @@ def perform_function_optimization( # noqa: PLR0911
301287
}
302288

303289
module_prep_result = server.optimizer.prepare_module_for_optimization(current_function.file_path)
290+
if not module_prep_result:
291+
return {
292+
"functionName": params.functionName,
293+
"status": "error",
294+
"message": "Failed to prepare module for optimization",
295+
}
304296

305297
validated_original_code, original_module_ast = module_prep_result
306298

@@ -309,7 +301,7 @@ def perform_function_optimization( # noqa: PLR0911
309301
function_to_optimize_source_code=validated_original_code[current_function.file_path].source_code,
310302
original_module_ast=original_module_ast,
311303
original_module_path=current_function.file_path,
312-
function_to_tests=server.optimizer.discovered_tests or {},
304+
function_to_tests={},
313305
)
314306

315307
server.optimizer.current_function_optimizer = function_optimizer
@@ -328,6 +320,13 @@ def perform_function_optimization( # noqa: PLR0911
328320
function_name=current_function.function_name,
329321
)
330322

323+
optimizable_funcs = {current_function.file_path: [current_function]}
324+
325+
devnull_writer = open(os.devnull, "w") # noqa
326+
with contextlib.redirect_stdout(devnull_writer):
327+
function_to_tests, num_discovered_tests = server.optimizer.discover_tests(optimizable_funcs)
328+
function_optimizer.function_to_tests = function_to_tests
329+
331330
test_setup_result = function_optimizer.generate_and_instrument_tests(
332331
code_context, should_run_experiment=should_run_experiment
333332
)

codeflash/lsp/helpers.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,44 @@
22
import re
33
from functools import lru_cache
44

5-
_double_quote_pat = re.compile(r'"(.*?)"')
5+
from rich.tree import Tree
6+
7+
from codeflash.models.test_type import TestType
68

9+
_double_quote_pat = re.compile(r'"(.*?)"')
710
_single_quote_pat = re.compile(r"'(.*?)'")
11+
worktree_path_regex = re.compile(r'\/[^"]*worktrees\/[^"]\S*')
812

913

1014
@lru_cache(maxsize=1)
1115
def is_LSP_enabled() -> bool:
1216
return os.getenv("CODEFLASH_LSP", default="false").lower() == "true"
1317

1418

15-
worktree_path_regex = re.compile(r'\/[^"]*worktrees\/[^"]\S*')
19+
def tree_to_markdown(tree: Tree, level: int = 0) -> str:
20+
"""Convert a rich Tree into a Markdown bullet list."""
21+
indent = " " * level
22+
if level == 0:
23+
lines: list[str] = [f"{indent}### {tree.label}"]
24+
else:
25+
lines: list[str] = [f"{indent}- {tree.label}"]
26+
for child in tree.children:
27+
lines.extend(tree_to_markdown(child, level + 1).splitlines())
28+
return "\n".join(lines)
29+
30+
31+
def report_to_markdown_table(report: dict[TestType, dict[str, int]], title: str) -> str:
32+
lines = ["| Test Type | Passed ✅ | Failed ❌ |", "|-----------|--------|--------|"]
33+
for test_type in TestType:
34+
if test_type is TestType.INIT_STATE_TEST:
35+
continue
36+
passed = report[test_type]["passed"]
37+
failed = report[test_type]["failed"]
38+
# if passed == 0 and failed == 0:
39+
# continue
40+
lines.append(f"| {test_type.to_name()} | {passed} | {failed} |")
41+
table = "\n".join(lines)
42+
return f"### {title}\n{table}"
1643

1744

1845
def simplify_worktree_paths(msg: str, highlight: bool = True) -> str: # noqa: FBT001, FBT002

codeflash/lsp/lsp_logger.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,23 @@ class LspMessageTags:
1313
not_lsp: bool = False
1414
loading: bool = False
1515

16+
h1: bool = False
17+
h2: bool = False
18+
h3: bool = False
19+
h4: bool = False
20+
21+
22+
def add_heading_tags(msg: str, tags: LspMessageTags) -> str:
23+
if tags.h1:
24+
return "# " + msg
25+
if tags.h2:
26+
return "## " + msg
27+
if tags.h3:
28+
return "### " + msg
29+
if tags.h4:
30+
return "#### " + msg
31+
return msg
32+
1633

1734
def extract_tags(msg: str) -> tuple[LspMessageTags, str]:
1835
if not isinstance(msg, str):
@@ -26,12 +43,29 @@ def extract_tags(msg: str) -> tuple[LspMessageTags, str]:
2643
message_tags.not_lsp = True
2744
if "loading" in tags:
2845
message_tags.loading = True
46+
if "h1" in tags:
47+
message_tags.h1 = True
48+
if "h2" in tags:
49+
message_tags.h2 = True
50+
if "h3" in tags:
51+
message_tags.h3 = True
52+
if "h4" in tags:
53+
message_tags.h4 = True
2954
return message_tags, parts[1]
3055

3156
return LspMessageTags(), msg
3257

3358

34-
def enhanced_log(msg: str, actual_log_fn: Callable[[str, Any, Any], None], *args: Any, **kwargs: Any) -> None: # noqa: ANN401
59+
supported_lsp_log_levels = ("info", "debug")
60+
61+
62+
def enhanced_log(
63+
msg: str,
64+
actual_log_fn: Callable[[str, Any, Any], None],
65+
level: str,
66+
*args: Any, # noqa: ANN401
67+
**kwargs: Any, # noqa: ANN401
68+
) -> None:
3569
lsp_enabled = is_LSP_enabled()
3670
if not lsp_enabled or not isinstance(msg, str):
3771
actual_log_fn(msg, *args, **kwargs)
@@ -52,10 +86,11 @@ def enhanced_log(msg: str, actual_log_fn: Callable[[str, Any, Any], None], *args
5286
return
5387

5488
#### LSP mode ####
55-
if tags.not_lsp:
89+
if tags.not_lsp or level not in supported_lsp_log_levels:
5690
return
5791

5892
if is_normal_text_message:
93+
clean_msg = add_heading_tags(clean_msg, tags)
5994
clean_msg = LspTextMessage(text=clean_msg, takes_time=tags.loading).serialize()
6095

6196
actual_log_fn(clean_msg, *args, **kwargs)

codeflash/lsp/lsp_message.py

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,12 @@ def type(self) -> str:
3131
raise NotImplementedError
3232

3333
def serialize(self) -> str:
34-
if isinstance(self, LspTextMessage):
35-
self.text = simplify_worktree_paths(self.text)
36-
self.text = replace_quotes_with_backticks(self.text)
37-
if isinstance(self, LspCodeMessage):
38-
self.file_name = simplify_worktree_paths(str(self.file_name), highlight=False)
39-
if isinstance(self, LspMarkdownMessage):
40-
self.markdown = simplify_worktree_paths(self.markdown)
41-
self.markdown = replace_quotes_with_backticks(self.markdown)
42-
4334
data = self._loop_through(asdict(self))
4435
# Important: keep type as the first key, for making it easy and fast for the client to know if this is a lsp message before parsing it
4536
ordered = {"type": self.type(), **data}
46-
return json.dumps(ordered)
37+
return (
38+
json.dumps(ordered) + "\u241f"
39+
) # \u241F is the message delimiter becuase it can be more than one message sent over the same message
4740

4841

4942
@dataclass
@@ -53,6 +46,11 @@ class LspTextMessage(LspMessage):
5346
def type(self) -> str:
5447
return "text"
5548

49+
def serialize(self) -> str:
50+
self.text = simplify_worktree_paths(self.text)
51+
self.text = replace_quotes_with_backticks(self.text)
52+
return super().serialize()
53+
5654

5755
@dataclass
5856
class LspCodeMessage(LspMessage):
@@ -70,6 +68,7 @@ def serialize(self) -> str:
7068
self.lines_count = code_lines_length
7169
if code_lines_length > max_code_lines_before_collapse:
7270
self.collapsed = True
71+
self.file_name = simplify_worktree_paths(str(self.file_name), highlight=False)
7372
return super().serialize()
7473

7574

@@ -80,10 +79,7 @@ class LspMarkdownMessage(LspMessage):
8079
def type(self) -> str:
8180
return "markdown"
8281

83-
84-
@dataclass
85-
class LspStatsMessage(LspMessage):
86-
stats: dict[str, Any]
87-
88-
def type(self) -> str:
89-
return "stats"
82+
def serialize(self) -> str:
83+
self.markdown = simplify_worktree_paths(self.markdown)
84+
self.markdown = replace_quotes_with_backticks(self.markdown)
85+
return super().serialize()

codeflash/models/models.py

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55

66
from rich.tree import Tree
77

8-
from codeflash.cli_cmds.console import DEBUG_MODE
8+
from codeflash.cli_cmds.console import DEBUG_MODE, lsp_log
9+
from codeflash.lsp.helpers import is_LSP_enabled, report_to_markdown_table
10+
from codeflash.lsp.lsp_message import LspMarkdownMessage
11+
from codeflash.models.test_type import TestType
912

1013
if TYPE_CHECKING:
1114
from collections.abc import Iterator
@@ -482,27 +485,6 @@ class VerificationType(str, Enum):
482485
INIT_STATE_HELPER = "init_state_helper" # Correctness verification for helper class instance attributes after init
483486

484487

485-
class TestType(Enum):
486-
EXISTING_UNIT_TEST = 1
487-
INSPIRED_REGRESSION = 2
488-
GENERATED_REGRESSION = 3
489-
REPLAY_TEST = 4
490-
CONCOLIC_COVERAGE_TEST = 5
491-
INIT_STATE_TEST = 6
492-
493-
def to_name(self) -> str:
494-
if self is TestType.INIT_STATE_TEST:
495-
return ""
496-
names = {
497-
TestType.EXISTING_UNIT_TEST: "⚙️ Existing Unit Tests",
498-
TestType.INSPIRED_REGRESSION: "🎨 Inspired Regression Tests",
499-
TestType.GENERATED_REGRESSION: "🌀 Generated Regression Tests",
500-
TestType.REPLAY_TEST: "⏪ Replay Tests",
501-
TestType.CONCOLIC_COVERAGE_TEST: "🔎 Concolic Coverage Tests",
502-
}
503-
return names[self]
504-
505-
506488
@dataclass(frozen=True)
507489
class InvocationId:
508490
test_module_path: str # The fully qualified name of the test module
@@ -644,6 +626,13 @@ def report_to_string(report: dict[TestType, dict[str, int]]) -> str:
644626
@staticmethod
645627
def report_to_tree(report: dict[TestType, dict[str, int]], title: str) -> Tree:
646628
tree = Tree(title)
629+
630+
if is_LSP_enabled():
631+
# Build markdown table
632+
markdown = report_to_markdown_table(report, title)
633+
lsp_log(LspMarkdownMessage(markdown=markdown))
634+
return tree
635+
647636
for test_type in TestType:
648637
if test_type is TestType.INIT_STATE_TEST:
649638
continue

codeflash/models/test_type.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from enum import Enum
2+
3+
4+
class TestType(Enum):
5+
EXISTING_UNIT_TEST = 1
6+
INSPIRED_REGRESSION = 2
7+
GENERATED_REGRESSION = 3
8+
REPLAY_TEST = 4
9+
CONCOLIC_COVERAGE_TEST = 5
10+
INIT_STATE_TEST = 6
11+
12+
def to_name(self) -> str:
13+
if self is TestType.INIT_STATE_TEST:
14+
return ""
15+
names = {
16+
TestType.EXISTING_UNIT_TEST: "⚙️ Existing Unit Tests",
17+
TestType.INSPIRED_REGRESSION: "🎨 Inspired Regression Tests",
18+
TestType.GENERATED_REGRESSION: "🌀 Generated Regression Tests",
19+
TestType.REPLAY_TEST: "⏪ Replay Tests",
20+
TestType.CONCOLIC_COVERAGE_TEST: "🔎 Concolic Coverage Tests",
21+
}
22+
return names[self]

0 commit comments

Comments
 (0)