|
10 | 10 | from typing import Any, List, Optional, Union
|
11 | 11 |
|
12 | 12 | import cattrs
|
13 |
| -from jedi import Project, __version__ |
| 13 | +from jedi import Project, Script, __version__ |
| 14 | +from jedi.api.classes import Name |
14 | 15 | from jedi.api.refactoring import RefactoringError
|
15 | 16 | from lsprotocol.types import (
|
16 | 17 | COMPLETION_ITEM_RESOLVE,
|
|
32 | 33 | TEXT_DOCUMENT_HOVER,
|
33 | 34 | TEXT_DOCUMENT_REFERENCES,
|
34 | 35 | TEXT_DOCUMENT_RENAME,
|
| 36 | + TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL, |
| 37 | + TEXT_DOCUMENT_SEMANTIC_TOKENS_RANGE, |
35 | 38 | TEXT_DOCUMENT_SIGNATURE_HELP,
|
36 | 39 | TEXT_DOCUMENT_TYPE_DEFINITION,
|
37 | 40 | WORKSPACE_DID_CHANGE_CONFIGURATION,
|
|
67 | 70 | NotebookDocumentSyncOptionsNotebookSelectorType2,
|
68 | 71 | NotebookDocumentSyncOptionsNotebookSelectorType2CellsType,
|
69 | 72 | ParameterInformation,
|
| 73 | + Position, |
| 74 | + Range, |
70 | 75 | RenameParams,
|
| 76 | + SemanticTokens, |
| 77 | + SemanticTokensLegend, |
| 78 | + SemanticTokensParams, |
| 79 | + SemanticTokensRangeParams, |
| 80 | + SemanticTokenTypes, |
71 | 81 | SignatureHelp,
|
72 | 82 | SignatureHelpOptions,
|
73 | 83 | SignatureInformation,
|
|
76 | 86 | WorkspaceEdit,
|
77 | 87 | WorkspaceSymbolParams,
|
78 | 88 | )
|
| 89 | +from lsprotocol.validators import INTEGER_MAX_VALUE |
79 | 90 | from pygls.capabilities import get_capability
|
80 | 91 | from pygls.protocol import LanguageServerProtocol, lsp_method
|
81 | 92 | from pygls.server import LanguageServer
|
82 | 93 |
|
83 | 94 | from . import jedi_utils, notebook_utils, pygls_utils, text_edit_utils
|
| 95 | +from .constants import TYPE_TO_TOKEN_ID |
84 | 96 | from .initialization_options import (
|
85 | 97 | InitializationOptions,
|
86 | 98 | initialization_options_converter,
|
@@ -724,6 +736,148 @@ def did_change_configuration(
|
724 | 736 | """
|
725 | 737 |
|
726 | 738 |
|
| 739 | +def _raw_semantic_token( |
| 740 | + server: JediLanguageServer, n: Name |
| 741 | +) -> Union[list[int], None]: |
| 742 | + """Find an appropriate semantic token for the name. |
| 743 | +
|
| 744 | + This works by looking up the definition (using jedi ``goto``) of the name and |
| 745 | + matching the definition's type to one of the availabile semantic tokens. Further |
| 746 | + improvements are possible by inspecting context, e.g. semantic token modifiers such |
| 747 | + as ``abstract`` or ``async`` or even different tokens, e.g. ``property`` or |
| 748 | + ``method``. Dunder methods may warrant special treatment/modifiers as well. |
| 749 | + The return is a "raw" semantic token rather than a "diff." This is in the form of a |
| 750 | + length 5 array of integers where the elements are the line number, starting |
| 751 | + character, length, token index, and modifiers (as an integer whose binary |
| 752 | + representation has bits set at the indices of all applicable modifiers). |
| 753 | + """ |
| 754 | + definitions: list[Name] = n.goto( |
| 755 | + follow_imports=True, |
| 756 | + follow_builtin_imports=True, |
| 757 | + only_stubs=False, |
| 758 | + prefer_stubs=False, |
| 759 | + ) |
| 760 | + if not definitions: |
| 761 | + server.show_message_log( |
| 762 | + f"no definitions found for name {n.description} ({n.line}:{n.column})", |
| 763 | + MessageType.Debug, |
| 764 | + ) |
| 765 | + return None |
| 766 | + |
| 767 | + if len(definitions) > 1: |
| 768 | + server.show_message_log( |
| 769 | + f"multiple definitions found for name {n.description} ({n.line}:{n.column})", |
| 770 | + MessageType.Debug, |
| 771 | + ) |
| 772 | + |
| 773 | + definition, *_ = definitions |
| 774 | + if ( |
| 775 | + definition_type := TYPE_TO_TOKEN_ID.get(definition.type, None) |
| 776 | + ) is None: |
| 777 | + server.show_message_log( |
| 778 | + f"no matching semantic token for name {n.description} ({n.line}:{n.column})", |
| 779 | + MessageType.Debug, |
| 780 | + ) |
| 781 | + return None |
| 782 | + |
| 783 | + return [n.line - 1, n.column, len(n.name), definition_type, 0] |
| 784 | + |
| 785 | + |
| 786 | +@SERVER.thread() |
| 787 | +@SERVER.feature( |
| 788 | + TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL, |
| 789 | + SemanticTokensLegend( |
| 790 | + token_types=list(SemanticTokenTypes), token_modifiers=[] |
| 791 | + ), |
| 792 | +) |
| 793 | +def semantic_tokens_full( |
| 794 | + server: JediLanguageServer, params: SemanticTokensParams |
| 795 | +) -> SemanticTokens: |
| 796 | + """Thin wrap around _semantic_tokens_range().""" |
| 797 | + document = server.workspace.get_text_document(params.text_document.uri) |
| 798 | + jedi_script = jedi_utils.script(server.project, document) |
| 799 | + |
| 800 | + server.show_message_log( |
| 801 | + f"semantic_tokens_full {params.text_document.uri} ", |
| 802 | + MessageType.Log, |
| 803 | + ) |
| 804 | + |
| 805 | + return _semantic_tokens_range( |
| 806 | + server, |
| 807 | + jedi_script, |
| 808 | + Range(Position(0, 0), Position(INTEGER_MAX_VALUE, INTEGER_MAX_VALUE)), |
| 809 | + ) |
| 810 | + |
| 811 | + |
| 812 | +@SERVER.thread() |
| 813 | +@SERVER.feature( |
| 814 | + TEXT_DOCUMENT_SEMANTIC_TOKENS_RANGE, |
| 815 | + SemanticTokensLegend( |
| 816 | + token_types=list(SemanticTokenTypes), token_modifiers=[] |
| 817 | + ), |
| 818 | +) |
| 819 | +def semantic_tokens_range( |
| 820 | + server: JediLanguageServer, params: SemanticTokensRangeParams |
| 821 | +) -> SemanticTokens: |
| 822 | + """Thin wrap around _semantic_tokens_range().""" |
| 823 | + document = server.workspace.get_text_document(params.text_document.uri) |
| 824 | + jedi_script = jedi_utils.script(server.project, document) |
| 825 | + |
| 826 | + server.show_message_log( |
| 827 | + f"semantic_tokens_range {params.text_document.uri} {params.range}", |
| 828 | + MessageType.Log, |
| 829 | + ) |
| 830 | + |
| 831 | + return _semantic_tokens_range(server, jedi_script, params.range) |
| 832 | + |
| 833 | + |
| 834 | +def _semantic_tokens_range( |
| 835 | + server: JediLanguageServer, jedi_script: Script, doc_range: Range |
| 836 | +) -> SemanticTokens: |
| 837 | + """General purpose function to do full / range semantic tokens.""" |
| 838 | + line, column = doc_range.start.line, doc_range.start.character |
| 839 | + names = jedi_script.get_names( |
| 840 | + all_scopes=True, definitions=True, references=True |
| 841 | + ) |
| 842 | + data = [] |
| 843 | + |
| 844 | + for n in names: |
| 845 | + if ( |
| 846 | + not doc_range.start |
| 847 | + < Position(n.line - 1, n.column) |
| 848 | + < doc_range.end |
| 849 | + ): |
| 850 | + continue |
| 851 | + |
| 852 | + token = _raw_semantic_token(server, n) |
| 853 | + |
| 854 | + server.show_message_log( |
| 855 | + f"raw token for name {n.description} ({n.line - 1}:{n.column}): {token}", |
| 856 | + MessageType.Debug, |
| 857 | + ) |
| 858 | + if token is None: |
| 859 | + continue |
| 860 | + |
| 861 | + token_line, token_column = token[0], token[1] |
| 862 | + delta_column = ( |
| 863 | + token_column - column if token_line == line else token_column |
| 864 | + ) |
| 865 | + delta_line = token_line - line |
| 866 | + |
| 867 | + line = token_line |
| 868 | + column = token_column |
| 869 | + token[0] = delta_line |
| 870 | + token[1] = delta_column |
| 871 | + |
| 872 | + server.show_message_log( |
| 873 | + f"diff token for name {n.description} ({n.line - 1}:{n.column}): {token}", |
| 874 | + MessageType.Debug, |
| 875 | + ) |
| 876 | + data.extend(token) |
| 877 | + |
| 878 | + return SemanticTokens(data=data) |
| 879 | + |
| 880 | + |
727 | 881 | # Static capability or initializeOptions functions that rely on a specific
|
728 | 882 | # client capability or user configuration. These are associated with
|
729 | 883 | # JediLanguageServer within JediLanguageServerProtocol.lsp_initialize
|
|
0 commit comments