Skip to content

Commit

Permalink
Add rename method with basic logic
Browse files Browse the repository at this point in the history
Summary:
# Context:
We want to set up Rename Symbols into few steps:
1. Enable textDocument/rename
2. Create constructs for Rename request/params/response/etc
3. Add logic to wrap references into rename response (test by hooking up references from Glean)
4. Add alternative methods to finding local references
5. Merge references from both local and global.

These stack of diffs will address 1~3.

# This Diff:
Step 3
Update RemoteIndexBackedQuerier to wrap references into TextEdits/WorkspaceEdit to use for RenameResponse.
Update PyreLanguageServer to listen for `textDocument/rename` and call the querier for rename response.

Reviewed By: pradeep90

Differential Revision: D47958684

fbshipit-source-id: 07fbe167e6729cf07d66ad3bf1fc749365fd4ea9
  • Loading branch information
Isaac Hwang authored and facebook-github-bot committed Aug 8, 2023
1 parent 05a3561 commit d96a057
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 2 deletions.
12 changes: 10 additions & 2 deletions client/commands/daemon_querier.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
import json
import logging
import os
from collections import defaultdict
from pathlib import Path
from typing import Callable, List, Optional, Union
from typing import Callable, DefaultDict, List, Optional, Union

from .. import dataclasses_json_extensions as json_mixins, error

Expand Down Expand Up @@ -921,7 +922,14 @@ async def get_rename(
position: lsp.PyrePosition,
new_text: str,
) -> Union[daemon_query.DaemonQueryFailure, Optional[lsp.WorkspaceEdit]]:
return None
references = await self.index.references(path, position)
if len(references) == 0:
return None
changes: DefaultDict[str, List[lsp.TextEdit]] = defaultdict(list)
for reference in references:
changes[reference.uri].append(lsp.TextEdit(reference.range, new_text))

return lsp.WorkspaceEdit(changes=changes)

async def handle_file_opened(
self,
Expand Down
76 changes: 76 additions & 0 deletions client/commands/pyre_language_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,15 @@ async def process_call_hierarchy_outgoing_call(
) -> None:
raise NotImplementedError()

@abc.abstractmethod
async def process_rename_request(
self,
parameters: lsp.RenameParameters,
request_id: Union[int, str, None],
activity_key: Optional[Dict[str, object]] = None,
) -> None:
raise NotImplementedError()

@abc.abstractmethod
async def process_shutdown_request(self, request_id: Union[int, str, None]) -> None:
raise NotImplementedError()
Expand Down Expand Up @@ -1153,6 +1162,65 @@ async def process_call_hierarchy_outgoing_call(
activity_key,
)

async def process_rename_request(
self,
parameters: lsp.RenameParameters,
request_id: Union[int, str, None],
activity_key: Optional[Dict[str, object]] = None,
) -> None:
document_path: Optional[
Path
] = parameters.text_document.document_uri().to_file_path()
daemon_status_before = self.server_state.status_tracker.get_status()
request_timer = timer.Timer()
if document_path is None:
raise json_rpc.InvalidRequestError(
f"Document URI is not a file: {parameters.text_document.uri}"
)
rename_edits = await self.querier.get_rename(
document_path, parameters.position.to_pyre_position(), parameters.new_name
)
error_message = None
if isinstance(rename_edits, DaemonQueryFailure):
LOG.info(
daemon_failure_string(
"rename",
str(type(rename_edits)),
rename_edits.error_message,
)
)
error_message = rename_edits.error_message

raw_response = (
None
if rename_edits is None
else lsp.WorkspaceEdit.cached_schema().dump(rename_edits)
)
LOG.info(f"Rename response: {raw_response}")

await lsp.write_json_rpc(
self.output_channel,
json_rpc.SuccessResponse(
id=request_id, activity_key=activity_key, result=raw_response
),
)
await self.write_telemetry(
{
"type": "LSP",
"operation": "rename",
"filepath": str(document_path),
"non_empty": rename_edits is not None,
"response": raw_response,
"duration_ms": request_timer.stop_in_millisecond(),
"server_state_open_documents_count": len(
self.server_state.opened_documents
),
"error_message": error_message,
**daemon_status_before.as_telemetry_dict(),
},
activity_key,
)

async def process_shutdown_request(self, request_id: Union[int, str, None]) -> None:
await lsp.write_json_rpc_ignore_connection_error(
self.output_channel,
Expand Down Expand Up @@ -1338,6 +1406,14 @@ async def dispatch_nonblocking_request(self, request: json_rpc.Request) -> None:
request.id,
request.activity_key,
)
elif request.method == "textDocument/rename":
await self.api.process_rename_request(
lsp.RenameParameters.from_json_rpc_parameters(
request.extract_parameters()
),
request.id,
request.activity_key,
)
elif request.id is not None:
raise lsp.RequestCancelledError(
f"{request.method} Request not supported yet"
Expand Down
110 changes: 110 additions & 0 deletions client/commands/tests/daemon_querier_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -686,3 +686,113 @@ async def test_AbstractDaemonQueryFailer_invoked(self) -> None:
"bar10.py",
],
)


