mirror of
https://github.com/Cian-H/dotfiles.git
synced 2025-12-22 19:31:57 +00:00
Removed micro plugin lsp (moving to submodule)
This commit is contained in:
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Robert Kunze
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,93 +0,0 @@
|
||||
# Micro Plugin LSP Client
|
||||
|
||||
**Please note:** This software is very much not finished. It is more like a
|
||||
proof of concept and might break if you call it names.
|
||||
|
||||
Provides LSP methods as actions to Micro that can subsequently be mapped to key
|
||||
bindings.
|
||||
|
||||
Currently implemented methods:
|
||||
|
||||
- textDocument/hover
|
||||
- textDocument/definition
|
||||
- textDocument/completion
|
||||
- textDocument/formatting
|
||||
- textDocument/references
|
||||
|
||||
If possible, this plugin will register the following shortcuts:
|
||||
|
||||
- Alt-k for hover
|
||||
- Alt-d for definition lookup
|
||||
- Alt-f for formatting
|
||||
- Alt-r for looking up references
|
||||
- Ctrl-space for completion
|
||||
|
||||
## Installation
|
||||
|
||||
Clone this repo into micro's plug folder:
|
||||
|
||||
```
|
||||
$ git clone https://github.com/AndCake/micro-plugin-lsp ~/.config/micro/plug/lsp
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
In your `settings.json`, you add the `lsp.server` option in order to enable
|
||||
using it for your languages' server.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
{
|
||||
"lsp.server": "python=pyls,go=gopls,typescript=deno lsp,rust=rls",
|
||||
"lsp.formatOnSave": true,
|
||||
"lsp.ignoreMessages": "LS message1 to ignore|LS message 2 to ignore|...",
|
||||
"lsp.tabcompletion": true,
|
||||
"lsp.ignoreTriggerCharacters": "completion,signature",
|
||||
"lsp.autocompleteDetails": false
|
||||
}
|
||||
```
|
||||
|
||||
The format for the `lsp.server` value is a comma-separated list for each file
|
||||
type you want to boot up a language server:
|
||||
|
||||
```
|
||||
<file type>=<executable with arguments where necessary>[=<initialization options passed to language server>][,...]
|
||||
```
|
||||
|
||||
You can also use an environment variable called `MICRO_LSP` to define the same
|
||||
information. If set, it will override the `lsp.server` from the `settings.json`.
|
||||
You can add a line such as the following to your shell profile (e.g. .bashrc):
|
||||
|
||||
```
|
||||
export MICRO_LSP='python=pyls,go=gopls,typescript=deno lsp={"importMap":"import_map.json"},rust=rls'
|
||||
```
|
||||
|
||||
If neither the MICRO_LSP nor the lsp.server is set, then the plugin falls back
|
||||
to the following settings:
|
||||
|
||||
```
|
||||
python=pylsp,go=gopls,typescript=deno lsp,javascript=deno lsp,markdown=deno lsp,json=deno lsp,jsonc=deno lsp,rust=rls,lua=lua-lsp,c++=clangd
|
||||
```
|
||||
|
||||
The option `lsp.autocompleteDetails` allows for showing all auto-completions in
|
||||
a horizontally split buffer view (true) instead of the status line (false).
|
||||
|
||||
## Testing
|
||||
|
||||
This plugin has been tested briefly with the following language servers:
|
||||
|
||||
- C++ [clangd](https://clangd.llvm.org) /
|
||||
[ccls](https://github.com/MaskRay/ccls)
|
||||
- go: [gopls](https://pkg.go.dev/golang.org/x/tools/gopls#section-readme)
|
||||
- markdown, JSON, typescript, javascript (including JSX/TSX):
|
||||
[deno](https://deno.land/)
|
||||
- python: pyls, [pylsp](https://github.com/python-lsp/python-lsp-server)
|
||||
- rust: [rls](https://github.com/rust-lang/rls)
|
||||
- lua: [lua-lsp](https://github.com/Alloyed/lua-lsp)
|
||||
- zig: [zls](https://github.com/zigtools/zls)
|
||||
|
||||
## Known issues
|
||||
|
||||
Not all possible types of modification events to the file are currently being
|
||||
sent to the language server. Saving the file will re-synchronize it, though.
|
||||
@@ -1,315 +0,0 @@
|
||||
# Micro Plugin LSP Client
|
||||
|
||||
LSP is a Language Server Protocol client. Features include function signatures
|
||||
and jump to definition.
|
||||
|
||||
This help page can be viewed in Micro editor with Ctrl-E 'help lsp'
|
||||
|
||||
## Features and Shortcuts
|
||||
|
||||
- Show function signature on status bar (alt-K) (textDocument/hover)
|
||||
- Open function definition in a new tab (alt-D) (textDocument/definition)
|
||||
- Format document (alt-F) (textDocument/formatting)
|
||||
- Show references to the current symbol in a buffer (alt-R)
|
||||
(textDocument/references), pressing return on the reference line, the
|
||||
reference's location is opened in a new tab
|
||||
|
||||
There is initial support for completion (ctrl-space) (textDocument/completion).
|
||||
|
||||
## Supported languages
|
||||
|
||||
Installation instructions for Go and Python are provided below. LSP Plugin has
|
||||
been briefly tested with
|
||||
|
||||
- C++: [clangd](https://clangd.llvm.org) /
|
||||
[ccls](https://github.com/MaskRay/ccls)
|
||||
- go: [gopls](https://pkg.go.dev/golang.org/x/tools/gopls#section-readme)
|
||||
- markdown, JSON, typescript, javascript (including JSX/TSX):
|
||||
[deno](https://deno.land/)
|
||||
- python: pyls and [pylsp](https://github.com/python-lsp/python-lsp-server)
|
||||
- rust: [rls](https://github.com/rust-lang/rls)
|
||||
- lua: [lua-lsp](https://github.com/Alloyed/lua-lsp)
|
||||
|
||||
## Install LSP plugin
|
||||
|
||||
$ micro --plugin install lsp
|
||||
|
||||
To configure the LSP Plugin, you can add two lines to settings.json
|
||||
|
||||
$ micro settings.json
|
||||
|
||||
Add lines
|
||||
|
||||
```json
|
||||
{
|
||||
"lsp.server": "python=pylsp,go=gopls,typescript=deno lsp={\"importMap\": \"./import_map.json\"}",
|
||||
"lsp.formatOnSave": true
|
||||
}
|
||||
```
|
||||
|
||||
Remember to add comma to previous line. Depending on the language server,
|
||||
automatic code formating can be quite opinionated. In that case, you can simply
|
||||
set lsp.formatOnSave to false.
|
||||
|
||||
For Python language server, the currently maintained fork is 'pylsp'. If you
|
||||
wish to use the Palantir version (last updated in 2020) instead, set
|
||||
"python=pyls" in lsp.server.
|
||||
|
||||
If your lsp.server settings are autoremoved, you can
|
||||
|
||||
$ export MICRO_LSP='python=pylsp,go=gopls,typescript=deno lsp={"importMap":"import_map.json"},rust=rls'
|
||||
|
||||
The lsp.server default settings (if no others are defined) are:
|
||||
|
||||
```
|
||||
python=pylsp,go=gopls,typescript=deno lsp,javascript=deno lsp,markdown=deno lsp,json=deno lsp,jsonc=deno lsp,rust=rls,lua=lua-lsp,c++=clangd
|
||||
```
|
||||
|
||||
## Install Language Server
|
||||
|
||||
To support each language, LSP plugin uses language servers. To use LSP plugin,
|
||||
you must install at least one language server.
|
||||
|
||||
If you want to quickly test LSP plugin, Go language server gopls is simple to
|
||||
install.
|
||||
|
||||
### gopls, Go language server
|
||||
|
||||
You will need command 'gopls'
|
||||
|
||||
$ gopls version
|
||||
golang.org/x/tools/gopls v0.7.3
|
||||
|
||||
In Debian, this is installed with
|
||||
|
||||
$ sudo apt-get update
|
||||
$ sudo apt-get -y install golang-go gopls
|
||||
|
||||
To test it, write a short go program
|
||||
|
||||
$ micro hello.go
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("hello world")
|
||||
}
|
||||
```
|
||||
|
||||
Move cursor over Println and press alt-k. The function signature is shown on the
|
||||
bottom of the screen, in Micro status bar. It shows you what parameters the
|
||||
function can take. The signature should look similar to this: "func
|
||||
fmt.Println(a ...interface{}) (n int, err error)Println formats using the
|
||||
default formats..."
|
||||
|
||||
Can you see the function signature with alt-k? If you can, you have succesfully
|
||||
installed Micro LSP plugin and GoPLS language server.
|
||||
|
||||
Keep your cursor over Println, and press alt-d. The file defining Println opens.
|
||||
In this case, it's fmt/print.go. As Go reference documentation is in code
|
||||
comments, this is very convenient. You can navigate between tabs with atl-,
|
||||
(alt-comma) and alt-. (alt - full stop). To close the tab, press Ctrl-Q.
|
||||
|
||||
### Markdown, JSON/JSONC, Typescript, Javascript
|
||||
|
||||
The Deno LSP server will provide full support for Typescript and Javascript.
|
||||
Additionally, it supports formatting for Markdown and JSON files. The
|
||||
installation of this is fairly straight forward:
|
||||
|
||||
On Mac/Linux:
|
||||
|
||||
$ curl -fsSL https://deno.land/install.sh | sh
|
||||
|
||||
On Powershell:
|
||||
|
||||
$ iwr https://deno.land/install.ps1 -useb | iex
|
||||
|
||||
### typescript-language-server
|
||||
|
||||
This LSP server will allow for Javascript as well as Typescript support. For
|
||||
using it, you first need to install it using NPM:
|
||||
|
||||
$ npm install -g typescript-language-server typescript
|
||||
|
||||
Once it has been installed, you can use it like so:
|
||||
|
||||
$ micro hello.js
|
||||
|
||||
Press ctrl-e and type in:
|
||||
|
||||
set lsp.server "typescript=typescript-language-server --stdio,javascript=typescript-language-server --stdio"
|
||||
|
||||
After you restarted micro, you can use the features for typescript and
|
||||
javascript accordingly.
|
||||
|
||||
### pylsp, Python language server
|
||||
|
||||
Installing Python language server PyLSP is a bit more involved.
|
||||
|
||||
You will need 'virtualenv' command to create virtual environments and 'pip' to
|
||||
install Python packages. You can also use one of the many other commands for
|
||||
keeping your 'pip' packages in order.
|
||||
|
||||
In Debian, these are installed with
|
||||
|
||||
$ sudo apt-get update
|
||||
$ sudo apt-get install python-pip virtualenv
|
||||
|
||||
Create a new virtual environment
|
||||
|
||||
$ mkdir somePythonProject; cd somePythonProject
|
||||
$ virtualenv -p python3 env/
|
||||
$ source env/bin/activate
|
||||
|
||||
Your prompt likely shows "(env)" to confirm you're inside your virtual
|
||||
environment.
|
||||
|
||||
List the packages you want installed.
|
||||
|
||||
$ micro requirements.txt
|
||||
|
||||
This list is to provide the most useful suggestions. If you would like to get a
|
||||
lot more opinionated advice, such as adding two empty lines between functions,
|
||||
you could use "python-lsp-server[all]". The mypy package provides optional
|
||||
static type checking. requirements.txt:
|
||||
|
||||
```
|
||||
python-lsp-server[rope,pyflakes,mccabe,pylsp-mypy]
|
||||
pylsp-mypy
|
||||
```
|
||||
|
||||
And actually install
|
||||
|
||||
$ pip install -r requirements.txt
|
||||
|
||||
No you can test your Python environment
|
||||
|
||||
$ micro hello.py
|
||||
|
||||
```python
|
||||
def helloWorld():
|
||||
return a
|
||||
```
|
||||
|
||||
Save with Ctrl-S. A red warning sign ">>" lists up in the gutter, on the left
|
||||
side of Micro. Move cursor to the line "return a". The status bar shows the
|
||||
warning: "undefined name 'a'". Well done, you have now installed Python LSP
|
||||
support for Micro.
|
||||
|
||||
MyPy provides optional static type setting. You can write normally, and type
|
||||
checking is ignored. You can define types for some functions, and you get
|
||||
automatic warnings for incorrect use of types. This is how types are marked:
|
||||
|
||||
```python
|
||||
def square(x: int) -> int:
|
||||
return x*x
|
||||
```
|
||||
|
||||
Depending on your project, taste and installed linters, pylsp sometimes shows
|
||||
warnings you would like to hide. Hiding messages is possible using
|
||||
lsp.ignoreMessages, explained in later in this help document.
|
||||
|
||||
### lua-lsp, Lua language server
|
||||
|
||||
These are the initial installation instructions. This installation will support
|
||||
linter messages in the gutter (on the left of editing area) and jump to
|
||||
definition inside the same file (alt-D). All LSP features are not yet supported
|
||||
with Lua.
|
||||
|
||||
Install 'luarocks' command using your package manager. For example, on Debian
|
||||
|
||||
$ sudo apt-get update
|
||||
$ sudo apt-get -y install luarocks
|
||||
|
||||
Use luarocks to install helper packages used by lua-lsp
|
||||
|
||||
$ sudo luarocks install luacheck
|
||||
$ sudo luarocks install Formatter
|
||||
$ sudo luarocks install lcf
|
||||
|
||||
Install lua-lsp, the Lua language server
|
||||
|
||||
$ sudo luarocks install --server=ssh://luarocks.org/dev lua-lsp
|
||||
|
||||
This command uses different URL from official lua-lsp instructions due to
|
||||
[a change in how packages are downloaded](https://github.com/Alloyed/lua-lsp/issues/45).
|
||||
This command uses ssh instead of http.
|
||||
|
||||
To test it, open a Lua file
|
||||
|
||||
$ micro $HOME/.config/micro/plug/lsp/main.lua
|
||||
|
||||
Can you see some linter warnings ">>" in the gutter? Can you jump to functions
|
||||
inside the same file with Alt-D? Well done, you've installed Lua LSP support for
|
||||
micro.
|
||||
|
||||
All features don't work yet with Lua LSP.
|
||||
|
||||
### zls, ZIG language server
|
||||
|
||||
The ZIG language server provides formatting, goto definition, auto-completion as
|
||||
well as hover and references. It can be installed by following
|
||||
[these instruction](https://github.com/zigtools/zls).
|
||||
|
||||
Once installed, open micro, press ctrl+e and type the following command:
|
||||
|
||||
set lsp.server zig=zls
|
||||
|
||||
Close micro again and open a zig file.
|
||||
|
||||
## Ignoring unhelpful messages
|
||||
|
||||
In addition to providing assistance while coding, some language servers can show
|
||||
spurious, unnecessary or too oppinionated messages. Sometimes, it's not obvious
|
||||
how these messages are disable using language server settings.
|
||||
|
||||
This plugin allows you to selectively ignore unwanted warnings while keeping
|
||||
others. This is done my matching the start of the message. By default, nothing
|
||||
is ignored.
|
||||
|
||||
Consider a case where you're working with an external Python project that
|
||||
indents with tabs. When joining an existing project, you might not want to
|
||||
impose your own conventions to every code file. On the other hand, LSP support
|
||||
is not useful if nearly every line is marked with a warning.
|
||||
|
||||
Moving the cursor to a line with the warning, you see that the line starts with
|
||||
"W191 indentation contains tabs". This, and similar unhelpful messages (in the
|
||||
context of your current project) can be ignored by editing
|
||||
~/.config/micro/settings.json
|
||||
|
||||
```json
|
||||
{
|
||||
"lsp.ignoreMessages": "Skipping analyzing |W191 indentation contains tabs|E101 indentation contains mixed spaces and tabs|See https://mypy.readthedocs.io/en"
|
||||
}
|
||||
```
|
||||
|
||||
As you now open the same file, you can see that warning "W191 indentation
|
||||
contains tabs" is no longer shown. Also the warning mark ">>" in the gutter is
|
||||
gone. Try referring to a variable that does not exist, and you can see a helpful
|
||||
warning appear. You have now disabled the warnings you don't need, while keeping
|
||||
the useful ones.
|
||||
|
||||
## See also
|
||||
|
||||
[Official repostory](https://github.com/AndCake/micro-plugin-lsp)
|
||||
|
||||
[Usage examples with screenshots](https://terokarvinen.com/2022/micro-editor-lsp-support-python-and-go-jump-to-definition-show-function-signature/)
|
||||
|
||||
[Language Server Protocol](https://microsoft.github.io/language-server-protocol/)
|
||||
|
||||
[gopls - the Go language server](https://pkg.go.dev/golang.org/x/tools/gopls)
|
||||
|
||||
[pylsp - Python LSP Server](https://github.com/python-lsp/python-lsp-server)
|
||||
|
||||
[mypy - Optional Static Typing for Python](http://mypy-lang.org/)
|
||||
|
||||
[rls - Rust Language Server](https://github.com/rust-lang/rls)
|
||||
|
||||
[deno](https://deno.land/)
|
||||
|
||||
[typescript-language-server](https://www.npmjs.com/package/typescript-language-server)
|
||||
|
||||
[lua-lsp - A Lua language server](https://github.com/Alloyed/lua-lsp)
|
||||
@@ -1,933 +0,0 @@
|
||||
VERSION = "0.6.2"
|
||||
|
||||
local micro = import("micro")
|
||||
local config = import("micro/config")
|
||||
local shell = import("micro/shell")
|
||||
local util = import("micro/util")
|
||||
local buffer = import("micro/buffer")
|
||||
local fmt = import("fmt")
|
||||
local go_os = import("os")
|
||||
local path = import("path")
|
||||
local filepath = import("path/filepath")
|
||||
|
||||
local cmd = {}
|
||||
local id = {}
|
||||
local version = {}
|
||||
local currentAction = {}
|
||||
local capabilities = {}
|
||||
local filetype = ''
|
||||
local rootUri = ''
|
||||
local message = ''
|
||||
local completionCursor = 0
|
||||
local lastCompletion = {}
|
||||
local splitBP = nil
|
||||
local tabCount = 0
|
||||
|
||||
local json = {}
|
||||
|
||||
function toBytes(str)
|
||||
local result = {}
|
||||
for i=1,#str do
|
||||
local b = str:byte(i)
|
||||
if b < 32 then
|
||||
table.insert(result, b)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function getUriFromBuf(buf)
|
||||
if buf == nil then return; end
|
||||
local file = buf.AbsPath
|
||||
local uri = fmt.Sprintf("file://%s", file)
|
||||
return uri
|
||||
end
|
||||
|
||||
function mysplit (inputstr, sep)
|
||||
if sep == nil then
|
||||
sep = "%s"
|
||||
end
|
||||
local t={}
|
||||
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
|
||||
table.insert(t, str)
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
function parseOptions(inputstr)
|
||||
local t = {}
|
||||
inputstr = inputstr:gsub("[%w+_-]+=[^=,]+={.-}", function (str)
|
||||
table.insert(t, str)
|
||||
return '';
|
||||
end)
|
||||
inputstr = inputstr:gsub("[%w+_-]+=[^=,]+", function (str)
|
||||
table.insert(t, str)
|
||||
return '';
|
||||
end)
|
||||
return t
|
||||
end
|
||||
|
||||
function startServer(filetype, callback)
|
||||
local wd, _ = go_os.Getwd()
|
||||
rootUri = fmt.Sprintf("file://%s", wd)
|
||||
local envSettings, _ = go_os.Getenv("MICRO_LSP")
|
||||
local settings = config.GetGlobalOption("lsp.server")
|
||||
local fallback = "python=pylsp,go=gopls,typescript=deno lsp,javascript=deno lsp,markdown=deno lsp,json=deno lsp,jsonc=deno lsp,rust=rls,lua=lua-lsp,c++=clangd"
|
||||
if envSettings ~= nil and #envSettings > 0 then
|
||||
settings = envSettings
|
||||
end
|
||||
if settings ~= nil and #settings > 0 then
|
||||
settings = settings .. "," .. fallback
|
||||
else
|
||||
settings = fallback
|
||||
end
|
||||
local server = parseOptions(settings)
|
||||
micro.Log("Server Options", server)
|
||||
for i in pairs(server) do
|
||||
local part = mysplit(server[i], "=")
|
||||
local run = mysplit(part[2], "%s")
|
||||
local initOptions = part[3] or '{}'
|
||||
local runCmd = table.remove(run, 1)
|
||||
local args = run
|
||||
if filetype == part[1] then
|
||||
local send = withSend(part[1])
|
||||
if cmd[part[1]] ~= nil then return; end
|
||||
id[part[1]] = 0
|
||||
micro.Log("Starting server", part[1])
|
||||
cmd[part[1]] = shell.JobSpawn(runCmd, args, onStdout(part[1]), onStderr, onExit(part[1]), {})
|
||||
currentAction[part[1]] = { method = "initialize", response = function (bp, data)
|
||||
send("initialized", "{}", true)
|
||||
capabilities[filetype] = data.result and data.result.capabilities or {}
|
||||
callback(bp.Buf, filetype)
|
||||
end }
|
||||
send(currentAction[part[1]].method, fmt.Sprintf('{"processId": %.0f, "rootUri": "%s", "workspaceFolders": [{"name": "root", "uri": "%s"}], "initializationOptions": %s, "capabilities": {"textDocument": {"hover": {"contentFormat": ["plaintext", "markdown"]}, "publishDiagnostics": {"relatedInformation": false, "versionSupport": false, "codeDescriptionSupport": true, "dataSupport": true}, "signatureHelp": {"signatureInformation": {"documentationFormat": ["plaintext", "markdown"]}}}}}', go_os.Getpid(), rootUri, rootUri, initOptions))
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function init()
|
||||
config.RegisterCommonOption("lsp", "server", "python=pylsp,go=gopls,typescript=deno lsp,javascript=deno lsp,markdown=deno lsp,json=deno lsp,jsonc=deno lsp,rust=rls,lua=lua-lsp,c++=clangd")
|
||||
config.RegisterCommonOption("lsp", "formatOnSave", true)
|
||||
config.RegisterCommonOption("lsp", "autocompleteDetails", false)
|
||||
config.RegisterCommonOption("lsp", "ignoreMessages", "")
|
||||
config.RegisterCommonOption("lsp", "tabcompletion", true)
|
||||
config.RegisterCommonOption("lsp", "ignoreTriggerCharacters", "completion")
|
||||
-- example to ignore all LSP server message starting with these strings:
|
||||
-- "lsp.ignoreMessages": "Skipping analyzing |See https://"
|
||||
|
||||
config.MakeCommand("hover", hoverAction, config.NoComplete)
|
||||
config.MakeCommand("definition", definitionAction, config.NoComplete)
|
||||
config.MakeCommand("lspcompletion", completionAction, config.NoComplete)
|
||||
config.MakeCommand("format", formatAction, config.NoComplete)
|
||||
config.MakeCommand("references", referencesAction, config.NoComplete)
|
||||
|
||||
config.TryBindKey("Alt-k", "command:hover", false)
|
||||
config.TryBindKey("Alt-d", "command:definition", false)
|
||||
config.TryBindKey("Alt-f", "command:format", false)
|
||||
config.TryBindKey("Alt-r", "command:references", false)
|
||||
config.TryBindKey("CtrlSpace", "command:lspcompletion", false)
|
||||
|
||||
config.AddRuntimeFile("lsp", config.RTHelp, "help/lsp.md")
|
||||
|
||||
-- @TODO register additional actions here
|
||||
end
|
||||
|
||||
function withSend(filetype)
|
||||
return function (method, params, isNotification)
|
||||
if cmd[filetype] == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local msg = fmt.Sprintf('{"jsonrpc": "2.0", %s"method": "%s", "params": %s}', not isNotification and fmt.Sprintf('"id": %.0f, ', id[filetype]) or "", method, params)
|
||||
id[filetype] = id[filetype] + 1
|
||||
msg = fmt.Sprintf("Content-Length: %.0f\r\n\r\n%s", #msg, msg)
|
||||
--micro.Log("send", filetype, "sending", method or msg, msg)
|
||||
shell.JobSend(cmd[filetype], msg)
|
||||
end
|
||||
end
|
||||
|
||||
function preRune(bp, r)
|
||||
if splitBP ~= nil then
|
||||
pcall(function () splitBP:Unsplit(); end)
|
||||
splitBP = nil
|
||||
local cur = bp.Buf:GetActiveCursor()
|
||||
cur:Deselect(false);
|
||||
cur:GotoLoc(buffer.Loc(cur.X + 1, cur.Y))
|
||||
end
|
||||
end
|
||||
|
||||
-- when a new character is types, the document changes
|
||||
function onRune(bp, r)
|
||||
local filetype = bp.Buf:FileType()
|
||||
if cmd[filetype] == nil then
|
||||
return
|
||||
end
|
||||
if splitBP ~= nil then
|
||||
pcall(function () splitBP:Unsplit(); end)
|
||||
splitBP = nil
|
||||
end
|
||||
|
||||
local send = withSend(filetype)
|
||||
local uri = getUriFromBuf(bp.Buf)
|
||||
if r ~= nil then
|
||||
lastCompletion = {}
|
||||
end
|
||||
-- allow the document contents to be escaped properly for the JSON string
|
||||
local content = util.String(bp.Buf:Bytes()):gsub("\\", "\\\\"):gsub("\n", "\\n"):gsub("\r", "\\r"):gsub('"', '\\"'):gsub("\t", "\\t")
|
||||
-- increase change version
|
||||
version[uri] = (version[uri] or 0) + 1
|
||||
send("textDocument/didChange", fmt.Sprintf('{"textDocument": {"version": %.0f, "uri": "%s"}, "contentChanges": [{"text": "%s"}]}', version[uri], uri, content), true)
|
||||
local ignored = mysplit(config.GetGlobalOption("lsp.ignoreTriggerCharacters") or '', ",")
|
||||
if r and capabilities[filetype] then
|
||||
if not contains(ignored, "completion") and capabilities[filetype].completionProvider and capabilities[filetype].completionProvider.triggerCharacters and contains(capabilities[filetype].completionProvider.triggerCharacters, r) then
|
||||
completionAction(bp)
|
||||
elseif not contains(ignored, "signature") and capabilities[filetype].signatureHelpProvider and capabilities[filetype].signatureHelpProvider.triggerCharacters and contains(capabilities[filetype].signatureHelpProvider.triggerCharacters, r) then
|
||||
hoverAction(bp)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- alias functions for any kind of change to the document
|
||||
-- @TODO: add missing ones
|
||||
function onBackspace(bp) onRune(bp); end
|
||||
function onCut(bp) onRune(bp); end
|
||||
function onCutLine(bp) onRune(bp); end
|
||||
function onDuplicateLine(bp) onRune(bp); end
|
||||
function onDeleteLine(bp) onRune(bp); end
|
||||
function onDelete(bp) onRune(bp); end
|
||||
function onUndo(bp) onRune(bp); end
|
||||
function onRedo(bp) onRune(bp); end
|
||||
function onIndent(bp) onRune(bp); end
|
||||
function onIndentSelection(bp) onRune(bp); end
|
||||
function onPaste(bp) onRune(bp); end
|
||||
function onSave(bp) onRune(bp); end
|
||||
|
||||
function onEscape(bp)
|
||||
if splitBP ~= nil then
|
||||
pcall(function () splitBP:Unsplit(); end)
|
||||
splitBP = nil
|
||||
end
|
||||
end
|
||||
|
||||
function preInsertNewline(bp)
|
||||
if bp.Buf.Path == "References found" then
|
||||
local cur = bp.Buf:GetActiveCursor()
|
||||
cur:SelectLine()
|
||||
local data = util.String(cur:GetSelection())
|
||||
local file, line, character = data:match("(./[^:]+):([^:]+):([^:]+)")
|
||||
local doc, _ = file:gsub("^file://", "")
|
||||
buf, _ = buffer.NewBufferFromFile(doc)
|
||||
bp:AddTab()
|
||||
micro.CurPane():OpenBuffer(buf)
|
||||
buf:GetActiveCursor():GotoLoc(buffer.Loc(character * 1, line * 1))
|
||||
micro.CurPane():Center()
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function preSave(bp)
|
||||
if config.GetGlobalOption("lsp.formatOnSave") then
|
||||
onRune(bp)
|
||||
formatAction(bp, function ()
|
||||
bp:Save()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function handleInitialized(buf, filetype)
|
||||
if cmd[filetype] == nil then return; end
|
||||
micro.Log("Found running lsp server for ", filetype, "firing textDocument/didOpen...")
|
||||
local send = withSend(filetype)
|
||||
local uri = getUriFromBuf(buf)
|
||||
local content = util.String(buf:Bytes()):gsub("\\", "\\\\"):gsub("\n", "\\n"):gsub("\r", "\\r"):gsub('"', '\\"'):gsub("\t", "\\t")
|
||||
send("textDocument/didOpen", fmt.Sprintf('{"textDocument": {"uri": "%s", "languageId": "%s", "version": 1, "text": "%s"}}', uri, filetype, content), true)
|
||||
end
|
||||
|
||||
function onBufferOpen(buf)
|
||||
local filetype = buf:FileType()
|
||||
micro.Log("ONBUFFEROPEN", filetype)
|
||||
if filetype ~= "unknown" and rootUri == "" and not cmd[filetype] then return startServer(filetype, handleInitialized); end
|
||||
if cmd[filetype] then
|
||||
handleInitialized(buf, filetype)
|
||||
end
|
||||
end
|
||||
|
||||
function contains(list, x)
|
||||
for _, v in pairs(list) do
|
||||
if v == x then return true; end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function string.starts(String, Start)
|
||||
return string.sub(String, 1, #Start) == Start
|
||||
end
|
||||
|
||||
function string.ends(String, End)
|
||||
return string.sub(String, #String - (#End - 1), #String) == End
|
||||
end
|
||||
|
||||
function string.random(CharSet, Length, prefix)
|
||||
|
||||
local _CharSet = CharSet or '.'
|
||||
|
||||
if _CharSet == '' then
|
||||
return ''
|
||||
else
|
||||
local Result = prefix or ""
|
||||
math.randomseed(os.time())
|
||||
for Loop = 1,Length do
|
||||
local char = math.random(1, #CharSet)
|
||||
Result = Result .. CharSet:sub(char,char)
|
||||
end
|
||||
|
||||
return Result
|
||||
end
|
||||
end
|
||||
|
||||
function string.parse(text)
|
||||
if not text:find('"jsonrpc":') then return {}; end
|
||||
local start,fin = text:find("\n%s*\n")
|
||||
local cleanedText = text
|
||||
if fin ~= nil then
|
||||
cleanedText = text:sub(fin)
|
||||
end
|
||||
local status, res = pcall(json.parse, cleanedText)
|
||||
if status then
|
||||
return res
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function isIgnoredMessage(msg)
|
||||
-- Return true if msg matches one of the ignored starts of messages
|
||||
-- Useful for linters that show spurious, hard to disable warnings
|
||||
local ignoreList = mysplit(config.GetGlobalOption("lsp.ignoreMessages"), "|")
|
||||
for i, ignore in pairs(ignoreList) do
|
||||
if string.match(msg, ignore) then -- match from start of string
|
||||
micro.Log("Ignore message: '", msg, "', because it matched: '", ignore, "'.")
|
||||
return true -- ignore this message, dont show to user
|
||||
end
|
||||
end
|
||||
return false -- show this message to user
|
||||
end
|
||||
|
||||
function onStdout(filetype)
|
||||
return function (text)
|
||||
if text:starts("Content-Length:") then
|
||||
message = text
|
||||
else
|
||||
message = message .. text
|
||||
end
|
||||
if not text:ends("}") then
|
||||
return
|
||||
end
|
||||
local data = message:parse()
|
||||
if data == false then
|
||||
return
|
||||
end
|
||||
|
||||
if data.method == "workspace/configuration" then
|
||||
-- actually needs to respond with the same ID as the received JSON
|
||||
local message = fmt.Sprintf('{"jsonrpc": "2.0", "id": %.0f, "result": [{"enable": true}]}', data.id)
|
||||
shell.JobSend(cmd[filetype], fmt.Sprintf('Content-Length: %.0f\n\n%s', #message, message))
|
||||
elseif data.method == "textDocument/publishDiagnostics" or data.method == "textDocument\\/publishDiagnostics" then
|
||||
-- react to server-published event
|
||||
local bp = micro.CurPane().Buf
|
||||
bp:ClearMessages("lsp")
|
||||
bp:AddMessage(buffer.NewMessage("lsp", "", buffer.Loc(0, 10000000), buffer.Loc(0, 10000000), buffer.MTInfo))
|
||||
local uri = getUriFromBuf(bp)
|
||||
if data.params.uri == uri then
|
||||
for _, diagnostic in ipairs(data.params.diagnostics) do
|
||||
local type = buffer.MTInfo
|
||||
if diagnostic.severity == 1 then
|
||||
type = buffer.MTError
|
||||
elseif diagnostic.severity == 2 then
|
||||
type = buffer.MTWarning
|
||||
end
|
||||
local mstart = buffer.Loc(diagnostic.range.start.character, diagnostic.range.start.line)
|
||||
local mend = buffer.Loc(diagnostic.range["end"].character, diagnostic.range["end"].line)
|
||||
|
||||
if not isIgnoredMessage(diagnostic.message) then
|
||||
msg = buffer.NewMessage("lsp", diagnostic.message, mstart, mend, type)
|
||||
bp:AddMessage(msg)
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif currentAction[filetype] and currentAction[filetype].method and not data.method and currentAction[filetype].response and data.jsonrpc then -- react to custom action event
|
||||
local bp = micro.CurPane()
|
||||
micro.Log("Received message for ", filetype, data)
|
||||
currentAction[filetype].response(bp, data)
|
||||
currentAction[filetype] = {}
|
||||
elseif data.method == "window/showMessage" or data.method == "window\\/showMessage" then
|
||||
if filetype == micro.CurPane().Buf:FileType() then
|
||||
micro.InfoBar():Message(data.params.message)
|
||||
else
|
||||
micro.Log(filetype .. " message " .. data.params.message)
|
||||
end
|
||||
elseif data.method == "window/logMessage" or data.method == "window\\/logMessage" then
|
||||
micro.Log(data.params.message)
|
||||
elseif message:starts("Content-Length:") then
|
||||
if message:find('"') and not message:find('"result":null') then
|
||||
micro.Log("Unhandled message 1", filetype, message)
|
||||
end
|
||||
else
|
||||
-- enable for debugging purposes
|
||||
micro.Log("Unhandled message 2", filetype, message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function onStderr(text)
|
||||
micro.Log("ONSTDERR", text)
|
||||
--micro.InfoBar():Message(text)
|
||||
end
|
||||
|
||||
function onExit(filetype)
|
||||
return function (str)
|
||||
currentAction[filetype] = nil
|
||||
cmd[filetype] = nil
|
||||
micro.Log("ONEXIT", filetype, str)
|
||||
end
|
||||
end
|
||||
|
||||
-- the actual hover action request and response
|
||||
-- the hoverActionResponse is hooked up in
|
||||
function hoverAction(bp)
|
||||
local filetype = bp.Buf:FileType()
|
||||
if cmd[filetype] ~= nil then
|
||||
local send = withSend(filetype)
|
||||
local file = bp.Buf.AbsPath
|
||||
local line = bp.Buf:GetActiveCursor().Y
|
||||
local char = bp.Buf:GetActiveCursor().X
|
||||
currentAction[filetype] = { method = "textDocument/hover", response = hoverActionResponse }
|
||||
send(currentAction[filetype].method, fmt.Sprintf('{"textDocument": {"uri": "file://%s"}, "position": {"line": %.0f, "character": %.0f}}', file, line, char))
|
||||
end
|
||||
end
|
||||
|
||||
function hoverActionResponse(buf, data)
|
||||
if data.result and data.result.contents ~= nil and data.result.contents ~= "" then
|
||||
if data.result.contents.value then
|
||||
micro.InfoBar():Message(data.result.contents.value)
|
||||
elseif #data.result.contents > 0 then
|
||||
micro.InfoBar():Message(data.result.contents[1].value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- the definition action request and response
|
||||
function definitionAction(bp)
|
||||
local filetype = bp.Buf:FileType()
|
||||
if cmd[filetype] == nil then return; end
|
||||
|
||||
local send = withSend(filetype)
|
||||
local file = bp.Buf.AbsPath
|
||||
local line = bp.Buf:GetActiveCursor().Y
|
||||
local char = bp.Buf:GetActiveCursor().X
|
||||
currentAction[filetype] = { method = "textDocument/definition", response = definitionActionResponse }
|
||||
send(currentAction[filetype].method, fmt.Sprintf('{"textDocument": {"uri": "file://%s"}, "position": {"line": %.0f, "character": %.0f}}', file, line, char))
|
||||
end
|
||||
|
||||
function definitionActionResponse(bp, data)
|
||||
local results = data.result or data.partialResult
|
||||
if results == nil then return; end
|
||||
local file = bp.Buf.AbsPath
|
||||
if results.uri ~= nil then
|
||||
-- single result
|
||||
results = { results }
|
||||
end
|
||||
if #results <= 0 then return; end
|
||||
local uri = (results[1].uri or results[1].targetUri)
|
||||
local doc = uri:gsub("^file://", "")
|
||||
local buf = bp.Buf
|
||||
if file ~= doc then
|
||||
-- it's from a different file, so open it as a new tab
|
||||
buf, _ = buffer.NewBufferFromFile(doc)
|
||||
bp:AddTab()
|
||||
micro.CurPane():OpenBuffer(buf)
|
||||
-- shorten the displayed name in status bar
|
||||
name = buf:GetName()
|
||||
local wd, _ = go_os.Getwd()
|
||||
if name:starts(wd) then
|
||||
buf:SetName("." .. name:sub(#wd + 1, #name + 1))
|
||||
else
|
||||
if #name > 30 then
|
||||
buf:SetName("..." .. name:sub(-30, #name + 1))
|
||||
end
|
||||
end
|
||||
end
|
||||
local range = results[1].range or results[1].targetSelectionRange
|
||||
buf:GetActiveCursor():GotoLoc(buffer.Loc(range.start.character, range.start.line))
|
||||
bp:Center()
|
||||
end
|
||||
|
||||
function completionAction(bp)
|
||||
local filetype = bp.Buf:FileType()
|
||||
local send = withSend(filetype)
|
||||
local file = bp.Buf.AbsPath
|
||||
local line = bp.Buf:GetActiveCursor().Y
|
||||
local char = bp.Buf:GetActiveCursor().X
|
||||
|
||||
if lastCompletion[1] == file and lastCompletion[2] == line and lastCompletion[3] == char then
|
||||
completionCursor = completionCursor + 1
|
||||
else
|
||||
completionCursor = 0
|
||||
if bp.Cursor:HasSelection() then
|
||||
-- we have a selection
|
||||
-- assume we want to indent the selection
|
||||
bp:IndentSelection()
|
||||
return
|
||||
end
|
||||
if char == 0 then
|
||||
-- we are at the very first character of a line
|
||||
-- assume we want to indent
|
||||
bp:IndentLine()
|
||||
return
|
||||
end
|
||||
local cur = bp.Buf:GetActiveCursor()
|
||||
cur:SelectLine()
|
||||
local lineContent = util.String(cur:GetSelection())
|
||||
cur:ResetSelection()
|
||||
cur:GotoLoc(buffer.Loc(char, line))
|
||||
local startOfLine = "" .. lineContent:sub(1, char)
|
||||
if startOfLine:match("^%s+$") then
|
||||
-- we are at the beginning of a line
|
||||
-- assume we want to indent the line
|
||||
bp:IndentLine()
|
||||
return
|
||||
end
|
||||
end
|
||||
if cmd[filetype] == nil then return; end
|
||||
lastCompletion = {file, line, char}
|
||||
currentAction[filetype] = { method = "textDocument/completion", response = completionActionResponse }
|
||||
send(currentAction[filetype].method, fmt.Sprintf('{"textDocument": {"uri": "file://%s"}, "position": {"line": %.0f, "character": %.0f}}', file, line, char))
|
||||
end
|
||||
|
||||
table.filter = function(t, filterIter)
|
||||
local out = {}
|
||||
|
||||
for k, v in pairs(t) do
|
||||
if filterIter(v, k, t) then table.insert(out, v) end
|
||||
end
|
||||
|
||||
return out
|
||||
end
|
||||
|
||||
function findCommon(input, list)
|
||||
local commonLen = 0
|
||||
local prefixList = {}
|
||||
local str = input.textEdit and input.textEdit.newText or input.label
|
||||
for i = 1,#str,1 do
|
||||
local prefix = str:sub(1, i)
|
||||
prefixList[prefix] = 0
|
||||
for idx, entry in ipairs(list) do
|
||||
local currentEntry = entry.textEdit and entry.textEdit.newText or entry.label
|
||||
if currentEntry:starts(prefix) then
|
||||
prefixList[prefix] = prefixList[prefix] + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
local longest = ""
|
||||
for idx, entry in pairs(prefixList) do
|
||||
if entry >= #list then
|
||||
if #longest < #idx then
|
||||
longest = idx
|
||||
end
|
||||
end
|
||||
end
|
||||
if #list == 1 then
|
||||
return list[1].textEdit and list[1].textEdit.newText or list[1].label
|
||||
end
|
||||
return longest
|
||||
end
|
||||
|
||||
function completionActionResponse(bp, data)
|
||||
local results = data.result
|
||||
if results == nil then
|
||||
return
|
||||
end
|
||||
if results.items then
|
||||
results = results.items
|
||||
end
|
||||
|
||||
local xy = buffer.Loc(bp.Cursor.X, bp.Cursor.Y)
|
||||
local start = xy
|
||||
if bp.Cursor:HasSelection() then
|
||||
bp.Cursor:DeleteSelection()
|
||||
end
|
||||
|
||||
local found = false
|
||||
local prefix = ""
|
||||
local reversed = ""
|
||||
-- if we have no defined ranges in the result
|
||||
-- try to find out what our prefix is we want to filter against
|
||||
if not results[1] or not results[1].textEdit or not results[1].textEdit.range then
|
||||
if capabilities[bp.Buf:FileType()] and capabilities[bp.Buf:FileType()].completionProvider and capabilities[bp.Buf:FileType()].completionProvider.triggerCharacters then
|
||||
local cur = bp.Buf:GetActiveCursor()
|
||||
cur:SelectLine()
|
||||
local lineContent = util.String(cur:GetSelection())
|
||||
reversed = string.reverse(lineContent:gsub("\r?\n$", ""):sub(1, xy.X))
|
||||
local triggerChars = capabilities[bp.Buf:FileType()].completionProvider.triggerCharacters
|
||||
for i = 1,#reversed,1 do
|
||||
local char = reversed:sub(i,i)
|
||||
-- try to find a trigger character or any other non-word character
|
||||
if contains(triggerChars, char) or contains({" ", ":", "/", "-", "\t", ";"}, char) then
|
||||
found = true
|
||||
start = buffer.Loc(#reversed - (i - 1), bp.Cursor.Y)
|
||||
bp.Cursor:SetSelectionStart(start)
|
||||
bp.Cursor:SetSelectionEnd(xy)
|
||||
prefix = util.String(cur:GetSelection())
|
||||
bp.Cursor:DeleteSelection()
|
||||
bp.Cursor:ResetSelection()
|
||||
break
|
||||
end
|
||||
end
|
||||
if not found then
|
||||
prefix = lineContent:gsub("\r?\n$", '')
|
||||
end
|
||||
end
|
||||
-- if we have found a prefix
|
||||
if prefix ~= "" then
|
||||
-- filter it down to what is suggested by the prefix
|
||||
results = table.filter(results, function (entry)
|
||||
return entry.label:starts(prefix)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(results, function (left, right)
|
||||
return (left.sortText or left.label) < (right.sortText or right.label)
|
||||
end)
|
||||
|
||||
entry = results[(completionCursor % #results) + 1]
|
||||
-- if no matching results are found
|
||||
if entry == nil then
|
||||
-- reposition cursor and stop
|
||||
bp.Cursor:GotoLoc(xy)
|
||||
return
|
||||
end
|
||||
local commonStart = ''
|
||||
local toInsert = entry.textEdit and entry.textEdit.newText or entry.label
|
||||
local isTabCompletion = config.GetGlobalOption("lsp.tabcompletion")
|
||||
if isTabCompletion and not entry.textEdit then
|
||||
commonStart = findCommon(entry, results)
|
||||
bp.Buf:Insert(start, commonStart)
|
||||
if prefix ~= commonStart then
|
||||
return
|
||||
end
|
||||
start = buffer.Loc(start.X + #prefix, start.Y)
|
||||
else
|
||||
prefix = ''
|
||||
end
|
||||
|
||||
if entry.textEdit and entry.textEdit.range then
|
||||
start = buffer.Loc(entry.textEdit.range.start.character, entry.textEdit.range.start.line)
|
||||
bp.Cursor:SetSelectionStart(start)
|
||||
bp.Cursor:SetSelectionEnd(xy)
|
||||
bp.Cursor:DeleteSelection()
|
||||
bp.Cursor:ResetSelection()
|
||||
elseif capabilities[bp.Buf:FileType()] and capabilities[bp.Buf:FileType()].completionProvider and capabilities[bp.Buf:FileType()].completionProvider.triggerCharacters then
|
||||
if not found then
|
||||
-- we found nothing - so assume we need the beginning of the line
|
||||
if reversed:starts(" ") or reversed:starts("\t") then
|
||||
-- if we end with some indentation, keep it
|
||||
start = buffer.Loc(#reversed, bp.Cursor.Y)
|
||||
else
|
||||
start = buffer.Loc(0, bp.Cursor.Y)
|
||||
end
|
||||
bp.Cursor:SetSelectionStart(start)
|
||||
bp.Cursor:SetSelectionEnd(xy)
|
||||
bp.Cursor:DeleteSelection()
|
||||
bp.Cursor:ResetSelection()
|
||||
end
|
||||
end
|
||||
local inserting = "" .. toInsert:gsub(prefix, "")
|
||||
bp.Buf:Insert(start, inserting)
|
||||
|
||||
if #results > 1 then
|
||||
if entry.textEdit then
|
||||
bp.Cursor:GotoLoc(start)
|
||||
bp.Cursor:SetSelectionStart(start)
|
||||
else
|
||||
-- if we had to calculate everything outselves
|
||||
-- go back to the original location
|
||||
bp.Cursor:GotoLoc(xy)
|
||||
bp.Cursor:SetSelectionStart(xy)
|
||||
end
|
||||
bp.Cursor:SetSelectionEnd(buffer.Loc(start.X + #toInsert, start.Y))
|
||||
else
|
||||
bp.Cursor:GotoLoc(buffer.Loc(start.X + #inserting, start.Y))
|
||||
end
|
||||
|
||||
local startLoc = buffer.Loc(0, 0)
|
||||
local endLoc = buffer.Loc(0, 0)
|
||||
local msg = ''
|
||||
local insertion = ''
|
||||
if entry.detail or entry.documentation then
|
||||
insertion = fmt.Sprintf("%s", entry.detail or entry.documentation or '')
|
||||
for idx, result in ipairs(results) do
|
||||
if #msg > 0 then
|
||||
msg = msg .. "\n"
|
||||
end
|
||||
local insertion = fmt.Sprintf("%s %s", result.detail or '', result.documentation or '')
|
||||
if idx == (completionCursor % #results) + 1 then
|
||||
local msglines = mysplit(msg, "\n")
|
||||
startLoc = buffer.Loc(0, #msglines)
|
||||
endLoc = buffer.Loc(#insertion - 1, #msglines)
|
||||
end
|
||||
msg = msg .. insertion
|
||||
end
|
||||
else
|
||||
insertion = entry.label
|
||||
for idx, result in ipairs(results) do
|
||||
if #msg > 0 then
|
||||
local msglines = mysplit(msg, "\n")
|
||||
local lastLine = msglines[#msglines]
|
||||
local len = #result.label + 4
|
||||
if #lastLine + len >= bp:GetView().Width then
|
||||
msg = msg .. "\n "
|
||||
else
|
||||
msg = msg .. ' '
|
||||
end
|
||||
else
|
||||
msg = " "
|
||||
end
|
||||
if idx == (completionCursor % #results) + 1 then
|
||||
local msglines = mysplit(msg, "\n")
|
||||
local prefixLen = 0
|
||||
if #msglines > 0 then
|
||||
prefixLen = #msglines[#msglines]
|
||||
else
|
||||
prefixLen = #msg
|
||||
end
|
||||
startLoc = buffer.Loc(prefixLen or 0, #msglines - 1)
|
||||
endLoc = buffer.Loc(prefixLen + #result.label, #msglines - 1)
|
||||
end
|
||||
msg = msg .. result.label
|
||||
end
|
||||
end
|
||||
if config.GetGlobalOption("lsp.autocompleteDetails") then
|
||||
if not splitBP then
|
||||
local tmpName = ("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"):random(32)
|
||||
local logBuf = buffer.NewBuffer(msg, tmpName)
|
||||
splitBP = bp:HSplitBuf(logBuf)
|
||||
bp:NextSplit()
|
||||
else
|
||||
splitBP:SelectAll()
|
||||
splitBP.Cursor:DeleteSelection()
|
||||
splitBP.Cursor:ResetSelection()
|
||||
splitBP.Buf:insert(buffer.Loc(1, 1), msg)
|
||||
end
|
||||
splitBP.Cursor:ResetSelection()
|
||||
splitBP.Cursor:SetSelectionStart(startLoc)
|
||||
splitBP.Cursor:SetSelectionEnd(endLoc)
|
||||
else
|
||||
if entry.detail or entry.documentation then
|
||||
micro.InfoBar():Message(insertion)
|
||||
else
|
||||
local cleaned = " " .. msg:gsub("%s+", " ")
|
||||
local replaced, _ = cleaned:gsub(".*%s" .. insertion .. "%s?", " [" .. insertion .. "] ")
|
||||
micro.InfoBar():Message(replaced)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function formatAction(bp, callback)
|
||||
local filetype = bp.Buf:FileType()
|
||||
if cmd[filetype] == nil then return; end
|
||||
local send = withSend(filetype)
|
||||
local file = bp.Buf.AbsPath
|
||||
|
||||
currentAction[filetype] = { method = "textDocument/formatting", response = formatActionResponse(callback) }
|
||||
send(currentAction[filetype].method, fmt.Sprintf('{"textDocument": {"uri": "file://%s"}, "options": {"tabSize": 4, "insertSpaces": true}}', file))
|
||||
end
|
||||
|
||||
function formatActionResponse(callback)
|
||||
return function (bp, data)
|
||||
if data.result == nil then return; end
|
||||
local edits = data.result
|
||||
-- make sure we apply the changes from back to front
|
||||
-- this allows for changes to not need position updates
|
||||
table.sort(edits, function (left, right)
|
||||
-- go by lines first
|
||||
return left.range['end'].line > right.range['end'].line or
|
||||
-- if lines match, go by end character
|
||||
left.range['end'].line == right.range['end'].line and left.range['end'].character > right.range['end'].character or
|
||||
-- if they match too, go by start character
|
||||
left.range['end'].line == right.range['end'].line and left.range['end'].character == right.range['end'].character and left.range.start.line == left.range['end'].line and left.range.start.character > right.range.start.character
|
||||
end)
|
||||
|
||||
-- save original cursor position
|
||||
local xy = buffer.Loc(bp.Cursor.X, bp.Cursor.Y)
|
||||
for _idx, edit in ipairs(edits) do
|
||||
rangeStart = buffer.Loc(edit.range.start.character, edit.range.start.line)
|
||||
rangeEnd = buffer.Loc(edit.range['end'].character, edit.range['end'].line)
|
||||
-- apply each change
|
||||
bp.Cursor:GotoLoc(rangeStart)
|
||||
bp.Cursor:SetSelectionStart(rangeStart)
|
||||
bp.Cursor:SetSelectionEnd(rangeEnd)
|
||||
bp.Cursor:DeleteSelection()
|
||||
bp.Cursor:ResetSelection()
|
||||
|
||||
if edit.newText ~= "" then
|
||||
bp.Buf:insert(rangeStart, edit.newText)
|
||||
end
|
||||
end
|
||||
-- put the cursor back where it was
|
||||
bp.Cursor:GotoLoc(xy)
|
||||
-- if any changes were applied
|
||||
if #edits > 0 then
|
||||
-- tell the server about the changed document
|
||||
onRune(bp)
|
||||
end
|
||||
|
||||
if callback ~= nil then
|
||||
callback(bp)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- the references action request and response
|
||||
function referencesAction(bp)
|
||||
local filetype = bp.Buf:FileType()
|
||||
if cmd[filetype] == nil then return; end
|
||||
|
||||
local send = withSend(filetype)
|
||||
local file = bp.Buf.AbsPath
|
||||
local line = bp.Buf:GetActiveCursor().Y
|
||||
local char = bp.Buf:GetActiveCursor().X
|
||||
currentAction[filetype] = { method = "textDocument/references", response = referencesActionResponse }
|
||||
send(currentAction[filetype].method, fmt.Sprintf('{"textDocument": {"uri": "file://%s"}, "position": {"line": %.0f, "character": %.0f}, "context": {"includeDeclaration":true}}', file, line, char))
|
||||
end
|
||||
|
||||
function referencesActionResponse(bp, data)
|
||||
if data.result == nil then return; end
|
||||
local results = data.result or data.partialResult
|
||||
if results == nil or #results <= 0 then return; end
|
||||
|
||||
local file = bp.Buf.AbsPath
|
||||
|
||||
local msg = ''
|
||||
for _idx, ref in ipairs(results) do
|
||||
if msg ~= '' then msg = msg .. '\n'; end
|
||||
local doc = (ref.uri or ref.targetUri)
|
||||
msg = msg .. "." .. doc:sub(#rootUri + 1, #doc) .. ":" .. ref.range.start.line .. ":" .. ref.range.start.character
|
||||
end
|
||||
|
||||
local logBuf = buffer.NewBuffer(msg, "References found")
|
||||
local splitBP = bp:HSplitBuf(logBuf)
|
||||
end
|
||||
|
||||
--
|
||||
-- @TODO implement additional functions here...
|
||||
--
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- JSON
|
||||
--
|
||||
-- Internal functions.
|
||||
|
||||
local function kind_of(obj)
|
||||
if type(obj) ~= 'table' then return type(obj) end
|
||||
local i = 1
|
||||
for _ in pairs(obj) do
|
||||
if obj[i] ~= nil then i = i + 1 else return 'table' end
|
||||
end
|
||||
if i == 1 then return 'table' else return 'array' end
|
||||
end
|
||||
|
||||
local function escape_str(s)
|
||||
local in_char = {'\\', '"', '/', '\b', '\f', '\n', '\r', '\t'}
|
||||
local out_char = {'\\', '"', '/', 'b', 'f', 'n', 'r', 't'}
|
||||
for i, c in ipairs(in_char) do
|
||||
s = s:gsub(c, '\\' .. out_char[i])
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
-- Returns pos, did_find; there are two cases:
|
||||
-- 1. Delimiter found: pos = pos after leading space + delim; did_find = true.
|
||||
-- 2. Delimiter not found: pos = pos after leading space; did_find = false.
|
||||
-- This throws an error if err_if_missing is true and the delim is not found.
|
||||
local function skip_delim(str, pos, delim, err_if_missing)
|
||||
pos = pos + #str:match('^%s*', pos)
|
||||
if str:sub(pos, pos) ~= delim then
|
||||
if err_if_missing then
|
||||
error('Expected ' .. delim .. ' near position ' .. pos)
|
||||
end
|
||||
return pos, false
|
||||
end
|
||||
return pos + 1, true
|
||||
end
|
||||
|
||||
-- Expects the given pos to be the first character after the opening quote.
|
||||
-- Returns val, pos; the returned pos is after the closing quote character.
|
||||
local function parse_str_val(str, pos, val)
|
||||
val = val or ''
|
||||
local early_end_error = 'End of input found while parsing string.'
|
||||
if pos > #str then error(early_end_error) end
|
||||
local c = str:sub(pos, pos)
|
||||
if c == '"' then return val, pos + 1 end
|
||||
if c ~= '\\' then return parse_str_val(str, pos + 1, val .. c) end
|
||||
-- We must have a \ character.
|
||||
local esc_map = {b = '\b', f = '\f', n = '\n', r = '\r', t = '\t'}
|
||||
local nextc = str:sub(pos + 1, pos + 1)
|
||||
if not nextc then error(early_end_error) end
|
||||
return parse_str_val(str, pos + 2, val .. (esc_map[nextc] or nextc))
|
||||
end
|
||||
|
||||
-- Returns val, pos; the returned pos is after the number's final character.
|
||||
local function parse_num_val(str, pos)
|
||||
local num_str = str:match('^-?%d+%.?%d*[eE]?[+-]?%d*', pos)
|
||||
local val = tonumber(num_str)
|
||||
if not val then error('Error parsing number at position ' .. pos .. '.') end
|
||||
return val, pos + #num_str
|
||||
end
|
||||
|
||||
json.null = {} -- This is a one-off table to represent the null value.
|
||||
|
||||
function json.parse(str, pos, end_delim)
|
||||
pos = pos or 1
|
||||
if pos > #str then error('Reached unexpected end of input.' .. str) end
|
||||
local pos = pos + #str:match('^%s*', pos) -- Skip whitespace.
|
||||
local first = str:sub(pos, pos)
|
||||
if first == '{' then -- Parse an object.
|
||||
local obj, key, delim_found = {}, true, true
|
||||
pos = pos + 1
|
||||
while true do
|
||||
key, pos = json.parse(str, pos, '}')
|
||||
if key == nil then return obj, pos end
|
||||
if not delim_found then error('Comma missing between object items.') end
|
||||
pos = skip_delim(str, pos, ':', true) -- true -> error if missing.
|
||||
obj[key], pos = json.parse(str, pos)
|
||||
pos, delim_found = skip_delim(str, pos, ',')
|
||||
end
|
||||
elseif first == '[' then -- Parse an array.
|
||||
local arr, val, delim_found = {}, true, true
|
||||
pos = pos + 1
|
||||
while true do
|
||||
val, pos = json.parse(str, pos, ']')
|
||||
if val == nil then return arr, pos end
|
||||
if not delim_found then error('Comma missing between array items.') end
|
||||
arr[#arr + 1] = val
|
||||
pos, delim_found = skip_delim(str, pos, ',')
|
||||
end
|
||||
elseif first == '"' then -- Parse a string.
|
||||
return parse_str_val(str, pos + 1)
|
||||
elseif first == '-' or first:match('%d') then -- Parse a number.
|
||||
return parse_num_val(str, pos)
|
||||
elseif first == end_delim then -- End of an object or array.
|
||||
return nil, pos + 1
|
||||
else -- Parse true, false, or null.
|
||||
local literals = {['true'] = true, ['false'] = false, ['null'] = json.null}
|
||||
for lit_str, lit_val in pairs(literals) do
|
||||
local lit_end = pos + #lit_str - 1
|
||||
if str:sub(pos, lit_end) == lit_str then return lit_val, lit_end + 1 end
|
||||
end
|
||||
local pos_info_str = 'position ' .. pos .. ': ' .. str:sub(pos, pos + 10)
|
||||
error('Invalid json syntax starting at ' .. pos_info_str .. ': ' .. str)
|
||||
end
|
||||
end
|
||||
@@ -1,78 +0,0 @@
|
||||
[{
|
||||
"Name": "lsp",
|
||||
"Description": "Generic LSP Client for Micro",
|
||||
"Website": "https://github.com/AndCake/micro-plugin-lsp",
|
||||
"Tags": ["lsp"],
|
||||
"Versions": [
|
||||
{
|
||||
"Version": "0.4.1",
|
||||
"Url": "https://github.com/AndCake/micro-plugin-lsp/archive/v0.4.1.zip",
|
||||
"Require": {
|
||||
"micro": ">=2.0.10"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Version": "0.4.2",
|
||||
"Url": "https://github.com/AndCake/micro-plugin-lsp/archive/v0.4.2.zip",
|
||||
"Require": {
|
||||
"micro": ">=2.0.8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Version": "0.4.3",
|
||||
"Url": "https://github.com/AndCake/micro-plugin-lsp/archive/v0.4.3.zip",
|
||||
"Require": {
|
||||
"micro": ">=2.0.8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Version": "0.5.0",
|
||||
"Url": "https://github.com/AndCake/micro-plugin-lsp/archive/v0.5.0.zip",
|
||||
"Require": {
|
||||
"micro": ">=2.0.8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Version": "0.5.1",
|
||||
"Url": "https://github.com/AndCake/micro-plugin-lsp/archive/v0.5.1.zip",
|
||||
"Require": {
|
||||
"micro": ">=2.0.8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Version": "0.5.2",
|
||||
"Url": "https://github.com/AndCake/micro-plugin-lsp/archive/v0.5.2.zip",
|
||||
"Require": {
|
||||
"micro": ">=2.0.8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Version": "0.5.3",
|
||||
"Url": "https://github.com/AndCake/micro-plugin-lsp/archive/v0.5.3.zip",
|
||||
"Require": {
|
||||
"micro": ">=2.0.8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Version": "0.6.0",
|
||||
"Url": "https://github.com/AndCake/micro-plugin-lsp/archive/v0.6.0.zip",
|
||||
"Require": {
|
||||
"micro": ">=2.0.8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Version": "0.6.1",
|
||||
"Url": "https://github.com/AndCake/micro-plugin-lsp/archive/v0.6.1.zip",
|
||||
"Require": {
|
||||
"micro": ">=2.0.8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Version": "0.6.2",
|
||||
"Url": "https://github.com/AndCake/micro-plugin-lsp/archive/v0.6.2.zip",
|
||||
"Require": {
|
||||
"micro": ">=2.0.8"
|
||||
}
|
||||
}
|
||||
]
|
||||
}]
|
||||
Reference in New Issue
Block a user