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

Add support for folding range request #2304

Merged
merged 16 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
6 changes: 6 additions & 0 deletions Default.sublime-keymap
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,12 @@
// "command": "lsp_expand_selection",
// "context": [{"key": "lsp.session_with_capability", "operand": "selectionRangeProvider"}]
// },
// Fold around caret position
// {
jwortmann marked this conversation as resolved.
Show resolved Hide resolved
// "keys": ["UNBOUND"],
// "command": "lsp_fold",
// "context": [{"key": "lsp.session_with_capability", "operand": "foldingRangeProvider"}]
// },
//==== Internal key-bindings ====
{
"keys": ["<character>"],
Expand Down
13 changes: 13 additions & 0 deletions Main.sublime-menu
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,23 @@
{
"id": "edit",
"children": [
{
"id": "fold",
"children": [
{
"command": "lsp_fold",
"args": {"manual": false}
}
]
},
{
"id": "lsp",
"caption": "-"
},
{
"command": "lsp_fold",
"args": {"manual": false, "hidden": true}
},
{
"command": "lsp_source_action",
"args": {"id": -1}
Expand Down
1 change: 1 addition & 0 deletions boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from .plugin.documents import TextChangeListener
from .plugin.edit import LspApplyDocumentEditCommand
from .plugin.execute_command import LspExecuteCommand
from .plugin.folding_range import LspFoldCommand
from .plugin.formatting import LspFormatCommand
from .plugin.formatting import LspFormatDocumentCommand
from .plugin.formatting import LspFormatDocumentRangeCommand
Expand Down
1 change: 1 addition & 0 deletions docs/src/keyboard_shortcuts.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Refer to the [Customization section](customization.md#keyboard-shortcuts-key-bin
| Auto Complete | <kbd>ctrl</kbd> <kbd>space</kbd> (also on macOS) | `auto_complete`
| Expand Selection | unbound | `lsp_expand_selection`
| Find References | <kbd>shift</kbd> <kbd>f12</kbd> | `lsp_symbol_references` (supports optional args: `{"include_declaration": true/false}`)
| Fold | unbound | `lsp_fold`
| Follow Link | unbound | `lsp_open_link`
| Format File | unbound | `lsp_format_document`
| Format Selection | unbound | `lsp_format_document_range`
Expand Down
4 changes: 4 additions & 0 deletions plugin/core/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -5967,6 +5967,10 @@ def prepareRename(cls, params: PrepareRenameParams, view: sublime.View, progress
def selectionRange(cls, params: SelectionRangeParams) -> 'Request':
return Request('textDocument/selectionRange', params)

@classmethod
def foldingRange(cls, params: FoldingRangeParams, view: sublime.View) -> 'Request':
return Request('textDocument/foldingRange', params, view)

@classmethod
def workspaceSymbol(cls, params: WorkspaceSymbolParams) -> 'Request':
return Request("workspace/symbol", params, None, progress=True)
Expand Down
11 changes: 11 additions & 0 deletions plugin/core/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from .protocol import ExecuteCommandParams
from .protocol import FailureHandlingKind
from .protocol import FileEvent
from .protocol import FoldingRangeKind
from .protocol import GeneralClientCapabilities
from .protocol import InitializeError
from .protocol import InitializeParams
Expand Down Expand Up @@ -399,6 +400,16 @@ def get_initialize_params(variables: Dict[str, str], workspace_folders: List[Wor
"selectionRange": {
"dynamicRegistration": True
},
"foldingRange": {
"dynamicRegistration": True,
"foldingRangeKind": {
"valueSet": [
FoldingRangeKind.Comment,
FoldingRangeKind.Imports,
FoldingRangeKind.Region
]
}
},
"codeLens": {
"dynamicRegistration": True
},
Expand Down
138 changes: 138 additions & 0 deletions plugin/folding_range.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
from .core.protocol import FoldingRange
from .core.protocol import FoldingRangeKind
from .core.protocol import FoldingRangeParams
from .core.protocol import Range
from .core.protocol import Request
from .core.protocol import UINT_MAX
from .core.registry import LspTextCommand
from .core.typing import List, Optional
from .core.views import range_to_region
from .core.views import text_document_identifier
from functools import partial
import sublime


def folding_range_to_range(folding_range: FoldingRange) -> Range:
return {
'start': {
'line': folding_range['startLine'],
'character': folding_range.get('startCharacter', UINT_MAX)
},
'end': {
'line': folding_range['endLine'],
'character': folding_range.get('endCharacter', UINT_MAX)
}
}


class LspFoldCommand(LspTextCommand):

capability = 'foldingRangeProvider'
folding_ranges = [] # type: List[FoldingRange]
change_count = -1
folding_region = None # type: Optional[sublime.Region]

def is_visible(
self, manual: bool = True, hidden: bool = False, event: Optional[dict] = None, point: Optional[int] = None
) -> bool:
if manual:
return True
# There should be a single empty selection in the view, otherwise this functionality would be misleading
selection = self.view.sel()
if len(selection) != 1 or not selection[0].empty():
return False
if hidden: # This is our dummy menu item, with the purpose to run the request when the "Edit" menu gets opened
view_change_count = self.view.change_count()
# If the stored change_count matches the view's actual change count, the request has already been run for
# this document state (i.e. "Edit" menu was opened before) and the results are still valid - no need to send
# another request.
if self.change_count == view_change_count:
return False
self.change_count = -1
session = self.best_session(self.capability)
if session:
params = {'textDocument': text_document_identifier(self.view)} # type: FoldingRangeParams
session.send_request_async(
Request.foldingRange(params, self.view),
partial(self._handle_response_async, view_change_count)
)
return False
return self.folding_region is not None # Already set or unset by self.description

def _handle_response_async(self, change_count: int, response: Optional[List[FoldingRange]]) -> None:
self.change_count = change_count
self.folding_ranges = response or []

def description(
self, manual: bool = True, hidden: bool = False, event: Optional[dict] = None, point: Optional[int] = None
) -> str:
if manual:
return "LSP: Fold"
# Implementation detail of Sublime Text: TextCommand.description is called *before* TextCommand.is_visible
self.folding_region = None
if self.change_count != self.view.change_count(): # Ensure that the response has already arrived
return "LSP <debug>" # is_visible will return False
if point is not None:
pt = point
else:
selection = self.view.sel()
if len(selection) != 1 or not selection[0].empty():
return "LSP <debug>" # is_visible will return False
pt = selection[0].b
for folding_range in sorted(self.folding_ranges, key=lambda r: r['startLine'], reverse=True):
region = range_to_region(folding_range_to_range(folding_range), self.view)
if region.contains(pt):
# Store the relevant folding region, so that we don't need to do the same computation again in
# self.is_visible and self.run
self.folding_region = region
return {
FoldingRangeKind.Comment: "LSP: Fold this comment",
FoldingRangeKind.Imports: "LSP: Fold imports",
FoldingRangeKind.Region: "LSP: Fold this region",
'array': "LSP: Fold this array", # used by LSP-json
'object': "LSP: Fold this object", # used by LSP-json
}.get(folding_range.get('kind', ''), "LSP: Fold")
return "LSP <debug>" # is_visible will return False

def run(
self,
edit: sublime.Edit,
manual: bool = True,
hidden: bool = False,
event: Optional[dict] = None,
point: Optional[int] = None
) -> None:
if manual:
if point is not None:
pt = point
else:
selection = self.view.sel()
if len(selection) != 1 or not selection[0].empty():
self.view.run_command('fold_unfold')
return
pt = selection[0].b
session = self.best_session(self.capability)
if session:
params = {'textDocument': text_document_identifier(self.view)} # type: FoldingRangeParams
session.send_request_async(
Request.foldingRange(params, self.view),
partial(self._handle_response_manual_async, pt)
)
elif self.folding_region is not None:
self.view.fold(self.folding_region)
rchl marked this conversation as resolved.
Show resolved Hide resolved

def _handle_response_manual_async(self, point: int, response: Optional[List[FoldingRange]]) -> None:
if not response:
window = self.view.window()
if window:
window.status_message("Code Folding not available")
return
for folding_range in sorted(response, key=lambda r: r['startLine'], reverse=True):
region = range_to_region(folding_range_to_range(folding_range), self.view)
if region.contains(point):
self.view.fold(region)
return
else:
window = self.view.window()
if window:
window.status_message("Code Folding not available")