Skip to content

Commit

Permalink
Implement call hierarchy request (#2151)
Browse files Browse the repository at this point in the history
  • Loading branch information
jwortmann authored Jan 22, 2023
1 parent ff57279 commit ba5aba3
Show file tree
Hide file tree
Showing 18 changed files with 652 additions and 69 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
python-version: '3.8'
- run: sudo apt update
- run: sudo apt install --no-install-recommends -y x11-xserver-utils
- run: pip3 install mypy==0.971 flake8==5.0.4 pyright==1.1.271 yapf==0.31.0 --user
- run: pip3 install mypy==0.971 flake8==5.0.4 pyright==1.1.285 yapf==0.31.0 --user
- run: echo "$HOME/.local/bin" >> $GITHUB_PATH
# - run: mypy -p plugin
- run: flake8 plugin tests
Expand Down
4 changes: 4 additions & 0 deletions Context.sublime-menu
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
"command": "lsp_symbol_implementation",
"caption": "Goto Implementation…"
},
{
"command": "lsp_call_hierarchy",
"caption": "Show Call Hierarchy"
},
{
"command": "lsp_symbol_rename",
"caption": "Rename…"
Expand Down
4 changes: 4 additions & 0 deletions Default.sublime-commands
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,8 @@
"caption": "LSP: Run Code Lens",
"command": "lsp_code_lens"
},
{
"caption": "LSP: Show Call Hierarchy",
"command": "lsp_call_hierarchy"
},
]
6 changes: 6 additions & 0 deletions Default.sublime-keymap
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,12 @@
// "command": "lsp_open_link",
// "context": [{"key": "lsp.link_available"}]
// },
// Show Call Hierarchy
// {
// "keys": ["UNBOUND"],
// "command": "lsp_call_hierarchy",
// "context": [{"key": "lsp.session_with_capability", "operand": "callHierarchyProvider"}]
// },
// Expand Selection (a replacement for ST's "Expand Selection")
// {
// "keys": ["primary+shift+a"],
Expand Down
40 changes: 6 additions & 34 deletions boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import sublime_plugin

