diff --git a/README.md b/README.md index 9290c4a58c..a30a43d559 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,7 @@ Note: - `lsp-bridge-peek-tree-next-node`: Select the next lower-level node in the browsing history (default binding to ``) - `lsp-bridge-indent-left`: Indents the pasted text to the left according to the indent values defined in `lsp-bridge-formatting-indent-alist` - `lsp-bridge-indent-right`: Indents the pasted text to the right according to the indent values defined in `lsp-bridge-formatting-indent-alist` +- `lsp-bridge-semantic-tokens-mode`: Enable or disable semantic token highlighting, Please refer to [Semantic Tokens Wiki](https://github.com/manateelazycat/lsp-bridge/wiki/Semantic-Tokens) for detailed instructions on how to use. ## LSP server options lsp-bridge provides support for more than two language servers for many languages. You can customize the following options to choose the language server you prefer: @@ -379,6 +380,7 @@ The following is the directory structure of the lsp-bridge project: | lsp-bridge-inlay-hint.el | Provides code type hints, more useful for static languages, such as Rust or Haskell | | lsp-bridge-jdtls.el | Provides third-party library jump function for Java language | | lsp-bridge-dart.el | Provides support for Dart's private protocols, such as Dart's Closing Labels protocol | +| lsp-bridge-semantic-tokens.el | Flexible display of certain semantic symbols is especially useful for static languages such as C or C++. | | lsp-bridge-lsp-installer.el | Install TabNine and Omnisharp | | lsp-bridge-peek.el | Use peek windows to view definitions and references | | lsp-bridge.py | The main Python logic part of lsp-bridge, providing event loops, message scheduling, and status management | diff --git a/README.zh-CN.md b/README.zh-CN.md index 17f47dd739..403999b431 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -146,6 +146,7 @@ lsp-bridge 开箱即用, 安装好语言对应的 [LSP 服务器](https://gith - `lsp-bridge-peek-tree-next-node`: 选择浏览历史上下一级节点 (默认绑定到 `` ) - `lsp-bridge-indent-left`: 根据 `lsp-bridge-formatting-indent-alist` 定义的缩进值, 向左缩进刚刚粘贴的文本 - `lsp-bridge-indent-right`: 根据 `lsp-bridge-formatting-indent-alist` 定义的缩进值, 向右缩进刚刚粘贴的文本 +- `lsp-bridge-semantic-tokens-mode`: 开启或者关闭语义符号高亮,详细用法请看 [Semantic Tokens Wiki](https://github.com/manateelazycat/lsp-bridge/wiki/Semantic-Tokens-%5B%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87%5D) ## LSP 服务器选项 lsp-bridge 针对许多语言都提供 2 个以上的语言服务器支持, 您可以通过定制下面的选项来选择你喜欢的语言服务器: @@ -376,9 +377,10 @@ lsp-bridge 每种语言的服务器配置存储在 [lsp-bridge/langserver](https | lsp-bridge-code-action.el | 代码修复相关代码 | | lsp-bridge-diagnostic.el | 诊断信息相关代码 | | lsp-bridge-ref.el | 代码引用查看框架, 提供引用查看、 批量重命名、 引用结果正则过滤等, 核心代码 fork 自 color-rg.el | -| lsp-bridge-inlay-hint.el | 提供代码类型提示, 对于静态语言, 比如 Rust 或 Haskell 比较有用 | +| lsp-bridge-inlay-hint.el | 提供代码类型提示, 对于静态语言, 比如 Rust 或 Haskell 比较有用 | | lsp-bridge-jdtls.el | 提供 Java 语言第三方库跳转功能 | -| lsp-bridge-dart.el | 提供对 Dart 私有协议的支持, 比如 Dart 的 Closing Labels 协议 | +| lsp-bridge-dart.el | 提供对 Dart 私有协议的支持, 比如 Dart 的 Closing Labels 协议 | +| lsp-bridge-semantic-tokens.el | 灵活显示某些语义符号, 对于静态语言, 比如 C 或 C++ 比较有用 | | lsp-bridge-lsp-installer.el | 安装 TabNine 和 Omnisharp | | lsp-bridge-peek.el | 用 peek windows 来查看定义和引用 | | lsp-bridge.py | lsp-bridge 的 Python 主逻辑部分, 提供事件循环、 消息调度和状态管理 | diff --git a/core/handler/__init__.py b/core/handler/__init__.py index 34d2fa4b28..ebd33290b5 100644 --- a/core/handler/__init__.py +++ b/core/handler/__init__.py @@ -65,3 +65,4 @@ def handle_response(self, request_id, response): from core.handler.jdtls.jdtls_list_overridable_methods import JdtlsListOverridableMethods from core.handler.jdtls.jdtls_add_overridable_methods import JdtlsAddOverridableMethods from core.handler.inlay_hint import InlayHint +from core.handler.semantic_tokens import SemanticTokens diff --git a/core/handler/semantic_tokens.py b/core/handler/semantic_tokens.py new file mode 100644 index 0000000000..44adc7421b --- /dev/null +++ b/core/handler/semantic_tokens.py @@ -0,0 +1,162 @@ +import time + +from core.handler import Handler +from core.utils import * + +class SemanticTokens(Handler): + name = "semantic_tokens" + method = "textDocument/semanticTokens/full" + cancel_on_change = False + send_document_uri = True + provider = "semantic_tokens_provider" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.buffer_name = None + self.range_begin = None + self.range_end = None + + self.pre_last_change = (time.time(), time.time()) + self.tokens = [] + self.render_tokens = set() + + self.type_face_names = None + self.type_face_names_dict = dict() + self.type_modifier_face_names = None + self.ignore_modifier_limit_types = None + + + def process_request(self, buffer_name, range_begin, range_end, use_cache): + self.buffer_name = buffer_name + self.range_begin = range_begin + self.range_end = range_end + if not use_cache: + self.render_tokens = set() + return dict() + + + def process_response(self, response): + if response is None: + return + data = response.get("data") + if data is None: + return + self.tokens = data + self.update_tokens(self.tokens) + + def update_tokens(self, tokens): + if tokens is None: + return + index = 0 + cur_line = 0 + start_character = 0 + render_tokens = set() + while index < len(tokens): + if tokens[index] != 0: + start_character = 0 + cur_line += tokens[index] + start_character += tokens[index + 1] + if (cur_line >= self.range_begin["line"] and + cur_line <= self.range_end["line"]): + faces_index = self.get_faces_index(tokens[index + 3], tokens[index + 4]) + if faces_index is not None: + render_tokens.add((cur_line, start_character, tokens[index + 2], faces_index[0], faces_index[1])) + index = index + 5 + + (new_tokens, old_tokens) = self.calc_diff_tokens(self.render_tokens, render_tokens) + + if len(new_tokens) != 0 or len(old_tokens) != 0: + eval_in_emacs("lsp-bridge-semantic-tokens--update", self.buffer_name, list(old_tokens), self.absolute_line_to_relative(new_tokens)) + self.render_tokens = render_tokens + + def get_faces_index(self, type_index, type_modifier_index): + type_name = self.file_action.single_server.semantic_tokens_provider["legend"]["tokenTypes"][type_index] + ignore_modifier = self.is_ignore_modifier(type_name) + if type_modifier_index == 0 and not ignore_modifier: + return None + type_face_index = self.get_type_face_index(type_name) + if type_face_index is None: + return None + type_modifier_faces_index = self.get_type_modifier_faces_index(type_modifier_index) + if len(type_modifier_faces_index) == 0 and not ignore_modifier: + return None + return (type_face_index, type_modifier_faces_index) + + def get_type_face_index(self, type_name): + if self.type_face_names is None: + [type_faces] = get_emacs_vars(["lsp-bridge-semantic-tokens-type-faces"]) + if type_faces is not None: + faces = type_faces.I + self.type_face_names = dict() + for index in range(len(faces)): + self.type_face_names[faces[index][0]] = index + else: + return None + face_index = self.type_face_names.get(type_name) + return face_index + + def get_type_modifier_faces_index(self, type_modifier_index): + if self.type_modifier_face_names is None: + [type_modifier_faces] = get_emacs_vars(["lsp-bridge-semantic-tokens-type-modifier-faces"]) + if type_modifier_faces is not None: + faces = type_modifier_faces.I + self.type_modifier_face_names = dict() + for index in range(len(faces)): + self.type_modifier_face_names[faces[index][0]] = index + else: + return () + + token_modifiers = self.file_action.single_server.semantic_tokens_provider["legend"]["tokenModifiers"] + type_modifier_names = [token_modifiers[index] for index in self.find_ones(type_modifier_index)] + type_modifier_faces_index = [] + for name in type_modifier_names: + index = self.type_modifier_face_names.get(name) + if index is not None: + type_modifier_faces_index.append(index) + return tuple(type_modifier_faces_index) + + def is_ignore_modifier(self, type_name): + if self.ignore_modifier_limit_types is None: + [types] = get_emacs_vars(["lsp-bridge-semantic-tokens-ignore-modifier-limit-types"]) + types = types.I + if types is not None: + self.ignore_modifier_limit_types = dict() + for index in range(len(types)): + self.ignore_modifier_limit_types[types[index]] = index + + return type_name in self.ignore_modifier_limit_types + + def calc_diff_tokens(self, pre_tokens, cur_tokens): + common_tokens = cur_tokens & pre_tokens + new_tokens = list(cur_tokens - common_tokens) + old_tokens = list(pre_tokens - common_tokens) + new_tokens.sort(key = lambda token : token[0]) + old_tokens.sort(key = lambda token : token[0]) + return (new_tokens, old_tokens) + + def absolute_line_to_relative(self, tokens): + relative_tokens = [] + cur_line = 0 + delta_line = 0 + start_character = 0 + delta_character = 0 + for token in tokens: + delta_line = token[0] - cur_line + if token[0] != cur_line: + cur_line = token[0] + start_character = 0 + delta_character = token[1] - start_character + start_character = token[1] + relative_tokens.append((delta_line, delta_character, token[2], token[3], token[4])) + + return relative_tokens + + def find_ones(self, num): + ones = [] + bit = 0 + while num > 0: + if num & 1: + ones.append(bit) + num >>= 1 + bit += 1 + return ones diff --git a/core/lspserver.py b/core/lspserver.py index a36343e15e..94154096cb 100755 --- a/core/lspserver.py +++ b/core/lspserver.py @@ -221,6 +221,7 @@ def __init__(self, message_queue, project_path, server_info, server_name, enable self.signature_help_provider = False self.workspace_symbol_provider = False self.inlay_hint_provider = False + self.semantic_tokens_provider = False self.code_action_kinds = [ "quickfix", @@ -608,7 +609,8 @@ def save_attribute_from_message(self, message): ("workspace_symbol_provider", ["result", "capabilities", "workspaceSymbolProvider"]), ("inlay_hint_provider", ["result", "capabilities", "inlayHintProvider", "resolveProvider"]), ("save_include_text", ["result", "capabilities", "textDocumentSync", "save", "includeText"]), - ("text_document_sync", ["result", "capabilities", "textDocumentSync"])] + ("text_document_sync", ["result", "capabilities", "textDocumentSync"]), + ("semantic_tokens_provider", ["result", "capabilities", "semanticTokensProvider"])] for attr, path in attributes_to_set: self.set_attribute_from_message(message, attr, path) diff --git a/lsp-bridge-semantic-tokens.el b/lsp-bridge-semantic-tokens.el new file mode 100644 index 0000000000..f0e1a9f54c --- /dev/null +++ b/lsp-bridge-semantic-tokens.el @@ -0,0 +1,284 @@ +;;; lsp-bridge-semantic-tokens.el --- LSP bridge -*- lexical-binding: t -*- + +(defgroup lsp-bridge-semantic-tokens nil + "Semantic tokens support." + :prefix "lsp-bridge-semantic-tokens-" + :group 'lsp-bridge) + +(defcustom lsp-bridge-semantic-tokens-apply-modifiers 'override + "The method used to apply modifiers." + :group 'lsp-bridge-semantic-tokens + :type '(radio + (const nil) + (const :tag "override" override) + (const :tag "combine" combine))) + +(defcustom lsp-bridge-semantic-tokens-auto-update 'timer + "The method used to auto update semantic tokens." + :group 'lsp-bridge-semantic-tokens + :type '(radio + (const :tag "timer" timer) + (const :tag "hook" hook))) + +(defcustom lsp-bridge-semantic-tokens-timer-update-interval 1 + "Value is a number specifying how many seconds to request semantic tokens in +timer auto update method." + :group 'lsp-bridge-semantic-tokens + :type 'number) + +(defcustom lsp-bridge-semantic-tokens-delay 0.5 + "Value is a number specifying how many seconds to wait after a +window has been (re)scrolled or buffer has been changed before +requesting new semantic tokens." + :group 'lsp-bridge-semantic-tokens + :type 'number) + +(defface lsp-bridge-semantic-tokens-variable-face + '((t (:inherit font-lock-variable-name-face))) + "Face used for variable name." + :group 'lsp-bridge-semantic-tokens) + +(defface lsp-bridge-semantic-tokens-method-face + '((t (:inherit font-lock-function-name-face))) + "Face used for method name." + :group 'lsp-bridge-semantic-tokens) + +(defface lsp-bridge-semantic-tokens-comment-face + '((t (:inherit font-lock-comment-face))) + "Face used for deprecated token." + :group 'lsp-bridge-semantic-tokens) + +(defface lsp-bridge-semantic-tokens-global-scope-face + '((t :weight extra-bold)) + "Face used for globalScope token." + :group 'lsp-bridge-semantic-tokens) + +(defvar-local lsp-bridge-semantic-tokens-type-faces + [("comment" . lsp-bridge-semantic-tokens-comment-face)] + "Faces to use for semantic tokens.") + +(defvar-local lsp-bridge-semantic-tokens-type-modifier-faces [] + "Semantic tokens modifier faces. +Faces to use for semantic token modifiers.") + +(defvar-local lsp-bridge-semantic-tokens-ignore-modifier-limit-types ["comment"] + "Which types need to ignore modifier limit.") + +(defvar-local lsp-bridge-semantic-tokens--overlays nil "Semantic tokens overlays.") + +(defconst lsp-bridge-semantic-tokens--face-attribute-names + (apply 'append + (mapcar (lambda (x) (list (car x))) + face-attribute-name-alist))) + +(defun lsp-bridge-semantic-tokens--combine-faces (faces) + "Combine attributes of faces to one face." + (let ((attributes (list))) + (dolist (face faces) + (dolist (attr-name lsp-bridge-semantic-tokens--face-attribute-names) + (when-let* ((value (face-attribute face attr-name (window-frame))) + (valid (not (eq value 'unspecified)))) + (setq attributes (plist-put attributes attr-name value 'equal))))) + `((t ,@attributes)))) + +(defun lsp-bridge-semantic-tokens--delete-overlays (keys) + "Delete semantic tokens overlays." + (dolist (key keys) + (when-let ((ov (gethash key lsp-bridge-semantic-tokens--overlays))) + (delete-overlay ov) + (remhash key lsp-bridge-semantic-tokens--overlays)))) + +(defun lsp-bridge-semantic-tokens--update (buffer-name old-tokens new-tokens) + "Update semantic tokens." + (with-current-buffer buffer-name + (with-silent-modifications + (lsp-bridge-semantic-tokens--delete-overlays old-tokens) + (save-mark-and-excursion + (save-restriction + (widen) + (goto-char (point-min)) + (let ((current-line 0) + (line-start-pos (point)) + (colum 0) + (line-delta) + (token-begin) + (token-end) + (ov)) + (dolist (token new-tokens) + (setq line-delta (nth 0 token)) + (unless (= line-delta 0) + (forward-line line-delta) + (setq line-start-pos (point)) + (setq colum 0) + (setq current-line (+ current-line line-delta))) + (setq colum (+ colum (nth 1 token))) + (setq token-begin (+ line-start-pos colum)) + (setq token-end (min (line-end-position) + (+ token-begin (nth 2 token)))) + (setq ov (make-overlay token-begin token-end)) + + ;; apply face + (pcase lsp-bridge-semantic-tokens-apply-modifiers + ('override + (if-let ((last-modifier-face-index (car (last (nth 4 token))))) + (overlay-put ov 'face (cdr (aref lsp-bridge-semantic-tokens-type-modifier-faces last-modifier-face-index))) + (overlay-put ov 'face (cdr (aref lsp-bridge-semantic-tokens-type-faces (nth 3 token)))))) + ('combine + (let ((faces-alist (cons (aref lsp-bridge-semantic-tokens-type-faces (nth 3 token)) + (mapcar #'(lambda (face-index) + (aref lsp-bridge-semantic-tokens-type-modifier-faces face-index)) + (nth 4 token)))) + (combine-face-name "lsp-bridge-semantic-tokens-combine") + (faces)) + (dolist (face-alist faces-alist) + (setq combine-face-name (concat combine-face-name "-" (car face-alist))) + (push (cdr face-alist) faces)) + (let ((combine-face-symbol (intern combine-face-name))) + (unless (facep combine-face-symbol) + (make-empty-face combine-face-symbol) + (face-spec-set combine-face-symbol + (lsp-bridge-semantic-tokens--combine-faces faces))) + (overlay-put ov 'face combine-face-symbol)))) + (_ + (overlay-put ov 'face (cdr (aref lsp-bridge-semantic-tokens-type-faces (nth 3 token)))))) + + (puthash (list current-line colum (nth 2 token) (nth 3 token) (nth 4 token)) + ov lsp-bridge-semantic-tokens--overlays)))))))) + + +(defun lsp-bridge-semantic-tokens--request-1 (from to use-cache) + "Try request semantic tokens between FROM to TO." + (lsp-bridge-call-file-api "semantic_tokens" + (buffer-name) + (lsp-bridge--point-position from) + (lsp-bridge--point-position to) + (if use-cache + 1 + 0))) + +(defun lsp-bridge-semantic-tokens--after-window-scroll (window display-start) + "Try request semantic tokens after window scroll." + (cl-macrolet ((wsetq (sym val) `(set-window-parameter window ',sym ,val)) + (wgetq (sym) `(window-parameter window ',sym))) + (let ((buf (window-buffer window)) + (timer (wgetq lsp-bridge-semantic-tokens--timer)) + (last-display-start (wgetq lsp-bridge-semantic-tokens--last-display-start))) + (unless (eql last-display-start display-start) + (when timer + (cancel-timer timer)) + (wsetq lsp-bridge-semantic-tokens--last-display-start display-start) + (wsetq lsp-bridge-semantic-tokens--timer + (run-at-time lsp-bridge-semantic-tokens-delay nil + (lambda () + (when (buffer-live-p buf) + (with-current-buffer buf + (when (eq buf (window-buffer window)) + (lsp-bridge-semantic-tokens--request-1 (window-start window) (window-end window) t) + (wsetq lsp-bridge-semantic-tokens--timer nil))))))))))) + +(defun lsp-bridge-semantic-tokens--after-window-config-change () + "Try request semantic tokens after window config change scroll." + (lsp-bridge-semantic-tokens--request-1 (window-start) (window-end) t)) + +(defvar-local lsp-bridge-semantic-tokens--after-change-timer nil) + +(defun lsp-bridge-semantic-tokens--after-change () + "Try request semantic tokens after change." + (let ((buf (current-buffer))) + (when lsp-bridge-semantic-tokens--after-change-timer + (cancel-timer lsp-bridge-semantic-tokens--after-change-timer)) + + (setq-local lsp-bridge-semantic-tokens--after-change-timer + (run-at-time lsp-bridge-semantic-tokens-delay nil + (lambda () + (when (buffer-live-p buf) + (with-current-buffer buf + (lsp-bridge-semantic-tokens--request-1 (window-start) (window-end) nil) + (setq-local lsp-bridge-semantic-tokens--after-change-timer nil)))))))) + +(defun lsp-bridge-semantic-tokens--timer-update () + "Try request semantic tokens after idle timer." + (when lsp-bridge-semantic-tokens-mode + (lsp-bridge-semantic-tokens--request-1 (window-start) (window-end) t))) + +(defvar-local lsp-bridge-semantic-tokens--monitor-change nil) + +(defun lsp-bridge-semantic-tokens--hook-enable () + (when (lsp-bridge-has-lsp-server-p) + (unless lsp-bridge-semantic-tokens--overlays + (setq-local lsp-bridge-semantic-tokens--overlays (make-hash-table :test 'equal))) + + (setq-local lsp-bridge-semantic-tokens--monitor-change t) + + (add-hook 'window-scroll-functions + #'lsp-bridge-semantic-tokens--after-window-scroll nil t) + + (add-hook 'window-configuration-change-hook + #'lsp-bridge-semantic-tokens--after-window-config-change nil t) + + (run-at-time lsp-bridge-semantic-tokens-delay nil #'lsp-bridge-semantic-tokens-request))) + +(defun lsp-bridge-semantic-tokens--hook-disable () + (remove-hook 'window-configuration-change-hook + #'lsp-bridge-semantic-tokens--after-window-config-change t) + (remove-hook 'window-scroll-functions + #'lsp-bridge-semantic-tokens--after-window-scroll t) + (setq-local lsp-bridge-semantic-tokens--monitor-change nil) + + (lsp-bridge-semantic-tokens--delete-overlays (hash-table-keys lsp-bridge-semantic-tokens--overlays)) + (setq-local lsp-bridge-semantic-tokens--overlays nil)) + + +(defvar lsp-bridge-semantic-tokens--timer-update-buffers 0 "The count of enable lsp-bridge-semantic-tokens-mode.") + +(defun lsp-bridge-semantic-tokens--close-file () + (when lsp-bridge-semantic-tokens-mode + (lsp-bridge-semantic-tokens-mode -1))) + +(defun lsp-bridge-semantic-tokens--timer-enable () + (when (lsp-bridge-has-lsp-server-p) + (unless lsp-bridge-semantic-tokens--overlays + (setq-local lsp-bridge-semantic-tokens--overlays (make-hash-table :test 'equal))) + + (when (equal lsp-bridge-semantic-tokens--timer-update-buffers 0) + (run-with-idle-timer lsp-bridge-semantic-tokens-timer-update-interval t #'lsp-bridge-semantic-tokens--timer-update)) + + (cl-incf lsp-bridge-semantic-tokens--timer-update-buffers) + + (add-hook 'kill-buffer-hook #'lsp-bridge-semantic-tokens--close-file nil t) + (run-at-time lsp-bridge-semantic-tokens-delay nil #'lsp-bridge-semantic-tokens-request))) + + +(defun lsp-bridge-semantic-tokens--timer-disable () + (cl-decf lsp-bridge-semantic-tokens--timer-update-buffers) + (when (equal lsp-bridge-semantic-tokens--timer-update-buffers 0) + (cancel-function-timers #'lsp-bridge-semantic-tokens--timer-update)) + + (remove-hook 'kill-buffer-hook #'lsp-bridge-semantic-tokens--close-file t) + + (lsp-bridge-semantic-tokens--delete-overlays (hash-table-keys lsp-bridge-semantic-tokens--overlays)) + (setq-local lsp-bridge-semantic-tokens--overlays nil)) + +(defun lsp-bridge-semantic-tokens-request () + "Try request semantic tokens." + (lsp-bridge-semantic-tokens--request-1 (window-start) (window-end) nil)) + +(define-minor-mode lsp-bridge-semantic-tokens-mode + "Mirror mode for show semantic tokens." + :global nil + (cond (lsp-bridge-semantic-tokens-mode + (pcase lsp-bridge-semantic-tokens-auto-update + ('hook + (lsp-bridge-semantic-tokens--hook-enable)) + ('timer + (lsp-bridge-semantic-tokens--timer-enable)))) + (t + (pcase lsp-bridge-semantic-tokens-auto-update + ('hook + (lsp-bridge-semantic-tokens--hook-disable)) + ('timer + (lsp-bridge-semantic-tokens--timer-disable)))))) + +(provide 'lsp-bridge-semantic-tokens) + +;;; lsp-bridge-semantic-tokens.el ends here diff --git a/lsp-bridge.el b/lsp-bridge.el index 909f75b43d..ae6b510fcb 100644 --- a/lsp-bridge.el +++ b/lsp-bridge.el @@ -96,6 +96,7 @@ (require 'lsp-bridge-org-babel) (require 'lsp-bridge-inlay-hint) (require 'lsp-bridge-dart) +(require 'lsp-bridge-semantic-tokens) (defgroup lsp-bridge nil "LSP-Bridge group."