diff --git a/core/remote_file.py b/core/remote_file.py index 6a48921483..21c06831b1 100755 --- a/core/remote_file.py +++ b/core/remote_file.py @@ -205,7 +205,7 @@ def handle_close_file(self, data, client_socket): path = data["path"] if path in self.file_dict: - self.file_dict[path] = "" + del self.file_dict[path] def save_ip_to_file(ip, filename): with open(filename, 'r') as f: diff --git a/core/search_file_words.py b/core/search_file_words.py index 02552e5c8a..9ab88343ef 100755 --- a/core/search_file_words.py +++ b/core/search_file_words.py @@ -60,10 +60,13 @@ def index_file(self, filepath, content): self.search_words_queue.put("search_words") - def load_file(self, filepath): + def load_file(self, filepath, from_file_server=False): try: - with open(filepath) as f: - content = f.read() + if from_file_server: + content = get_file_content_from_file_server(filepath) + else: + with open(filepath) as f: + content = f.read() except: return diff --git a/core/utils.py b/core/utils.py index 1709c82d34..b152d6bb1c 100755 --- a/core/utils.py +++ b/core/utils.py @@ -133,6 +133,14 @@ def get_buffer_content(filename, buffer_name): else: return get_emacs_func_result('get-buffer-content', buffer_name) +def get_file_content_from_file_server(filename): + global remote_file_server + + if filename in remote_file_server.file_dict: + return remote_file_server.file_dict[filename] + else: + return "" + def get_current_line(): return get_emacs_func_result('get-current-line') @@ -423,6 +431,9 @@ def split_ssh_path(ssh_path): else: return None +def is_remote_path(filepath): + return filepath.startswith("/ssh:") + def eval_sexp_in_emacs(sexp): epc_client.call("eval-in-emacs", [sexp]) diff --git a/lsp-bridge.el b/lsp-bridge.el index c56a48c81d..5b777fca58 100644 --- a/lsp-bridge.el +++ b/lsp-bridge.el @@ -101,6 +101,8 @@ :group 'applications) (require 'acm) +(require 'tramp) +(defvar lsp-bridge-tramp-alias-alist nil) (setq acm-backend-lsp-fetch-completion-item-func 'lsp-bridge-fetch-completion-item-info) @@ -649,6 +651,15 @@ you can customize `lsp-bridge-get-workspace-folder' to return workspace folder p "Open characters for string interpolation. The elements are cons cell (major-mode . open-char-regexp)" :type 'cons) +(defvar lsp-bridge-enable-with-tramp nil + "Whether enable lsp-bridge when editing tramp file.") + +(defun lsp-bridge-find-file-hook-function () + (when (and lsp-bridge-enable-with-tramp (file-remote-p (buffer-file-name))) + (lsp-bridge-sync-tramp-remote))) + +(add-hook 'find-file-hook #'lsp-bridge-find-file-hook-function) + (defun lsp-bridge--get-indent-width (mode) "Get indentation offset for MODE." (or (alist-get mode lsp-bridge-formatting-indent-alist) @@ -690,9 +701,11 @@ So we build this macro to restore postion after code format." (defun lsp-bridge-get-match-buffer-by-remote-file (host path) (cl-dolist (buffer (buffer-list)) (with-current-buffer buffer - (when (string-equal (buffer-name) (format "[LBR] %s" (file-name-nondirectory path))) - (cl-return buffer)) - ))) + (when (and (boundp 'lsp-bridge-remote-file-path) + (string-equal lsp-bridge-remote-file-path path) + (boundp 'lsp-bridge-remote-file-host) + (string-equal lsp-bridge-remote-file-host host)) + (cl-return buffer))))) (defun lsp-bridge-get-match-buffer-by-filepath (name) (cl-dolist (buffer (buffer-list)) @@ -1389,7 +1402,7 @@ So we build this macro to restore postion after code format." ;; Codeium search. (when (and acm-enable-codeium ;; Codeium backend not support remote file now, disable it temporary. - (not (lsp-bridge-is-remote-file)) + (or (not (lsp-bridge-is-remote-file)) lsp-bridge-use-local-codeium) ;; Don't enable codeium on Markdown mode, Org mode, ielm and minibuffer, very disruptive to writing. (not (or (derived-mode-p 'markdown-mode) (eq major-mode 'org-mode) @@ -1489,8 +1502,7 @@ So we build this macro to restore postion after code format." (defun lsp-bridge-search-words-update (begin-pos end-pos change-text) (if (lsp-bridge-is-remote-file) (progn - (lsp-bridge-remote-save-buffer) - (lsp-bridge-remote-send-func-request "search_file_words_load_file" (list lsp-bridge-remote-file-path))) + (lsp-bridge-remote-send-func-request "search_file_words_load_file" (list lsp-bridge-remote-file-path t))) (when (lsp-bridge-epc-live-p lsp-bridge-epc-process) (lsp-bridge-call-async "search_file_words_change_buffer" (buffer-name) @@ -1697,8 +1709,9 @@ Off by default." (let (position-before-jump) (lsp-bridge-define--jump-record-postion) - (if (string-equal filehost "") + (if (or (string-equal filehost "") lsp-bridge-enable-with-tramp) (progn + (setq filename (concat (cdr (assoc filehost lsp-bridge-tramp-alias-alist) filename))) (let ((match-window (lsp-bridge-find-window-match-filename filename))) (if (and lsp-bridge-find-def-select-in-open-windows match-window) @@ -2232,6 +2245,9 @@ We need exclude `markdown-code-fontification:*' buffer in `lsp-bridge-monitor-be (= after-point buffer-max) max-num-results)))) +(defvar lsp-bridge-use-local-codeium nil + "Whether use local codeium when editing remote file.") + (defun lsp-bridge-codeium-complete () (interactive) (let ((all-text (buffer-substring-no-properties (point-min) (point-max))) @@ -2241,7 +2257,7 @@ We need exclude `markdown-code-fontification:*' buffer in `lsp-bridge-monitor-be (while (not (alist-get mode acm-backend-codeium-language-alist)) (setq mode (get mode 'derived-mode-parent))) (alist-get mode acm-backend-codeium-language-alist)))) - (if (lsp-bridge-is-remote-file) + (if (and (lsp-bridge-is-remote-file) (not lsp-bridge-use-local-codeium)) (lsp-bridge-remote-send-func-request "codeium_complete" (list (1- (point)) @@ -2368,6 +2384,39 @@ We need exclude `markdown-code-fontification:*' buffer in `lsp-bridge-monitor-be (split-string (buffer-string) "\n" t))))) (lsp-bridge-call-async "open_remote_file" path (list :line 0 :character 0)))) +(defun lsp-bridge-sync-tramp-remote () + (interactive) + (let* ((tramp-file-name (tramp-dissect-file-name (buffer-file-name))) + (username (tramp-file-name-user tramp-file-name)) + (domain (tramp-file-name-domain tramp-file-name)) + (port (tramp-file-name-port tramp-file-name)) + (host (tramp-file-name-host tramp-file-name)) + (path (tramp-file-name-localname tramp-file-name)) + alias) + + (unless (network-lookup-address-info host 'ipv4 'numeric) + (setq alias host)) + + (if alias + (if-let (alias-host (assoc alias lsp-bridge-tramp-alias-alist)) + (setq host (cdr alias-host)) + (let ((default-directory "~/")) + (setq host (string-trim (shell-command-to-string + (format "ssh -G -T %s | grep '^hostname' | cut -d ' ' -f 2" alias))))) + (push `(,alias . ,host) lsp-bridge-tramp-alias-alist))) + + (unless (assoc host lsp-bridge-tramp-alias-alist) + (push `(,host . ,(concat (string-join (butlast (string-split (buffer-file-name) ":" t)) ":") ":")) + lsp-bridge-tramp-alias-alist)) + + (lsp-bridge-call-async "sync_tramp_remote" username host port alias) + + (setq-local lsp-bridge-remote-file-flag t) + (setq-local lsp-bridge-remote-file-host host) + (setq-local lsp-bridge-remote-file-path path) + + (add-hook 'kill-buffer-hook 'lsp-bridge-remote-kill-buffer nil t))) + (defun lsp-bridge-open-remote-file--response(server path content position) (let ((buf-name (format "[LBR] %s" (file-name-nondirectory path)))) (lsp-bridge-define--jump-record-postion) diff --git a/lsp_bridge.py b/lsp_bridge.py index 9ed407091b..729e6df85a 100755 --- a/lsp_bridge.py +++ b/lsp_bridge.py @@ -25,6 +25,7 @@ import traceback import json import socket + from functools import wraps from pathlib import Path @@ -236,6 +237,49 @@ def open_remote_file(self, path, jump_define_pos): else: message_emacs("Please input valid path match rule: 'ip:/path/file'.") + @threaded + def sync_tramp_remote(self, server_username, server_host, ssh_port, alias): + import paramiko + if alias: + if alias in self.host_names: + server_host = self.host_names[alias]["server_host"] + server_username = self.host_names[alias]["username"] + ssh_port = self.host_names[alias]["ssh_port"] + else: + ssh_config = paramiko.SSHConfig() + ssh_config.parse(open(os.path.expanduser('~/.ssh/config'))) + conf = ssh_config.lookup(alias) + + server_host = conf.get('hostname', server_host) + server_username = conf.get('user', server_username) + ssh_port = conf.get('port', ssh_port) + + self.host_names[alias] = {"server_host": server_host, "username": server_username, "ssh_port": ssh_port} + + if not server_username: + if server_host in self.host_names: + server_username = self.host_names[server_host]["username"] + else: + server_username = "root" + + if not ssh_port: + if server_host in self.host_names: + ssh_port = self.host_names[server_host]["ssh_port"] + else: + ssh_port = 22 + + self.host_names[server_host] = {"username": server_username, "ssh_port": ssh_port} + + try: + client_id = f"{server_host}:{REMOTE_FILE_ELISP_CHANNEL}" + if client_id not in self.client_dict: + client = self.get_socket_client(server_host, REMOTE_FILE_ELISP_CHANNEL) + client.send_message("Connect") + message_emacs(f"Connect {server_username}@{server_host}#{ssh_port}...") + + except paramiko.ssh_exception.ChannelException: + message_emacs(f"Connect {server_username}@{server_host}:{ssh_port} failed, please make sure `lsp_bridge.py` has start at server.") + @threaded def save_remote_file(self, remote_file_host, remote_file_path): self.send_remote_file_message(remote_file_host, { @@ -682,6 +726,8 @@ def maybe_create_org_babel_server(self, filepath): def build_file_action_function(self, name): def _do(filepath, *args): + if is_remote_path(filepath): + return open_file_success = True if not is_in_path_dict(FILE_ACTION_DICT, filepath):