# Please keep this list sorted (Edit -> Sort Lines)
from .plugin.call_hierarchy import LspCallHierarchyCommand
from .plugin.call_hierarchy import LspCallHierarchyToggleCommand
from .plugin.code_actions import LspCodeActionsCommand
from .plugin.code_actions import LspRefactorCommand
from .plugin.code_actions import LspSourceActionCommand
Expand All @@ -20,9 +22,10 @@
from .plugin.core.open import opening_files
from .plugin.core.panels import PanelName
from .plugin.core.protocol import Error
from .plugin.core.protocol import Location
from .plugin.core.protocol import LocationLink
from .plugin.core.registry import LspCollapseTreeItemCommand
from .plugin.core.registry import LspExpandTreeItemCommand
from .plugin.core.registry import LspNextDiagnosticCommand
from .plugin.core.registry import LspOpenLocationCommand
from .plugin.core.registry import LspPrevDiagnosticCommand
from .plugin.core.registry import LspRecheckSessionsCommand
from .plugin.core.registry import LspRestartServerCommand
Expand All @@ -35,8 +38,7 @@
from .plugin.core.signature_help import LspSignatureHelpNavigateCommand
from .plugin.core.signature_help import LspSignatureHelpShowCommand
from .plugin.core.transports import kill_all_subprocesses
from .plugin.core.typing import Any, Optional, List, Type, Dict, Union
from .plugin.core.views import get_uri_and_position_from_location
from .plugin.core.typing import Any, Optional, List, Type, Dict
from .plugin.core.views import LspRunTextCommandHelperCommand
from .plugin.document_link import LspOpenLinkCommand
from .plugin.documents import DocumentSyncListener
Expand Down Expand Up @@ -188,33 +190,3 @@ def on_post_window_command(self, window: sublime.Window, command_name: str, args
sublime.set_timeout_async(wm.update_diagnostics_panel_async)
elif panel_manager.is_panel_open(PanelName.Log):
sublime.set_timeout(panel_manager.update_log_panel)


class LspOpenLocationCommand(sublime_plugin.TextCommand):
"""
A command to be used by third-party ST packages that need to open an URI with some abstract scheme.
"""

def run(
self,
_: sublime.Edit,
location: Union[Location, LocationLink],
session_name: Optional[str] = None,
flags: int = 0,
group: int = -1
) -> None:
sublime.set_timeout_async(lambda: self._run_async(location, session_name, flags, group))

def _run_async(
self, location: Union[Location, LocationLink], session_name: Optional[str], flags: int = 0, group: int = -1
) -> None:
manager = windows.lookup(self.view.window())
if manager:
manager.open_location_async(location, session_name, self.view, flags, group) \
.then(lambda view: self._handle_continuation(location, view is not None))

def _handle_continuation(self, location: Union[Location, LocationLink], success: bool) -> None:
if not success:
uri, _ = get_uri_and_position_from_location(location)
message = "Failed to open {}".format(uri)
sublime.status_message(message)
1 change: 1 addition & 0 deletions docs/src/keyboard_shortcuts.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Refer to the [Customization section](customization.md#keyboard-shortcuts-key-bin
| Run Code Lens | unbound | `lsp_code_lens`
| Run Refactor Action | unbound | `lsp_code_actions` (with args: `{"only_kinds": ["refactor"]}`)
| Run Source Action | unbound | `lsp_code_actions` (with args: `{"only_kinds": ["source"]}`)
| Show Call Hierarchy | unbound | `lsp_call_hierarchy`
| Signature Help | <kbd>ctrl</kbd> <kbd>alt</kbd> <kbd>space</kbd> | `lsp_signature_help_show`
| Toggle Diagnostics Panel | <kbd>ctrl</kbd> <kbd>alt</kbd> <kbd>m</kbd> | `lsp_show_diagnostics_panel`
| Toggle Log Panel | unbound | `lsp_toggle_server_panel`
185 changes: 185 additions & 0 deletions plugin/call_hierarchy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
from .core.promise import Promise
from .core.protocol import CallHierarchyIncomingCall
from .core.protocol import CallHierarchyIncomingCallsParams
from .core.protocol import CallHierarchyItem
from .core.protocol import CallHierarchyOutgoingCall
from .core.protocol import CallHierarchyOutgoingCallsParams
from .core.protocol import CallHierarchyPrepareParams
from .core.protocol import DocumentUri
from .core.protocol import Request
from .core.registry import new_tree_view_sheet
from .core.registry import get_position
from .core.registry import LspTextCommand
from .core.registry import LspWindowCommand
from .core.sessions import Session
from .core.tree_view import TreeDataProvider
from .core.tree_view import TreeItem
from .core.typing import cast
from .core.typing import IntEnum, List, Optional
from .core.views import make_command_link
from .core.views import parse_uri
from .core.views import SYMBOL_KINDS
from .core.views import text_document_position_params
from .goto_diagnostic import simple_project_path
from functools import partial
from pathlib import Path
import sublime
import weakref


class CallHierarchyDirection(IntEnum):
IncomingCalls = 1
OutgoingCalls = 2


class CallHierarchyDataProvider(TreeDataProvider):

def __init__(
self,
weaksession: 'weakref.ref[Session]',
direction: CallHierarchyDirection,
root_elements: List[CallHierarchyItem]
) -> None:
self.weaksession = weaksession
self.direction = direction
self.root_elements = root_elements
session = self.weaksession()
self.session_name = session.config.name if session else None

def get_children(self, element: Optional[CallHierarchyItem]) -> Promise[List[CallHierarchyItem]]:
if element is None:
return Promise.resolve(self.root_elements)
session = self.weaksession()
if not session:
return Promise.resolve([])
if self.direction == CallHierarchyDirection.IncomingCalls:
params = cast(CallHierarchyIncomingCallsParams, {'item': element})
return session.send_request_task(Request.incomingCalls(params)).then(self._handle_incoming_calls_async)
elif self.direction == CallHierarchyDirection.OutgoingCalls:
params = cast(CallHierarchyOutgoingCallsParams, {'item': element})
return session.send_request_task(Request.outgoingCalls(params)).then(self._handle_outgoing_calls_async)
return Promise.resolve([])

def get_tree_item(self, element: CallHierarchyItem) -> TreeItem:
command_url = sublime.command_url('lsp_open_location', {
'location': {
'targetUri': element['uri'],
'targetRange': element['range'],
'targetSelectionRange': element['selectionRange']
},
'session_name': self.session_name,
'flags': sublime.ADD_TO_SELECTION | sublime.SEMI_TRANSIENT | sublime.CLEAR_TO_RIGHT
})
return TreeItem(
element['name'],
kind=SYMBOL_KINDS.get(element['kind'], sublime.KIND_AMBIGUOUS),
description=element.get('detail', ""),
tooltip="{}:{}".format(self._simple_path(element['uri']), element['selectionRange']['start']['line'] + 1),
command_url=command_url
)

def _simple_path(self, uri: DocumentUri) -> str:
scheme, path = parse_uri(uri)
session = self.weaksession()
if not session or scheme != 'file':
return path
simple_path = simple_project_path([Path(folder.path) for folder in session.get_workspace_folders()], Path(path))
return str(simple_path) if simple_path else path

def _handle_incoming_calls_async(
self, response: Optional[List[CallHierarchyIncomingCall]]
) -> List[CallHierarchyItem]:
return [incoming_call['from'] for incoming_call in response] if response else []

def _handle_outgoing_calls_async(
self, response: Optional[List[CallHierarchyOutgoingCall]]
) -> List[CallHierarchyItem]:
return [outgoing_call['to'] for outgoing_call in response] if response else []


class LspCallHierarchyCommand(LspTextCommand):

capability = 'callHierarchyProvider'

def is_visible(self, event: Optional[dict] = None, point: Optional[int] = None) -> bool:
if self.applies_to_context_menu(event):
return self.is_enabled(event, point)
return True

def run(self, edit: sublime.Edit, event: Optional[dict] = None, point: Optional[int] = None) -> None:
self._window = self.view.window()
session = self.best_session(self.capability)
if not session:
return
position = get_position(self.view, event, point)
if position is None:
return
params = cast(CallHierarchyPrepareParams, text_document_position_params(self.view, position))
request = Request.prepareCallHierarchy(params, self.view)
session.send_request_async(request, partial(self._handle_response_async, weakref.ref(session)))

def _handle_response_async(
self, weaksession: 'weakref.ref[Session]', response: Optional[List[CallHierarchyItem]]
) -> None:
if not self._window or not self._window.is_valid():
return
if not response:
self._window.status_message("Call hierarchy not available")
return
session = weaksession()
if not session:
return
data_provider = CallHierarchyDataProvider(weaksession, CallHierarchyDirection.IncomingCalls, response)
header = 'Call Hierarchy: Callers of… {}'.format(
make_command_link('lsp_call_hierarchy_toggle', "⇄", {
'session_name': session.config.name,
'direction': CallHierarchyDirection.OutgoingCalls,
'root_elements': response
}, tooltip="Show outgoing calls"))
new_tree_view_sheet(self._window, "Call Hierarchy", data_provider, header)
data_provider.get_children(None).then(partial(open_first, self._window, session.config.name))


class LspCallHierarchyToggleCommand(LspWindowCommand):

capability = 'callHierarchyProvider'

def run(
self, session_name: str, direction: CallHierarchyDirection, root_elements: List[CallHierarchyItem]
) -> None:
session = self.session_by_name(session_name)
if not session:
return
if direction == CallHierarchyDirection.IncomingCalls:
current_label = 'Callers of…'
new_direction = CallHierarchyDirection.OutgoingCalls
tooltip = 'Show Outgoing Calls'
elif direction == CallHierarchyDirection.OutgoingCalls:
current_label = 'Calls from…'
new_direction = CallHierarchyDirection.IncomingCalls
tooltip = 'Show Incoming Calls'
else:
return
header = 'Call Hierarchy: {} {}'.format(
current_label, make_command_link('lsp_call_hierarchy_toggle', "⇄", {
'session_name': session_name,
'direction': new_direction,
'root_elements': root_elements
}, tooltip=tooltip))
data_provider = CallHierarchyDataProvider(weakref.ref(session), direction, root_elements)
new_tree_view_sheet(self.window, "Call Hierarchy", data_provider, header)
data_provider.get_children(None).then(partial(open_first, self.window, session.config.name))


def open_first(window: sublime.Window, session_name: str, items: List[CallHierarchyItem]) -> None:
if items and window.is_valid():
item = items[0]
window.run_command('lsp_open_location', {
'location': {
'targetUri': item['uri'],
'targetRange': item['range'],
'targetSelectionRange': item['selectionRange']
},
'session_name': session_name,
'flags': sublime.ADD_TO_SELECTION | sublime.SEMI_TRANSIENT | sublime.CLEAR_TO_RIGHT
})
12 changes: 12 additions & 0 deletions plugin/core/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -5912,6 +5912,18 @@ def semanticTokensFullDelta(cls, params: SemanticTokensDeltaParams, view: sublim
def semanticTokensRange(cls, params: SemanticTokensRangeParams, view: sublime.View) -> 'Request':
return Request("textDocument/semanticTokens/range", params, view)

@classmethod
def prepareCallHierarchy(cls, params: CallHierarchyPrepareParams, view: sublime.View) -> 'Request':
return Request("textDocument/prepareCallHierarchy", params, view, progress=True)

@classmethod
def incomingCalls(cls, params: CallHierarchyIncomingCallsParams) -> 'Request':
return Request("callHierarchy/incomingCalls", params, None)

@classmethod
def outgoingCalls(cls, params: CallHierarchyOutgoingCallsParams) -> 'Request':
return Request("callHierarchy/outgoingCalls", params, None)

@classmethod
def resolveCompletionItem(cls, params: CompletionItem, view: sublime.View) -> 'Request':
return Request("completionItem/resolve", params, view)
Expand Down
Loading

0 comments on commit ba5aba3

Please sign in to comment.