From abf6c9c47d6c7cb30eb69f1bb584ff43e8f3f7e7 Mon Sep 17 00:00:00 2001 From: korin Date: Sat, 15 Apr 2023 16:06:49 -0400 Subject: [PATCH] micro-cfg --- .gitignore | 2 + bindings.json | 9 + colorschemes/vol-scheme.micro | 34 + .../autoclose-improved/autoclose-improved.lua | 93 ++ plug/lsp/LICENSE | 21 + plug/lsp/README.md | 93 ++ plug/lsp/help/lsp.md | 315 ++++++ plug/lsp/main.lua | 933 ++++++++++++++++++ plug/lsp/repo.json | 78 ++ settings.json | 6 + syntax/65xx.yaml | 45 + syntax/asm.yaml | 122 +++ syntax/rust.yaml | 73 ++ 13 files changed, 1824 insertions(+) create mode 100644 .gitignore create mode 100644 bindings.json create mode 100644 colorschemes/vol-scheme.micro create mode 100644 plug/autoclose-improved/autoclose-improved.lua create mode 100644 plug/lsp/LICENSE create mode 100644 plug/lsp/README.md create mode 100644 plug/lsp/help/lsp.md create mode 100644 plug/lsp/main.lua create mode 100644 plug/lsp/repo.json create mode 100644 settings.json create mode 100644 syntax/65xx.yaml create mode 100644 syntax/asm.yaml create mode 100644 syntax/rust.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..406b095 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/backups +/buffers diff --git a/bindings.json b/bindings.json new file mode 100644 index 0000000..a4f3fa4 --- /dev/null +++ b/bindings.json @@ -0,0 +1,9 @@ +{ + "Alt-/": "lua:comment.comment", + "Alt-d": "command:definition", + "Alt-f": "command:format", + "Alt-k": "command:hover", + "Alt-r": "command:references", + "CtrlSpace": "command:lspcompletion", + "CtrlUnderscore": "lua:comment.comment" +} diff --git a/colorschemes/vol-scheme.micro b/colorschemes/vol-scheme.micro new file mode 100644 index 0000000..2e6c36f --- /dev/null +++ b/colorschemes/vol-scheme.micro @@ -0,0 +1,34 @@ +color-link comment "#00FF00" +color-link identifier "#66D9EF" +color-link constant "#00AAFF" +color-link constant.number "#55FFFF" +color-link constant.string "#FF55FF" +color-link constant.string.char "#FF55FF" +color-link statement "#00AAFF" +color-link symbol "#FF0000" +color-link symbol.operator "#FF5500" +color-link symbol.brackets "#FF5500" +color-link preproc "#7F0000" +color-link type "#FFAA00" +color-link selection "#FFFFFF,#6EA1F1" +color-link special "#AAFFFF" +color-link underlined "bold #FF55FF" +color-link error "bold #FF0000" +color-link todo "bold #FFAA00" +color-link hlsearch "#282828,#E6DB74" +color-link statusline "#FFFFFF,#0055FF" +color-link tabbar "#282828,#F8F8F2" +color-link indent-char "#505050" +color-link line-number "#888888,#444444" +color-link current-line-number "#FFFFFF,#222222" +color-link diff-added "#00AF00" +color-link diff-modified "#FFAF00" +color-link diff-deleted "#D70000" +color-link gutter-error "#CB4B16" +color-link gutter-warning "#E6DB74" +color-link cursor-line "#323232" +color-link color-column "#323232" +#No extended types; Plain brackets. +color-link type.extended "default" +#color-link symbol.brackets "default" +color-link symbol.tag "#AE81FF" diff --git a/plug/autoclose-improved/autoclose-improved.lua b/plug/autoclose-improved/autoclose-improved.lua new file mode 100644 index 0000000..20e1daa --- /dev/null +++ b/plug/autoclose-improved/autoclose-improved.lua @@ -0,0 +1,93 @@ +VERSION = "1.0.0" + +local uutil = import("micro/util") +local utf8 = import("utf8") +local autoclose_pairs = {'""', "''", "``", "()", "{}", "[]"} +local conditional_autoclose_pairs = {"<>", "||"} +local auto_newline_pairs = {"()", "{}", "[]"} + +function char_at(str, i) + -- lua indexing is one off from go + return uutil.RuneAt(str, i - 1) +end + +function onRune(bp, r) + for i = 1, #autoclose_pairs do + local pair_at_index = autoclose_pairs[i] + + if r == char_at(pair_at_index, 2) then + local cur_line = bp.Buf:Line(bp.Cursor.Y) + + if char_at(cur_line, bp.Cursor.X + 1) == char_at(pair_at_index, 2) then + bp:Backspace() + bp:CursorRight() + + break + end + + if bp.Cursor.X > 1 + and (uutil.IsWordChar(char_at(cur_line, bp.Cursor.X - 1)) + or char_at(cur_line, bp.Cursor.X - 1) == char_at(pair_at_index, 1)) + then + break + end + end + + if r == char_at(pair_at_index, 1) then + local cur_line = bp.Buf:Line(bp.Cursor.Y) + + if bp.Cursor.X == uutil.CharacterCountInString(cur_line) + or not uutil.IsWordChar(char_at(cur_line, bp.Cursor.X + 1)) + then + -- the '-' here is to derefence the pointer to bp.Cursor.Loc which is automatically made + -- when converting go structs to lua + -- It needs to be dereferenced because the function expects a non pointer struct + bp.Buf:Insert(bp.Cursor.Loc * -1, char_at(pair_at_index, 2)) + bp:CursorLeft() + + break + end + end + end + + return true +end + +function preInsertNewline(bp) + local cur_line = bp.Buf:Line(bp.Cursor.Y) + local curRune = char_at(cur_line, bp.Cursor.X) + local nextRune = char_at(cur_line, bp.Cursor.X + 1) + local ws = uutil.GetLeadingWhitespace(cur_line) + + for i = 1, #auto_newline_pairs do + local autoNewLinePairsAtIndex = auto_newline_pairs[i] + + if curRune == char_at(autoNewlinePairsAtIndex, 1) then + if nextRune == char_at(autoNewlinePairsAtIndex, 2) then + bp:InsertNewline() + bp:InsertTab() + bp.Buf:Insert(bp.Cursor.Loc * -1, "\n" .. ws) + bp:StartOfLine() + bp:CursorLeft() + return false + end + end + end + + return true +end + +function preBackspace(bp) + for i = 1, #autoclose_pairs do + local pair_at_index = autoclose_pairs[i] + local cur_line = bp.Buf:Line(bp.Cursor.Y) + + if char_at(cur_line, bp.Cursor.X + 1) == char_at(pair_at_index, 2) + and char_at(cur_line, bp.Cursor.X) == char_at(pair_at_index, 1) + then + bp:Delete() + end + end + + return true +end diff --git a/plug/lsp/LICENSE b/plug/lsp/LICENSE new file mode 100644 index 0000000..62df372 --- /dev/null +++ b/plug/lsp/LICENSE @@ -0,0 +1,21 @@ +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. diff --git a/plug/lsp/README.md b/plug/lsp/README.md new file mode 100644 index 0000000..758ac8b --- /dev/null +++ b/plug/lsp/README.md @@ -0,0 +1,93 @@ +# 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: + +``` +=[=][,...] +``` + +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. diff --git a/plug/lsp/help/lsp.md b/plug/lsp/help/lsp.md new file mode 100644 index 0000000..dfc148a --- /dev/null +++ b/plug/lsp/help/lsp.md @@ -0,0 +1,315 @@ +# 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) diff --git a/plug/lsp/main.lua b/plug/lsp/main.lua new file mode 100644 index 0000000..a0fa4c9 --- /dev/null +++ b/plug/lsp/main.lua @@ -0,0 +1,933 @@ +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 diff --git a/plug/lsp/repo.json b/plug/lsp/repo.json new file mode 100644 index 0000000..15ad866 --- /dev/null +++ b/plug/lsp/repo.json @@ -0,0 +1,78 @@ +[{ + "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" + } + } + ] +}] diff --git a/settings.json b/settings.json new file mode 100644 index 0000000..6cfee01 --- /dev/null +++ b/settings.json @@ -0,0 +1,6 @@ +{ + "colorscheme": "vol-scheme", + "lsp.ignoreMessages": "LS message1 to ignore|LS message 2 to ignore|...", + "lsp.ignoreTriggerCharacters": "completion,signature", + "lsp.server": "python=pyls,go=gopls,typescript=deno lsp,rust=rust-analyzer" +} diff --git a/syntax/65xx.yaml b/syntax/65xx.yaml new file mode 100644 index 0000000..a00bcbf --- /dev/null +++ b/syntax/65xx.yaml @@ -0,0 +1,45 @@ +filetype: 65xx asm + +detect: + filename: "\\.(A|a|)$" + +rules: + # This file is made for ACME assembly + + ## Instructions + # 65xx + - statement: "\\b(?i)(adc|and|asl|bcc|bcs|beq|bmi|bne|bpl|bvc|bvs|bit|brk|clc)(?-i)\\b" + - statement: "\\b(?i)(cld|cli|clv|cmp|cpx|cpy|dec|dex|dey|eor|inc|inx|iny|jmp)(?-i)\\b" + - statement: "\\b(?i)(jsr|lda|ldx|ldy|lsr|nop|ora|pha|php|pla|plp|rol|ror|rti)(?-i)\\b" + - statement: "\\b(?i)(rts|sbc|sec|sed|sei|sta|stx|sty|tax|tay|tsx|txa|txs|tya)(?-i)\\b" + ## Constants + # Number - it doesn't work + - constant.number: "\\b[0-9 a-f A-F]+\\b" + - constant.number: "(\\#|\\$|%)" + + ## Other + - identifier: "[a-z A-Z 0-9 _]+:" + - preproc: "\\*\\=" + - preproc: "![a-z A-Z]+(.*, [0-9 a-z A-Z]+|)" + + - constant.string: + start: "\"" + end: "\"" + skip: "\\\\." + rules: + - constant.specialChar: "\\\\." + + - constant.string: + start: "'" + end: "'" + skip: "\\\\." + rules: + - constant.specialChar: "\\\\." + + - comment: + start: ";" + end: "$" + rules: + - todo: "(TODO|XXX|FIXME):?" + + diff --git a/syntax/asm.yaml b/syntax/asm.yaml new file mode 100644 index 0000000..94b099e --- /dev/null +++ b/syntax/asm.yaml @@ -0,0 +1,122 @@ +filetype: asm + +detect: + filename: "\\.(S|s|asm)$" + +rules: + # This file is made for NASM assembly + + ## Instructions + # x86 + - statement: "\\b(?i)(mov|aaa|aad|aam|aas|adc|add|and|call|cbw|clc|cld|cli|cmc|cmp|cmpsb|cmpsw|cwd|daa|das|dec|div|esc|hlt|idiv|imul|in|inc|int|into|iret|ja|jae|jb|jbe|jc|je|jg|jge|jl|jle|jna|jnae|jnb|jnbe|jnc|jne|jng|jnge|jnl|jnle|jno|jnp|jns|jnz|jo|jp|jpe|jpo|js|jz|jcxz|jmp|lahf|lds|lea|les|lock|lodsb|lodsw|loop|loope|loopne|loopnz|loopz|movsb|movsw|mul|neg|nop|or|pop|popf|push|pushf|rcl|rcr|rep|repe|repne|repnz|repz|ret|retn|retf|rol|ror|sahf|sal|sar|sbb|scasb|scasw|shl|shr|stc|std|sti|stosb|stosw|sub|test|wait|xchg|xlat|xor)(?-i)\\b" + - statement: "\\b(?i)(bound|enter|ins|leave|outs|popa|pusha)(?-i)\\b" + - statement: "\\b(?i)(arpl|clts|lar|lgdt|lidt|lldt|lmsw|loadall|lsl|ltr|sgdt|sidt|sldt|smsw|str|verr|verw)(?-i)\\b" + - statement: "\\b(?i)(bsf|bsr|bt|btc|btr|bts|cdq|cmpsd|cwde|insd|iret|iretd|iretf|jecxz|lfs|lgs|lss|lodsd|loopw|loopew|loopnew|loopnzw|loopzw|loopd|looped|loopned|loopnzd|loopzd|cr|tr|dr|movsd|movsx|movzx|outsd|popad|popfd|pushad|pushfd|scasd|seta|setae|setb|setbe|setc|sete|setg|setge|setl|setle|setna|setnae|setnb|setnbe|setnc|setne|setng|setnge|setnl|setnle|setno|setnp|setns|setnz|seto|setp|setpe|setpo|sets|setz|shdl|shrd|stosd)(?-i)\\b" + - statement: "\\b(?i)(bswap|cmpxcgh|invd|invlpg|wbinvd|xadd)(?-i)\\b" + - statement: "\\b(?i)(cpuid|cmpxchg8b|rdmsr|rdtsc|wrmsr|rsm)(?-i)\\b" + - statement: "\\b(?i)(rdpmc)(?-i)\\b" + - statement: "\\b(?i)(syscall|sysret)(?-i)\\b" + - statement: "\\b(?i)(cmova|cmovae|cmovb|cmovbe|cmovc|cmove|cmovg|cmovge|cmovl|cmovle|cmovna|cmovnae|cmovnb|cmovnbe|cmovnc|cmovne|cmovng|cmovnge|cmovnle|cmovno|cmovpn|cmovns|cmovnz|cmovo|cmovp|cmovpe|cmovpo|cmovs|cmovz|sysenter|sysexit|ud2)(?-i)\\b" + - statement: "\\b(?i)(maskmovq|movntps|movntq|prefetch0|prefetch1|prefetch2|prefetchnta|sfence)(?-i)\\b" + - statement: "\\b(?i)(clflush|lfence|maskmovdqu|mfence|movntdq|movnti|movntpd|pause)(?-i)\\b" + - statement: "\\b(?i)(monitor|mwait)(?-i)\\b" + - statement: "\\b(?i)(cdqe|cqo|cmpsq|cmpxchg16b|iretq|jrcxz|lodsq|movsdx|popfq|pushfq|rdtscp|scasq|stosq|swapgs)(?-i)\\b" + - statement: "\\b(?i)(clgi|invlpga|skinit|stgi|vmload|vmmcall|vmrun|vmsave)(?-i)\\b" + - statement: "\\b(?i)(vmptrdl|vmptrst|vmclear|vmread|vmwrite|vmcall|vmlaunch|vmresume|vmxoff|vmxon)(?-i)\\b" + - statement: "\\b(?i)(lzcnt|popcnt)(?-i)\\b" + - statement: "\\b(?i)(bextr|blcfill|blci|blcic|blcmask|blcs|blsfill|blsic|t1mskc|tzmsk)(?-i)\\b" + + # x87 + - statement: "\\b(?i)(f2xm1|fabs|fadd|faddp|fbld|fbstp|fchs|fclex|fcom|fcomp|fcompp|fdecstp|fdisi|fdiv|fvidp|fdivr|fdivrp|feni|ffree|fiadd|ficom|ficomp|fidiv|fidivr|fild|fimul|fincstp|finit|fist|fistp|fisub|fisubr|fld|fld1|fldcw|fldenv|fldenvw|fldl2e|fldl2t|fldlg2|fldln2|fldpi|fldz|fmul|fmulp|fnclex|fndisi|fneni|fninit|fnop|fnsave|fnsavenew|fnstcw|fnstenv|fnstenvw|fnstsw|fpatan|fprem|fptan|frndint|frstor|frstorw|fsave|fsavew|fscale|fsqrt|fst|fstcw|fstenv|fstenvw|fstp|fstpsw|fsub|fsubp|fsubr|fsubrp|ftst|fwait|fxam|fxch|fxtract|fyl2x|fyl2xp1)(?-i)\\b" + - statement: "\\b(?i)(fsetpm)(?-i)\\b" + - statement: "\\b(?i)(fcos|fldenvd|fsaved|fstenvd|fprem1|frstord|fsin|fsincos|fstenvd|fucom|fucomp|fucompp)(?-i)\\b" + - statement: "\\b(?i)(fcmovb|fcmovbe|fcmove|fcmove|fcmovnb|fcmovnbe|fcmovne|fcmovnu|fcmovu)(?-i)\\b" + - statement: "\\b(?i)(fcomi|fcomip|fucomi|fucomip)(?-i)\\b" + - statement: "\\b(?i)(fxrstor|fxsave)(?-i)\\b" + - statement: "\\b(?i)(fisttp)(?-i)\\b" + - statement: "\\b(?i)(ffreep)(?-i)\\b" + + # SIMD + - statement: "\\b(?i)(emms|movd|movq|packssdw|packsswb|packuswb|paddb|paddw|paddd|paddsb|paddsw|paddusb|paddusw|pand|pandn|por|pxor|pcmpeqb|pcmpeqw|pcmpeqd|pcmpgtb|pcmpgtw|pcmpgtd|pmaddwd|pmulhw|pmullw|psllw|pslld|psllq|psrad|psraw|psrlw|psrld|psrlq|psubb|psubw|psubd|psubsb|psubsw|psubusb|punpckhbw|punpckhwd|punpckhdq|punkcklbw|punpckldq|punpcklwd)(?-i)\\b" + - statement: "\\b(?i)(paveb|paddsiw|pmagw|pdistib|psubsiw|pmwzb|pmulhrw|pmvnzb|pmvlzb|pmvgezb|pmulhriw|pmachriw)(?-i)\\b" + - statement: "\\b(?i)(femms|pavgusb|pf2id|pfacc|pfadd|pfcmpeq|pfcmpge|pfcmpgt|pfmax|pfmin|pfmul|pfrcp|pfrcpit1|pfrcpit2|pfrsqit1|pfrsqrt|pfsub|pfsubr|pi2fd|pmulhrw|prefetch|prefetchw)(?-i)\\b" + - statement: "\\b(?i)(pf2iw|pfnacc|pfpnacc|pi2fw|pswapd)(?-i)\\b" + - statement: "\\b(?i)(pfrsqrtv|pfrcpv)(?-i)\\b" + - statement: "\\b(?i)(addps|addss|cmpps|cmpss|comiss|cvtpi2ps|cvtps2pi|cvtsi2ss|cvtss2si|cvttps2pi|cvttss2si|divps|divss|ldmxcsr|maxps|maxss|minps|minss|movaps|movhlps|movhps|movlhps|movlps|movmskps|movntps|movss|movups|mulps|mulss|rcpps|rcpss|rsqrtps|rsqrtss|shufps|sqrtps|sqrtss|stmxcsr|subps|subss|ucomiss|unpckhps|unpcklps)(?-i)\\b" + - statement: "\\b(?i)(andnps|andps|orps|pavgb|pavgw|pextrw|pinsrw|pmaxsw|pmaxub|pminsw|pminub|pmovmskb|pmulhuw|psadbw|pshufw|xorps)(?-i)\\b" + - statement: "\\b(?i)(movups|movss|movlps|movhlps|movlps|unpcklps|unpckhps|movhps|movlhps|prefetchnta|prefetch0|prefetch1|prefetch2|nop|movaps|cvtpi2ps|cvtsi2ss|cvtps2pi|cvttss2si|cvtps2pi|cvtss2si|ucomiss|comiss|sqrtps|sqrtss|rsqrtps|rsqrtss|rcpps|andps|orps|xorps|addps|addss|mulps|mulss|subps|subss|minps|minss|divps|divss|maxps|maxss|pshufw|ldmxcsr|stmxcsr|sfence|cmpps|cmpss|pinsrw|pextrw|shufps|pmovmskb|pminub|pmaxub|pavgb|pavgw|pmulhuw|movntq|pminsw|pmaxsw|psadbw|maskmovq)(?-i)\\b" + - statement: "\\b(?i)(addpd|addsd|addnpd|cmppd|cmpsd)(?-i)\\b" + - statement: "\\b(?i)(addpd|addsd|andnpd|andpd|cmppd|cmpsd|comisd|cvtdq2pd|cvtdq2ps|cvtpd2dq|cvtpd2pi|cvtpd2ps|cvtpi2pd|cvtps2dq|cvtps2pd|cvtsd2si|cvtsd2ss|cvtsi2sd|cvtss2sd|cvttpd2dq|cvttpd2pi|cvttps2dq|cvttsd2si|divpd|divsd|maxpd|maxsd|minpd|minsd|movapd|movhpd|movlpd|movmskpd|movsd|movupd|mulpd|mulsd|orpd|shufpd|sqrtpd|sqrtsd|subpd|subsd|ucomisd|unpckhpd|unpcklpd|xorpd)(?-i)\\b" + - statement: "\\b(?i)(movdq2q|movdqa|movdqu|movq2dq|paddq|psubq|pmuludq|pshufhw|pshuflw|pshufd|pslldq|psrldq|punpckhqdq|punpcklqdq)(?-i)\\b" + - statement: "\\b(?i)(addsubpd|addsubps|haddpd|haddps|hsubpd|hsubps|movddup|movshdup|movsldu)(?-i)\\b" + - statement: "\\b(?i)(lddqu)(?-i)\\b" + - statement: "\\b(?i)(psignw|psignd|psignb|pshufb|pmulhrsw|pmaddubsw|phsubw|phsubsw|phsubd|phaddw|phaddsw|phaddd|palignr|pabsw|pabsd|pabsb)(?-i)\\b" + - statement: "\\b(?i)(dpps|dppd|blendps|blendpd|blendvps|blendvpd|roundps|roundss|roundpd|roundsd|insertps|extractps)(?-i)\\b" + - statement: "\\b(?i)(mpsadbw|phminposuw|pmulld|pmuldq|pblendvb|pblendw|pminsb|pmaxsb|pminuw|pmaxuw|pminud|pmaxud|pminsd|pmaxsd|pinsrb|pinsrd/pinsrq|pextrb|pextrw|pextrd/pextrq|pmovsxbw|pmovzxbw|pmovsxbd|pmovzxbd|pmovsxbq|pmovzxbq|pmovsxwd|pmovzxwd|pmovsxwq|pmovzxwq|pmovsxdq|pmovzxdq|ptest|pcmpeqq|packusdw|movntdqa)(?-i)\\b" + - statement: "\\b(?i)(extrq|insertq|movntsd|movntss)(?-i)\\b" + - statement: "\\b(?i)(crc32|pcmpestri|pcmpestrm|pcmpistri|pcmpistrm|pcmpgtq)(?-i)\\b" + - statement: "\\b(?i)(vfmaddpd|vfmaddps|vfmaddsd|vfmaddss|vfmaddsubpd|vfmaddsubps|vfmsubaddpd|vfmsubaddps|vfmsubpd|vfmsubps|vfmsubsd|vfmsubss|vfnmaddpd|vfnmaddps|vfnmaddsd|vfnmaddss|vfnmsubps|vfnmsubsd|vfnmsubss)(?-i)\\b" + + # Crypto + - statement: "\\b(?i)(aesenc|aesenclast|aesdec|aesdeclast|aeskeygenassist|aesimc)(?-i)\\b" + - statement: "\\b(?i)(sha1rnds4|sha1nexte|sha1msg1|sha1msg2|sha256rnds2|sha256msg1|sha256msg2)(?-i)\\b" + + # Undocumented + - statement: "\\b(?i)(aam|aad|salc|icebp|loadall|loadalld|ud1)(?-i)\\b" + + ## Registers + - identifier: "\\b(?i)(al|ah|bl|bh|cl|ch|dl|dh|bpl|sil|r8b|r9b|r10b|r11b|dil|spl|r12b|r13b|r14b|r15)(?-i)\\b" + - identifier: "\\b(?i)(cw|sw|tw|fp_ds|fp_opc|fp_ip|fp_dp|fp_cs|cs|ss|ds|es|fs|gs|gdtr|idtr|tr|ldtr|ax|bx|cx|dx|bp|si|r8w|r9w|r10w|r11w|di|sp|r12w|r13w|r14w|r15w|ip)(?-i)\\b" + - identifier: "\\b(?i)(fp_dp|fp_ip|eax|ebx|ecx|edx|ebp|esi|r8d|r9d|r10d|r11d|edi|esp|r12d|r13d|r14d|r15d|eip|eflags|mxcsr)(?-i)\\b" + - identifier: "\\b(?i)(mm0|mm1|mm2|mm3|mm4|mm5|mm6|mm7|rax|rbx|rcx|rdx|rbp|rsi|r8|r9|r10|r11|rdi|rsp|r12|r13|r14|r15|rip|rflags|cr0|cr1|cr2|cr3|cr4|cr5|cr6|cr7|cr8|cr9|cr10|cr11|cr12|cr13|cr14|cr15|msw|dr0|dr1|dr2|dr3|r4|dr5|dr6|dr7|dr8|dr9|dr10|dr11|dr12|dr13|dr14|dr15)(?-i)\\b" + - identifier: "\\b(?i)(st0|st1|st2|st3|st4|st5|st6|st7)(?-i)\\b" + - identifier: "\\b(?i)(xmm0|xmm1|xmm2|xmm3|xmm4|xmm5|xmm6|xmm7|xmm8|xmm9|xmm10|xmm11|xmm12|xmm13|xmm14|xmm15)(?-i)\\b" + - identifier: "\\b(?i)(ymm0|ymm1|ymm2|ymm3|ymm4|ymm5|ymm6|ymm7|ymm8|ymm9|ymm10|ymm11|ymm12|ymm13|ymm14|ymm15)(?-i)\\b" + - identifier: "\\b(?i)(zmm0|zmm1|zmm2|zmm3|zmm4|zmm5|zmm6|zmm7|zmm8|zmm9|zmm10|zmm11|zmm12|zmm13|zmm14|zmm15|zmm16|zmm17|zmm18|zmm19|zmm20|zmm21|zmm22|zmm23|zmm24|zmm25|zmm26|zmm27|zmm28|zmm29|zmm30|zmm31)(?-i)\\b" + + # 65xx + - statement: "\\b(?i)(adc|and|asl|bcc|bcs|beq|bmi|bne|bpl|bvc|bvs|bit|brk|clc)(?-i)\\b" + - statement: "\\b(?i)(cld|cli|clv|cmp|cpx|cpy|dec|dex|dey|eor|inc|inx|iny|jmp)(?-i)\\b" + - statement: "\\b(?i)(jsr|lda|ldx|ldy|lsr|nop|ora|pha|php|pla|plp|rol|ror|rti)(?-i)\\b" + - statement: "\\b(?i)(rts|sbc|sec|sed|sei|sta|stx|sty|tax|tay|tsx|txa|txs|tya)(?-i)\\b" + + ## Preprocessor (NASM) + - preproc: "%+(\\+|\\?|\\?\\?|)[a-z A-Z 0-9]+" + - preproc: "%\\[[. a-z A-Z 0-9]*\\]" + + ## Constants + # Number - it works + # x86 + - constant.number: "\\b(|h|A|0x)+[0-9]+(|h|A)+\\b" + - constant.number: "\\b0x[0-9 a-f A-F]+\\b" + # Number - it doesn't work + # 65xx + #- constant.number: "\\b(|\\#)(\\#|$$|\\%)[0-9 a-f A-F]+\\b" + - constant.number: "(\\#|\\$|%)" + + ## Other + - statement: "\\b(?i)(extern|global|section|segment|_start|\\.text|\\.data|\\.bss)(?-i)\\b" + - statement: "\\b(?i)(db|dw|dd|dq|dt|ddq|do)(?-i)\\b" + - identifier: "[a-z A-Z 0-9 _]+:" + + - constant.string: + start: "\"" + end: "\"" + skip: "\\\\." + rules: + - constant.specialChar: "\\\\." + + - constant.string: + start: "'" + end: "'" + skip: "\\\\." + rules: + - constant.specialChar: "\\\\." + + - comment: + start: ";" + end: "$" + rules: + - todo: "(TODO|XXX|FIXME):?" + + diff --git a/syntax/rust.yaml b/syntax/rust.yaml new file mode 100644 index 0000000..1b0e020 --- /dev/null +++ b/syntax/rust.yaml @@ -0,0 +1,73 @@ +filetype: rust + +detect: + filename: "\\.rs$" + +rules: + # function definition + - identifier: "fn [a-z0-9_]+" + # Modules + - symbol: "mod [a-z0-9_]+" + - symbol: "[a-z0-9_]+::" + # Reserved words + - statement: "\\b(abstract|alignof|as|become|box|break|const|continue|crate|do|dyn|else|enum|extern|false|final|fn|for|if|impl|in|let|loop|macro|match|mod|move|mut|offsetof|override|priv|pub|pure|ref|return|sizeof|static|self|struct|super|true|trait|type|typeof|unsafe|unsized|use|virtual|where|while|yield)\\b" + # macros + - special: "[a-z_]+!" + # Constants + - constant: "\\b[A-Z][A-Z_0-9]+\\b" + # Numbers + - constant.number: "\\b[0-9]+\\b" + # Booleans + - constant: "\\b(true|false)\\b" + # Traits/Enums/Structs/Types/etc. + - type: "\\b[A-Z]+[a-zA-Z_0-9]*[a-z]+[a-zA-Z_0-9]*\\b" + # Builtin types that start with lowercase. + - type: "\\b(bool|str|isize|usize|((i|u)(8|16|32|64|128))|f32|f64)\\b" + # Symbols + - symbol.brackets: "[(){}\\[\\]]" + - symbol: "(\\*|//|/|%|\\+|-|\\^|>|>=|<|<=|!=|=|[\\.]{2,3}|#)" + - symbol.operator: "[-+/*=<>!%&|^\\?]" + # Attributes + - preproc: "#!?\\[.*\\]" + + + - constant.string: + start: "\"" + end: "\"" + skip: '\\.' + rules: + - constant.specialChar: '\\.' + + - constant.string: + start: "r#+\"" + end: "\"#+" + rules: [] + + # Character literals + # NOTE: This is an ugly hack to work around the fact that rust uses + # single quotes both for character literals and lifetimes. + # Match all character literals. + - constant.string: "'(\\\\.|.)'" + # Match the '"' literal which would otherwise match + # as a double quoted string and destroy the highlighting. + - constant.string: + start: "'\"" + end: "'" + rules: [] + + - comment: + start: "//" + end: "$" + rules: + - todo: "(TODO|XXX|FIXME):?" + + - comment: + start: "/\\*" + end: "\\*/" + rules: + - todo: "(TODO|XXX|FIXME):?" + + - special: + start: "#!\\[" + end: "\\]" + rules: []