Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement call hierarchy request #2151

Merged
merged 35 commits into from
Jan 22, 2023
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
09ad38c
Implement call hierarchy request
jwortmann Dec 23, 2022
865a93d
Expand root, better tooltip, decrease padding
jwortmann Dec 27, 2022
4a472b8
Add toggle button
jwortmann Dec 27, 2022
6140630
Cleanup
jwortmann Dec 27, 2022
9c8ee61
Merge branch 'main' into call-hierarchy
jwortmann Dec 27, 2022
7420ab6
Use different arrow symbols
jwortmann Dec 27, 2022
f59e38c
Fix weird lint errors
jwortmann Dec 27, 2022
f356103
And GitHub CI too
jwortmann Dec 27, 2022
8c02c57
Use Unicode characters instead of html entities for arrow symbols
jwortmann Dec 27, 2022
ec84c88
Fix erroneous capability restriction
jwortmann Dec 27, 2022
7b586fe
Adjust labels
jwortmann Dec 27, 2022
df7ef69
Add opened files to selection
jwortmann Dec 27, 2022
67bf89a
Adjust variable name
jwortmann Dec 29, 2022
3d85e4c
Derive from modifier keys whether add new tab to selection
jwortmann Dec 29, 2022
ef01161
Fix missed renamed argument name
jwortmann Dec 29, 2022
d31526f
Add call hierarchy to command palette
jwortmann Dec 29, 2022
e16767d
Add new command name to docs
jwortmann Dec 30, 2022
30a9355
Fix accidentally locking lsp_open_location to specific session
jwortmann Dec 30, 2022
96b896c
Reuse make_command_link
jwortmann Dec 30, 2022
9447f4d
Follow flags parameter whether to add existing TreeViewSheet to selec…
jwortmann Dec 30, 2022
28695ff
Formatting
jwortmann Dec 30, 2022
fbfc6cc
Use weak reference instead of session name where possible
jwortmann Dec 30, 2022
b76e369
Merge branch 'main' into call-hierarchy
jwortmann Dec 30, 2022
a4799c1
Remove obsolete function
jwortmann Dec 30, 2022
20d4343
Simpler way to silence pyright
jwortmann Jan 4, 2023
cadb53a
Simplify and better variable name
jwortmann Jan 4, 2023
b93fd28
Move LspOpenLocationCommand to core/registry.py
jwortmann Jan 4, 2023
7739774
Tweak path formatting in tooltip
jwortmann Jan 4, 2023
7f57405
Missed to remove an obsolete import
jwortmann Jan 4, 2023
0e4298d
Always open links in side-by-side mode
jwortmann Jan 22, 2023
1f3048f
Tweak function name
jwortmann Jan 22, 2023
12ce27a
workaround bug with view listeners not being initialized
rchl Jan 22, 2023
d1950b6
more specific workaround
rchl Jan 22, 2023
fad24ed
Merge branch 'main' into jwortmann-call-hierarchy
rchl Jan 22, 2023
45cad3a
add progress
rchl Jan 22, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) \
jwortmann marked this conversation as resolved.
Show resolved Hide resolved
.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`
168 changes: 168 additions & 0 deletions plugin/call_hierarchy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
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
})
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()
rchl marked this conversation as resolved.
Show resolved Hide resolved
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"))
rchl marked this conversation as resolved.
Show resolved Hide resolved
new_tree_view_sheet(self._window, "Call Hierarchy", data_provider, header, flags=sublime.ADD_TO_SELECTION)
rchl marked this conversation as resolved.
Show resolved Hide resolved


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, flags=sublime.ADD_TO_SELECTION)
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: Mapping[str, Any], view: sublime.View)
def semanticTokensRange(cls, params: Mapping[str, Any], 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)

@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)
rchl marked this conversation as resolved.
Show resolved Hide resolved

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