;;; Require
(require 'dbus)
(require 'cl-lib)
;;; Code:
(defcustom nox-buffer-name "*nox*"
"Name of Nox buffer."
:type 'string)
(defcustom nox-python-command "python3"
"The Python interpreter used to run nox.py."
:type 'string)
(defcustom nox-completion-delay 0.2
"The completion delay after change buffer, in millisecond."
:type 'float)
(defcustom nox-flash-line-delay .3
"How many seconds to flash `nox-font-lock-flash' after navigation.
Setting this to nil or 0 will turn off the indicator."
:type 'number)
(defface nox-font-lock-flash
'((t (:inherit highlight)))
"Face to flash the current line.")
(defvar nox-python-file (expand-file-name "nox.py" (file-name-directory load-file-name)))
(defvar nox--process nil)
(defvar nox--last-buffer nil)
(defvar nox--completion-item-kind-alist
'(
(1 . "text")
(2 . "method")
(3 . "fn")
(4 . "constructor")
(5 . "field")
(6 . "var")
(7 . "class")
(8 . "interface")
(9 . "module")
(10 . "property")
(11 . "unit")
(12 . "value")
(13 . "enum")
(14 . "keyword")
(15 . "snippet")
(16 . "color")
(17 . "file")
(18 . "reference")
(19 . "folder")
(20 . "enum member")
(21 . "constant")
(22 . "struct")
(23 . "event")
(24 . "operator")
(25 . "type parameter")
))
(defvar nox--language-alist
'(
("py" . ("python" . "python -m pyls"))
))
(defvar company-nox-keywords '())
(defun nox-start-server ()
"Start Nox server and init LSP client."
(interactive)
(if (process-live-p nox--process)
;; If nox server process has start, try init LSP client once.
(unless (boundp 'nox-init-lsp-client)
(nox-call "init_lsp_client" nox-language-id nox-language-cmd nox-root-uri)
(setq-local nox-init-lsp-client t))
;; Otherwise start server process and init LSP client.
(setq nox--process (apply #'start-process nox-buffer-name nox-buffer-name
nox-python-command
(list nox-python-file nox-language-id nox-language-cmd nox-root-uri)))
(message "Start Nox server...")
))
(defun nox-get-root-uri ()
"Get project uri or current file path.
Use for init LSP client."
(let ((project (project-current)))
(concat "file://"
(if project
(expand-file-name (cdr project))
(buffer-file-name)
))))
(defun nox-stop-server ()
"Stop Nox process."
(interactive)
;; Delete Nox buffer.
(when (get-buffer nox-buffer-name)
(kill-buffer nox-buffer-name))
;; Kill Nox process.
(if (process-live-p nox--process)
(progn
(delete-process nox--process)
(message "Nox process terminated."))
(message "Nox process has terminated.")))
(defun nox-mode-enable ()
"Enable nox mode."
(interactive)
;; Init local variables.
(unless (boundp 'nox-root-uri)
(setq-local nox-root-uri (nox-get-root-uri)))
(unless (boundp 'nox-language-id)
(let ((language-info (cdr (assoc (file-name-extension (buffer-file-name)) nox--language-alist))))
(setq-local nox-language-id (car language-info))
(setq-local nox-language-cmd (cdr language-info))))
;; Start Nox server.
(nox-start-server)
;; Add file change hook.
(add-hook 'post-command-hook #'nox-monitor-file-change t t))
(defun nox-monitor-file-change ()
"When user modify file, add some delay and try completion."
(when (buffer-modified-p)
(setq-local nox-change-id (nox-current-time))
(run-with-timer nox-completion-delay nil 'nox-try-completion)))
(defun nox-try-completion ()
"Try send completion request to LSP server."
;; If user type so fast than completion delay, cancel send completion request to LSP.
(when (< (+ nox-change-id (* nox-completion-delay 1000)) (nox-current-time))
(nox-call "completion" nox-language-id nox-root-uri (concat "file://" (buffer-file-name)) (format-mode-line "%l") (format-mode-line "%c") (char-before))
))
(defun nox-goto-define ()
(interactive)
(nox-call "goto_define" nox-language-id nox-root-uri (concat "file://" (buffer-file-name)) (format-mode-line "%l") (format-mode-line "%c")))
(defun nox-current-time ()
"Get current time, use to compare completion delay, in millisecond."
(* 1000 (time-to-seconds (current-time))))
(defun nox-call (method &rest args)
"Call NOX Python process using `dbus-call-method' with METHOD and ARGS."
(apply #'dbus-call-method
:session ; use the session (not system) bus
"com.lazycat.nox" ; service name
"/com/lazycat/nox" ; path name
"com.lazycat.nox" ; interface name
method
:timeout 1000000
args))
(dbus-register-signal
:session "com.lazycat.nox" "/com/lazycat/nox"
"com.lazycat.nox" "echo"
#'message)
(dbus-register-signal
:session "com.lazycat.nox" "/com/lazycat/nox"
"com.lazycat.nox" "popup_completion_menu"
#'nox--popup-completion-menu)
(defun nox--popup-completion-menu (items)
"Update completion candidates from popup_completion_menu signal."
(setq company-nox-keywords items))
(dbus-register-signal
:session "com.lazycat.nox" "/com/lazycat/nox"
"com.lazycat.nox" "open_define_position"
#'nox--open-define-position)
(defun nox--open-define-position (file start-row start-column end-row end-column)
(let ((pulse-iterations 1)
(pulse-delay nox-flash-line-delay)
start end)
;; Open file.
(find-file file)
;; Find end position.
(save-excursion
(goto-line end-row)
(nox-jump-to-column end-column)
(setq end (point)))
;; Jump to start position.
(goto-line start-row)
(nox-jump-to-column start-column)
(setq start (point))
;; Flash match line.
(pulse-momentary-highlight-region start end 'nox-font-lock-flash)
;; Message to user.
(message "Jump to the definition of %s" (symbol-at-point))
))
(defun nox-jump-to-column (column)
"This function use for jump to correct column positions in multi-byte strings.
Such as, mixed string of Chinese and English.
Function `move-to-column' can't handle mixed string of Chinese and English correctly."
(let ((scan-column 0)
(first-char-point (point)))
(while (> column scan-column)
(forward-char 1)
(setq scan-column (string-bytes (buffer-substring first-char-point (point)))))
(backward-char 1)))
(defun company-nox--make-candidate (candidate)
"Bulid candidate line."
(let ((text (nth 1 candidate))
(meta (format "[%s] %s"
(cdr (assoc (nth 0 candidate) nox--completion-item-kind-alist))
(nth 2 candidate))))
(propertize text 'meta meta)))
(defun company-nox--candidates (prefix)
"Filter candidates with user input prefix."
(let (res)
(dolist (item company-nox-keywords)
(when (string-prefix-p prefix (nth 1 item))
(push (company-nox--make-candidate item) res)))
res))
(defun company-nox--meta (candidate)
(format "%s of %s"
(get-text-property 0 'meta candidate)
(substring-no-properties candidate)))
(defun company-nox--annotation (candidate)
"Format candidate annotation."
(format " %s" (get-text-property 0 'meta candidate)))
(defun company-nox (command &optional arg &rest ignored)
"Company backend for Nox."
(interactive (list 'interactive))
(cl-case command
(interactive (company-begin-backend 'company-nox))
(prefix (company-grab-symbol-cons "\\.\\|->" 2))
(candidates (company-nox--candidates arg))
(annotation (company-nox--annotation arg))
(meta (company-nox--meta arg))))
(defun nox--monitor-buffer-change ()
"Start Nox mode if user switch to file that support in `nox--language-alist'."
(unless (eq (current-buffer)
nox--last-buffer)
;; Try start node.
(nox-try-start)
;; Switch buffer clean nox completion candidates.
(setq company-nox-keywords '())
;; Record last buffer.
(setq nox--last-buffer (current-buffer))))
(add-hook 'post-command-hook #'nox--monitor-buffer-change)
(defun nox-try-start ()
(when (and (buffer-file-name)
(assoc (file-name-extension (buffer-file-name)) nox--language-alist))
(nox-mode-enable)))
(nox-try-start)
(setq company-tooltip-align-annotations t)
(provide 'nox)
;;; nox.el ends here
from dbus.mainloop.glib import DBusGMainLoop
import dbus
import dbus.service
from gi.repository import GLib
import subprocess
import lsp
import os
import threading
NOX_DBUS_NAME = "com.lazycat.nox"
NOX_OBJECT_NAME = "/com/lazycat/nox"
class NOX(dbus.service.Object):
def __init__(self, args):
dbus.service.Object.__init__(
self,
dbus.service.BusName(NOX_DBUS_NAME, bus=dbus.SessionBus()),
NOX_OBJECT_NAME)
self.lsp_clients = {}
self.init_lsp_client(args[0], args[1], args[2])
@dbus.service.method(NOX_DBUS_NAME, in_signature="sss", out_signature="")
def init_lsp_client(self, language, command, root_uri):
if language not in self.lsp_clients or root_uri not in self.lsp_clients[language]:
p = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
read_pipe = lsp.ReadPipe(p.stderr)
read_pipe.start()
json_rpc_endpoint = lsp.JsonRpcEndpoint(p.stdin, p.stdout)
lsp_endpoint = lsp.LspEndpoint(json_rpc_endpoint)
lsp_client = lsp.LspClient(lsp_endpoint)
self.lsp_clients[language] = {}
self.lsp_clients[language][root_uri] = lsp_client
workspace_folders = [{'name': 'nox', 'uri': root_uri}]
lsp_client.server_capabilities = lsp_client.initialize(os.getppid(), None, root_uri, None, lsp.capabilities, "verbose", workspace_folders)
@dbus.service.method(NOX_DBUS_NAME, in_signature="ssssss", out_signature="")
def completion(self, language, root_uri, file_path, row, column, last_char):
# Use sub thread avoid completoin action block input.
threading.Thread(target=self.get_completion, args=(language, root_uri, file_path, row, column, last_char)).start()
def get_completion(self, language, root_uri, file_path, row, column, last_char):
if language in self.lsp_clients and root_uri in self.lsp_clients[language]:
# Init completion context.
context = lsp.CompletionContext(lsp.CompletionTriggerKind.Invoked)
# Change to TriggerCharacter type when last char is trigger char.
trigger_chars = self.lsp_clients[language][root_uri].server_capabilities["capabilities"]["completionProvider"]["triggerCharacters"]
is_trigger_char = chr(last_char) in trigger_chars
if is_trigger_char:
context = lsp.CompletionContext(lsp.CompletionTriggerKind.TriggerCharacter, ".")
items = self.lsp_clients[language][root_uri].completion(lsp.TextDocumentItem(file_path, language, 1, ""),
lsp.Position(int(row) - 1, int(column)),
context).items
if len(items) > 0:
candidates = []
if is_trigger_char:
candidates = list(map(lambda i: (i.kind, i.insertText, i.label),
filter(lambda x: "{}".format(x.detail) != "builtins", items)))
else:
candidates = list(map(lambda i: (i.kind, i.insertText, i.label), items))
self.popup_completion_menu(candidates)
@dbus.service.method(NOX_DBUS_NAME, in_signature="sssss", out_signature="")
def goto_define(self, language, root_uri, file_path, row, column):
if language in self.lsp_clients and root_uri in self.lsp_clients[language]:
location = self.lsp_clients[language][root_uri].definition(
lsp.TextDocumentItem(file_path, language, 1, ""),
lsp.Position(int(row) - 1, int(column)))
if len(location) > 0:
self.open_define_position(location[0].uri,
location[0].range.start.line + 1,
location[0].range.start.character + 1,
location[0].range.end.line + 1,
location[0].range.end.character + 1)
else:
self.echo("Can't found definition at cursor.")
@dbus.service.signal(NOX_DBUS_NAME)
def echo(self, message):
pass
@dbus.service.signal(NOX_DBUS_NAME)
def popup_completion_menu(self, items):
pass
@dbus.service.signal(NOX_DBUS_NAME)
def open_define_position(self, file, start_line, start_column, end_line, end_column):
pass
if __name__ == "__main__":
import sys
import signal
DBusGMainLoop(set_as_default=True) # WARING: only use once in one process
bus = dbus.SessionBus()
if bus.request_name(NOX_DBUS_NAME) != dbus.bus.REQUEST_NAME_REPLY_PRIMARY_OWNER:
print("NOX process is already running.")
else:
print("NOX process starting...")
NOX(sys.argv[1:])
GLib.MainLoop().run()
signal.signal(signal.SIGINT, signal.SIG_DFL)
sys.exit()