class MockGleanRemoteIndex(remote_index.AbstractRemoteIndex):
definition_response: List[lsp.LspLocation] = []
references_response: List[lsp.LspLocation] = []

async def definition(
self, path: Path, position: lsp.PyrePosition
) -> List[lsp.LspLocation]:
return self.definition_response

async def hover(
self, path: Path, position: lsp.PyrePosition
) -> lsp.LspHoverResponse:
raise NotImplementedError()

async def references(
self, path: Path, position: lsp.PyrePosition
) -> List[lsp.LspLocation]:
return self.references_response

async def prepare_call_hierarchy(
self,
path: Path,
position: lsp.PyrePosition,
relation_direction: lsp.PyreCallHierarchyRelationDirection,
) -> List[lsp.CallHierarchyItem]:
raise NotImplementedError()

async def call_hierarchy_from_item(
self,
path: Path,
item: lsp.CallHierarchyItem,
relation_direction: lsp.PyreCallHierarchyRelationDirection,
) -> List[lsp.CallHierarchyItem]:
raise NotImplementedError()


class RefactoringTest(testslide.TestCase):
@setup.async_test
async def test_rename(self) -> None:
"""
Validates the following:
1. Valid WorkspaceEdit response is returned
2. TextEdits return the locations from definitions and references
"""

# Setup test data
mocked_glean_index = MockGleanRemoteIndex()
foo_file = "file:///foo.py"
bar_file = "file:///bar.py"
mocked_glean_index.definition_response = [
lsp.LspLocation(
uri=foo_file,
range=lsp.LspRange(
start=lsp.LspPosition(line=3, character=2),
end=lsp.LspPosition(line=3, character=7),
),
)
]
mocked_glean_index.references_response = [
lsp.LspLocation(
uri=foo_file,
range=lsp.LspRange(
start=lsp.LspPosition(line=9, character=6),
end=lsp.LspPosition(line=9, character=11),
),
),
lsp.LspLocation(
uri=foo_file,
range=lsp.LspRange(
start=lsp.LspPosition(line=20, character=4),
end=lsp.LspPosition(line=20, character=9),
),
),
lsp.LspLocation(
uri=bar_file,
range=lsp.LspRange(
start=lsp.LspPosition(line=5, character=1),
end=lsp.LspPosition(line=5, character=6),
),
),
]
querier = RemoteIndexBackedQuerier(
CodeNavigationDaemonQuerier(
server_state=server_setup.create_server_state_with_options(
language_server_features=LanguageServerFeatures(),
),
),
index=mocked_glean_index,
)

workspaceEdits = await querier.get_rename(
Path("test.py"), lsp.PyrePosition(10, 0), "test"
)

self.assertIsNotNone(workspaceEdits)
self.assertFalse(isinstance(workspaceEdits, DaemonQueryFailure))

textEdits = workspaceEdits.changes
self.assertIsNotNone(textEdits)

# foo.py and bar.py
self.assertEqual(len(textEdits.keys()), 2)
foo_edits = textEdits[foo_file]
bar_edits = textEdits[bar_file]

self.assertEqual(len(foo_edits), 2)
self.assertEqual(len(bar_edits), 1)
self.assertEqual(bar_edits[0].new_text, "test")
8 changes: 8 additions & 0 deletions client/commands/tests/language_server_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,14 @@ async def process_call_hierarchy_outgoing_call(
) -> None:
raise NotImplementedError()

async def process_rename_request(
self,
parameters: lsp.RenameParameters,
request_id: Union[int, str, None],
activity_key: Optional[Dict[str, object]] = None,
) -> None:
raise NotImplementedError()

async def process_shutdown_request(self, request_id: Union[int, str, None]) -> None:
raise NotImplementedError()

Expand Down

0 comments on commit d96a057

Please sign in to comment.