Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 Manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ uuid = "14a3606d-f60d-562e-9121-12d972cd8159"
[[NetworkOptions]]
uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"

[[PackageCompiler]]
deps = ["Libdl", "Pkg", "UUIDs"]
git-tree-sha1 = "d448727c4b86be81b225b738c88d30334fda6779"
uuid = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d"
version = "1.2.5"

[[Parsers]]
deps = ["Dates"]
git-tree-sha1 = "c8abc88faa3f7a3950832ac5d6e690881590d6dc"
Expand Down
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
[deps]
LanguageServer = "2b0e0bc5-e4fd-59b4-8912-456d1b03d8d7"
PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d"
SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce"
SymbolServer = "cf896787-08d5-524d-9de7-132aaa0cb996"

[compat]
Expand Down
28 changes: 28 additions & 0 deletions README.org
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,31 @@ displays:
#+begin_quote
[ Info: Received new data from Julia Symbol Server.
#+end_quote


*** Can I speed-up server start-up times?

By default, starting a new language server (which is done once per project)
takes some time: around 20 seconds are needed before the server is ready, which
manifests itself by the =[eglot:projectName]= indicator showing up in the
modeline.

A custom system image, created using [[https://github.com/JuliaLang/PackageCompiler.jl][=PackageCompiler.jl=]], can be used to reduce
this latency. The whole process of creating and using a system image can be
automated by setting the =eglot-jl-enable-sysimage= customization option in
Emacs:
#+begin_src elisp
(setq eglot-jl-enable-sysimage t)
#+end_src

When this setting is activated, you'll be prompted to start building a system
image the first time =eglot-jl= is initialized. You'll also be prompted to
re-build the sysimage each time this is necessary: when the Julia version
changes, or when eglot-jl itself gets updated.

It will take a few minutes to generate a system image, which =eglot-jl= will
detect and automatically use for subsequent language server runs. This should
reduce language server startup times to 1 or 2 seconds.

If anything goes wrong with system images, you can deactivate this feature
altogether by restoring =eglot-jl-enable-sysimage= back to its default value.
30 changes: 30 additions & 0 deletions compile.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Usage:
# julia path/to/eglot-jl/compile.jl [LANGUAGESERVER_PROJECT_DIR]

# Path to the LanguageServer project. In order of increasing priority:
# - path to eglot-jl: @__DIR__
# - command-line: ARGS[1]
dir = length(ARGS) >= 1 ? ARGS[1] : @__DIR__

using Pkg
Pkg.activate(dir)
include("utils.jl")
pkg_resolve()

# Get a suitable system image name
sysimage = sysimage_path(dir)

# This tells `eglot-jl.jl` to switch to TEST mode, in which the server
# immediately reads an `exit` command after having started
ENV["EGLOT_JL_TEST"] = "1"


@info "Creating system image" path = sysimage

using PackageCompiler
create_sysimage([:LanguageServer, :SymbolServer];
sysimage_path = sysimage,
precompile_execution_file = joinpath(@__DIR__, "eglot-jl.jl"))

@assert isfile(sysimage)
@info "Successfully generated system image" path = sysimage
60 changes: 57 additions & 3 deletions eglot-jl.el
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

;; Copyright (C) 2019 Adam Beckmeyer

;; Version: 2.1.1
;; Version: 2.2.0
;; Author: Adam Beckmeyer <adam_git@thebeckmeyers.xyz>
;; Maintainer: Adam Beckmeyer <adam_git@thebeckmeyers.xyz>
;; URL: https://github.com/non-Jedi/eglot-jl
Expand Down Expand Up @@ -60,6 +60,10 @@ The project should have LanguageServer and SymbolServer packages
available."
:type 'string)

(defcustom eglot-jl-enable-sysimage nil
"When non-nil, attempt to use a PackageCompiler sysimage."
:type 'boolean)

;; Make project.el aware of Julia projects
(defun eglot-jl--project-try (dir)
"Return project instance if DIR is part of a julia project.
Expand All @@ -71,11 +75,36 @@ Otherwise returns nil"
(cl-defmethod project-roots ((project (head julia)))
(list (cdr project)))

(defun eglot-jl--julia-project-args ()
"Return Julia command-line switch to activate the LanguageServer project."
(concat "--project=" eglot-jl-language-server-project))

(defun eglot-jl--julia-sysimage-args ()
"Return a list of command-line switches suitable for sysimage loading.
Return nil if no suitable system image has been found or the use
of system images has not been enabled altogether."
(and
eglot-jl-enable-sysimage
(let ((sysimage-path
(with-temp-buffer
(call-process eglot-jl-julia-command nil t nil
(eglot-jl--julia-project-args)
"--compile=min" "--optimize=0"
"--startup-file=no" "--color=no"
(expand-file-name "utils.jl" eglot-jl-base)
eglot-jl-language-server-project)
;; Get the last output line
(goto-char (point-max))
(buffer-substring-no-properties (line-beginning-position) (point)))))
(when (file-exists-p sysimage-path)
(list "--sysimage" sysimage-path)))))

(defun eglot-jl--ls-invocation (_interactive)
"Return list of strings to be called to start the Julia language server."
`(,eglot-jl-julia-command
"--startup-file=no"
,(concat "--project=" eglot-jl-language-server-project)
,(eglot-jl--julia-project-args)
,@(eglot-jl--julia-sysimage-args)
,@eglot-jl-julia-flags
,(expand-file-name "eglot-jl.jl" eglot-jl-base)
,(file-name-directory (buffer-file-name))
Expand All @@ -88,7 +117,32 @@ Otherwise returns nil"
(add-hook 'project-find-functions #'eglot-jl--project-try)
(add-to-list 'eglot-server-programs
;; function instead of strings to find project dir at runtime
'(julia-mode . eglot-jl--ls-invocation)))
'(julia-mode . eglot-jl--ls-invocation))
(when (and
eglot-jl-enable-sysimage
(not (eglot-jl--julia-sysimage-args))
(not (bound-and-true-p eglot-jl--compiling-sysimage))
(y-or-n-p "No suitable system image found for the Julia language server. Would you like to start compiling one now? "))
(eglot-jl-compile-sysimage)))

;;;###autoload
(defun eglot-jl-compile-sysimage ()
"Create a compiled system image for the language server.
In combination with `eglot-jl-enable-sysimage`, this reduces
subsequent start-up times."
(interactive)
(setq eglot-jl--compiling-sysimage t)
(let ((buffer (get-buffer-create "*eglot-jl sysimage compilation*")))
(with-current-buffer buffer
(view-mode)
(let ((inhibit-read-only t))
(erase-buffer))
(start-process "eglot-jl-compile-sysimage" (current-buffer)
eglot-jl-julia-command
"--startup-file=no" "--color=no" "--quiet"
(expand-file-name "compile.jl" eglot-jl-base)
eglot-jl-language-server-project))
(display-buffer buffer)))

(provide 'eglot-jl)
;;; eglot-jl.el ends here
37 changes: 20 additions & 17 deletions eglot-jl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,13 @@
# being available on LOAD_PATH
import Pkg

# Resolving the environment is necessary for cases where the shipped
# Manifest.toml is not compatible with the Julia version.
for _ in 1:2
try
Pkg.resolve(io=stderr)
@info "Environment successfully resolved"
break
catch err
# Downgrading from 1.6 to 1.5 sometimes causes temporary errors
@warn "Error while resolving the environment; retrying..." err
end
include("utils.jl")
if Base.JLOptions().image_file_specified == 1
@info "Using custom sysimage" path=unsafe_string(Base.JLOptions().image_file)
else
pkg_resolve()
end

# In julia 1.4 this operation takes under a second. This can be
# crushingly slow in older versions of julia though.
Pkg.instantiate()

# Get the source path. In order of increasing priority:
# - default value: pwd()
# - command-line: ARGS[1]
Expand All @@ -44,8 +34,21 @@ project_path = something(Base.current_project(src_path), Base.load_path_expand(L
empty!(LOAD_PATH)
push!(LOAD_PATH, "@")

using LanguageServer.JSON
if get(ENV, "EGLOT_JL_TEST", "0") == "1"
msg = Dict("jsonrpc" => "2.0",
"id" => 1,
"method" => "exit",
"params" => Dict()) |> JSON.json
input = IOBuffer("Content-Length: $(length(msg))\n\n$msg")
mode = "TEST"
else
mode = "RUN"
input = stdin
end

using LanguageServer, SymbolServer

@info "Running language server" env=Base.load_path()[1] src_path project_path depot_path
server = LanguageServerInstance(stdin, stdout, project_path, depot_path)
@info "Running language server" mode env=Base.load_path()[1] src_path project_path depot_path
server = LanguageServerInstance(input, stdout, project_path, depot_path)
run(server)
56 changes: 56 additions & 0 deletions utils.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import SHA
import Pkg

"""
pkg_resolve()

Resolve the currently active environment, in order to make sure it is compatible
with the Julia version in use.
"""
function pkg_resolve()
# Resolving the environment is necessary for cases where the shipped
# Manifest.toml is not compatible with the Julia version.
for _ in 1:2
try
Pkg.resolve(io=stderr)
@info "Environment successfully resolved"
break
catch err
# Downgrading from 1.6 to 1.5 sometimes causes temporary errors
@warn "Error while resolving the environment; retrying..." err
end
end

# In julia 1.4 this operation takes under a second. This can be
# crushingly slow in older versions of julia though.
Pkg.instantiate()
end

"""
sysimage_path(dir)

Return a suitable name for the system image of a project in `dir`. In order to
avoid problems when/if Julia or the project dependencies get updated, all
versions get encoded in the system image file name.
"""
function sysimage_path(dir)
ext = if Sys.iswindows()
"dll"
elseif Sys.isapple()
"dylib"
else
"so"
end

hash = open(SHA.sha1, joinpath(dir, "Manifest.toml")) |> bytes2hex

joinpath(dir, "eglot-jl_$(Base.VERSION)_$hash.$ext")
end


# When used as a script: resolve the environment and print a suitable sysimage
# name
if abspath(PROGRAM_FILE) == @__FILE__
pkg_resolve()
print(sysimage_path(ARGS[1]))
end