diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 4bea8f1..9ece873 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -3,9 +3,8 @@ import logging import os from abc import ABC, abstractmethod from collections.abc import Iterator, Sequence -from dataclasses import asdict, dataclass, field -from difflib import SequenceMatcher -from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Self, Union +from dataclasses import asdict, dataclass +from typing import TYPE_CHECKING, Any, Self, Union from sensai.util.string import ToStringMixin @@ -21,151 +20,6 @@ if TYPE_CHECKING: log = logging.getLogger(__name__) -class LineChange(NamedTuple): - """Represents a change to a specific line or range of lines.""" - - operation: Literal["insert", "delete", "replace"] - original_start: int - original_end: int - modified_start: int - modified_end: int - original_lines: list[str] - modified_lines: list[str] - - -@dataclass -class CodeDiff: - """ - Represents the difference between original and modified code. - Provides object-oriented access to diff information including line numbers. - """ - - relative_path: str - original_content: str - modified_content: str - _line_changes: list[LineChange] = field(init=False) - - def __post_init__(self) -> None: - """Compute the diff using difflib's SequenceMatcher.""" - original_lines = self.original_content.splitlines(keepends=True) - modified_lines = self.modified_content.splitlines(keepends=True) - - matcher = SequenceMatcher(None, original_lines, modified_lines) - self._line_changes = [] - - for tag, orig_start, orig_end, mod_start, mod_end in matcher.get_opcodes(): - if tag == "equal": - continue - if tag == "insert": - self._line_changes.append( - LineChange( - operation="insert", - original_start=orig_start, - original_end=orig_start, - modified_start=mod_start, - modified_end=mod_end, - original_lines=[], - modified_lines=modified_lines[mod_start:mod_end], - ) - ) - elif tag == "delete": - self._line_changes.append( - LineChange( - operation="delete", - original_start=orig_start, - original_end=orig_end, - modified_start=mod_start, - modified_end=mod_start, - original_lines=original_lines[orig_start:orig_end], - modified_lines=[], - ) - ) - elif tag == "replace": - self._line_changes.append( - LineChange( - operation="replace", - original_start=orig_start, - original_end=orig_end, - modified_start=mod_start, - modified_end=mod_end, - original_lines=original_lines[orig_start:orig_end], - modified_lines=modified_lines[mod_start:mod_end], - ) - ) - - @property - def line_changes(self) -> list[LineChange]: - """Get all line changes in the diff.""" - return self._line_changes - - @property - def has_changes(self) -> bool: - """Check if there are any changes.""" - return len(self._line_changes) > 0 - - @property - def added_lines(self) -> list[tuple[int, str]]: - """Get all added lines with their line numbers (0-based) in the modified file.""" - result = [] - for change in self._line_changes: - if change.operation in ("insert", "replace"): - for i, line in enumerate(change.modified_lines): - result.append((change.modified_start + i, line)) - return result - - @property - def deleted_lines(self) -> list[tuple[int, str]]: - """Get all deleted lines with their line numbers (0-based) in the original file.""" - result = [] - for change in self._line_changes: - if change.operation in ("delete", "replace"): - for i, line in enumerate(change.original_lines): - result.append((change.original_start + i, line)) - return result - - @property - def modified_line_numbers(self) -> list[int]: - """Get all line numbers (0-based) that were modified in the modified file.""" - line_nums: set[int] = set() - for change in self._line_changes: - if change.operation in ("insert", "replace"): - line_nums.update(range(change.modified_start, change.modified_end)) - return sorted(line_nums) - - @property - def affected_original_line_numbers(self) -> list[int]: - """Get all line numbers (0-based) that were affected in the original file.""" - line_nums: set[int] = set() - for change in self._line_changes: - if change.operation in ("delete", "replace"): - line_nums.update(range(change.original_start, change.original_end)) - return sorted(line_nums) - - def get_unified_diff(self, context_lines: int = 3) -> str: - """Get the unified diff as a string.""" - import difflib - - original_lines = self.original_content.splitlines(keepends=True) - modified_lines = self.modified_content.splitlines(keepends=True) - - diff = difflib.unified_diff( - original_lines, modified_lines, fromfile=f"a/{self.relative_path}", tofile=f"b/{self.relative_path}", n=context_lines - ) - return "".join(diff) - - def get_context_diff(self, context_lines: int = 3) -> str: - """Get the context diff as a string.""" - import difflib - - original_lines = self.original_content.splitlines(keepends=True) - modified_lines = self.modified_content.splitlines(keepends=True) - - diff = difflib.context_diff( - original_lines, modified_lines, fromfile=f"a/{self.relative_path}", tofile=f"b/{self.relative_path}", n=context_lines - ) - return "".join(diff) - - @dataclass class LanguageServerSymbolLocation: """ diff --git a/test/serena/test_symbol_editing.py b/test/serena/test_symbol_editing.py index ed9cf75..5fa4401 100644 --- a/test/serena/test_symbol_editing.py +++ b/test/serena/test_symbol_editing.py @@ -6,13 +6,14 @@ import time from abc import abstractmethod from collections.abc import Iterator from contextlib import contextmanager +from dataclasses import dataclass, field +from difflib import SequenceMatcher from pathlib import Path -from typing import Literal +from typing import Literal, NamedTuple import pytest from serena.code_editor import CodeEditor, LanguageServerCodeEditor -from serena.symbol import CodeDiff from solidlsp.ls_config import Language from src.serena.symbol import LanguageServerSymbolRetriever from test.conftest import create_ls, get_repo_path @@ -22,6 +23,151 @@ pytestmark = pytest.mark.snapshot log = logging.getLogger(__name__) +class LineChange(NamedTuple): + """Represents a change to a specific line or range of lines.""" + + operation: Literal["insert", "delete", "replace"] + original_start: int + original_end: int + modified_start: int + modified_end: int + original_lines: list[str] + modified_lines: list[str] + + +@dataclass +class CodeDiff: + """ + Represents the difference between original and modified code. + Provides object-oriented access to diff information including line numbers. + """ + + relative_path: str + original_content: str + modified_content: str + _line_changes: list[LineChange] = field(init=False) + + def __post_init__(self) -> None: + """Compute the diff using difflib's SequenceMatcher.""" + original_lines = self.original_content.splitlines(keepends=True) + modified_lines = self.modified_content.splitlines(keepends=True) + + matcher = SequenceMatcher(None, original_lines, modified_lines) + self._line_changes = [] + + for tag, orig_start, orig_end, mod_start, mod_end in matcher.get_opcodes(): + if tag == "equal": + continue + if tag == "insert": + self._line_changes.append( + LineChange( + operation="insert", + original_start=orig_start, + original_end=orig_start, + modified_start=mod_start, + modified_end=mod_end, + original_lines=[], + modified_lines=modified_lines[mod_start:mod_end], + ) + ) + elif tag == "delete": + self._line_changes.append( + LineChange( + operation="delete", + original_start=orig_start, + original_end=orig_end, + modified_start=mod_start, + modified_end=mod_start, + original_lines=original_lines[orig_start:orig_end], + modified_lines=[], + ) + ) + elif tag == "replace": + self._line_changes.append( + LineChange( + operation="replace", + original_start=orig_start, + original_end=orig_end, + modified_start=mod_start, + modified_end=mod_end, + original_lines=original_lines[orig_start:orig_end], + modified_lines=modified_lines[mod_start:mod_end], + ) + ) + + @property + def line_changes(self) -> list[LineChange]: + """Get all line changes in the diff.""" + return self._line_changes + + @property + def has_changes(self) -> bool: + """Check if there are any changes.""" + return len(self._line_changes) > 0 + + @property + def added_lines(self) -> list[tuple[int, str]]: + """Get all added lines with their line numbers (0-based) in the modified file.""" + result = [] + for change in self._line_changes: + if change.operation in ("insert", "replace"): + for i, line in enumerate(change.modified_lines): + result.append((change.modified_start + i, line)) + return result + + @property + def deleted_lines(self) -> list[tuple[int, str]]: + """Get all deleted lines with their line numbers (0-based) in the original file.""" + result = [] + for change in self._line_changes: + if change.operation in ("delete", "replace"): + for i, line in enumerate(change.original_lines): + result.append((change.original_start + i, line)) + return result + + @property + def modified_line_numbers(self) -> list[int]: + """Get all line numbers (0-based) that were modified in the modified file.""" + line_nums: set[int] = set() + for change in self._line_changes: + if change.operation in ("insert", "replace"): + line_nums.update(range(change.modified_start, change.modified_end)) + return sorted(line_nums) + + @property + def affected_original_line_numbers(self) -> list[int]: + """Get all line numbers (0-based) that were affected in the original file.""" + line_nums: set[int] = set() + for change in self._line_changes: + if change.operation in ("delete", "replace"): + line_nums.update(range(change.original_start, change.original_end)) + return sorted(line_nums) + + def get_unified_diff(self, context_lines: int = 3) -> str: + """Get the unified diff as a string.""" + import difflib + + original_lines = self.original_content.splitlines(keepends=True) + modified_lines = self.modified_content.splitlines(keepends=True) + + diff = difflib.unified_diff( + original_lines, modified_lines, fromfile=f"a/{self.relative_path}", tofile=f"b/{self.relative_path}", n=context_lines + ) + return "".join(diff) + + def get_context_diff(self, context_lines: int = 3) -> str: + """Get the context diff as a string.""" + import difflib + + original_lines = self.original_content.splitlines(keepends=True) + modified_lines = self.modified_content.splitlines(keepends=True) + + diff = difflib.context_diff( + original_lines, modified_lines, fromfile=f"a/{self.relative_path}", tofile=f"b/{self.relative_path}", n=context_lines + ) + return "".join(diff) + + class EditingTest: def __init__(self, language: Language, rel_path: str): """