├── .termux ├── hosts ├── shell ├── font.ttf ├── termux.properties └── colors.properties ├── .gitconfig-decagon ├── .gitconfig-vertex ├── .gitconfig-merck ├── .config └── micro │ ├── bindings.json │ ├── settings.json │ └── plug │ └── lsp │ ├── LICENSE │ ├── repo.json │ ├── README.md │ ├── help │ └── lsp.md │ └── main.lua ├── .vim └── coc-settings.json ├── .gitconfig ├── .zshrc └── .vimrc /.termux/hosts: -------------------------------------------------------------------------------- 1 | 0.0.0.0 vaultext.test -------------------------------------------------------------------------------- /.termux/shell: -------------------------------------------------------------------------------- 1 | /data/data/com.termux/files/usr/bin/zsh -------------------------------------------------------------------------------- /.termux/font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devhammed/dotfiles-android/main/.termux/font.ttf -------------------------------------------------------------------------------- /.gitconfig-decagon: -------------------------------------------------------------------------------- 1 | [user] 2 | email = hammedo@decagonhq.com 3 | signingkey = ACA8D801EE6D4885 4 | [core] 5 | sshCommand = "ssh -i ~/.ssh/id_decagon" 6 | -------------------------------------------------------------------------------- /.gitconfig-vertex: -------------------------------------------------------------------------------- 1 | [user] 2 | username = hoyedele-vertex 3 | email = hoyedele@vertex.com 4 | signingkey = 261392B49887F6A5 5 | [core] 6 | sshCommand = "ssh -i ~/.ssh/id_vertex" 7 | -------------------------------------------------------------------------------- /.gitconfig-merck: -------------------------------------------------------------------------------- 1 | [user] 2 | username = oyedeleh_merck 3 | email = hammed.oyedele@merck.com 4 | signingkey = 045925AA9FF6386E 5 | [core] 6 | sshCommand = "ssh -i ~/.ssh/id_merck" 7 | -------------------------------------------------------------------------------- /.config/micro/bindings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Alt-/": "lua:comment.comment", 3 | "Alt-d": "command:definition", 4 | "Alt-f": "command:format", 5 | "Alt-k": "command:hover", 6 | "Alt-r": "command:references", 7 | "CtrlSpace": "command:lspcompletion", 8 | "CtrlUnderscore": "lua:comment.comment" 9 | } 10 | -------------------------------------------------------------------------------- /.config/micro/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "lsp.autocompleteDetails": false, 3 | "lsp.formatOnSave": true, 4 | "lsp.ignoreMessages": "LS message1 to ignore|LS message 2 to ignore|...", 5 | "lsp.ignoreTriggerCharacters": "completion,signature", 6 | "lsp.server": "python=pyls,go=gopls,typescript=deno lsp,rust=rls", 7 | "lsp.tabcompletion": true 8 | } 9 | -------------------------------------------------------------------------------- /.termux/termux.properties: -------------------------------------------------------------------------------- 1 | ### Adjust terminal scrollback buffer. Max is 50000. 2 | terminal-transcript-rows = 50000 3 | 4 | ### Extra keys rows 5 | extra-keys = [ \ 6 | [F1, F2, F3, F4, F5, F6, F7], \ 7 | [ESC, FN, '|', '/', {key: UP, popup: PGUP}, BACKSLASH, '~'], \ 8 | [CTRL, SHIFT, ALT, {key: LEFT, popup: HOME}, {key: DOWN, popup: PGDN}, {key: RIGHT, popup: END}, BKSP], \ 9 | [TAB, {key: APOSTROPHE, popup: QUOTE}, {key: '!', popup: '?'}, {key: '-', popup: '_'}, {key: ':', popup: ';'}, {key: KEYBOARD, popup: {macro: 'CTRL d', display: exit}}, DRAWER] \ 10 | ] -------------------------------------------------------------------------------- /.termux/colors.properties: -------------------------------------------------------------------------------- 1 | # https://draculatheme.com/ 2 | # https://github.com/dracula/xresources/blob/master/Xresources 3 | # special 4 | foreground=#f8f8f2 5 | cursor=#f8f8f2 6 | background=#282a36 7 | # black 8 | color0=#000000 9 | color8=#4d4d4d 10 | # red 11 | color1=#ff5555 12 | color9=#ff6e67 13 | # green 14 | color2=#50fa7b 15 | color10=#5af78e 16 | # yellow 17 | color3=#f1fa8c 18 | color11=#f4f99d 19 | # blue 20 | color4=#bd93f9 21 | color12=#caa9fa 22 | # magenta 23 | color5=#ff79c6 24 | color13=#ff92d0 25 | # cyan 26 | color6=#8be9fd 27 | color14=#9aedfe 28 | # white 29 | color7=#bfbfbf 30 | color15=#e6e6e6 31 | 32 | -------------------------------------------------------------------------------- /.vim/coc-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "coc.preferences.formatOnSave": true, 3 | "prettier.semi": true, 4 | "prettier.trailingComma": "none", 5 | "prettier.jsxSingleQuote": true, 6 | "prettier.singleQuote": true, 7 | "suggest.noselect": false, 8 | "cSpell.userWords": ["vala"], 9 | "python.jediEnabled": false, 10 | "intelephense.licenceKey": "/data/data/com.termux/files/home/.intelephense.txt", 11 | "languageserver": { 12 | "vala": { 13 | "command": "vala-language-server", 14 | "filetypes": ["vala", "genie"] 15 | }, 16 | "bash": { 17 | "command": "bash-language-server", 18 | "args": ["start"], 19 | "filetypes": ["sh"], 20 | "ignoredRootPaths": ["~"] 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitconfig: -------------------------------------------------------------------------------- 1 | [user] 2 | name = Hammed Oyedele 3 | email = itz.harmid@gmail.com 4 | username = devhammed 5 | signingkey = 990729A301F44E5C 6 | [init] 7 | defaultBranch = main 8 | templateDir = ~/.git-template 9 | [commit] 10 | gpgSign = true 11 | [tag] 12 | gpgSign = true 13 | [pull] 14 | rebase = false 15 | [core] 16 | sshCommand = "ssh -i ~/.ssh/id_personal" 17 | excludesFile = ~/.gitignore 18 | autocrlf = input 19 | [merge] 20 | renameLimit = 100000 21 | [includeIf "gitdir:~/projects/decagon/"] 22 | path = ~/.gitconfig-decagon 23 | [includeIf "gitdir:~/projects/vertex/"] 24 | path = ~/.gitconfig-vertex 25 | [includeIf "gitdir:~/projects/merck/"] 26 | path = ~/.gitconfig-merck 27 | [includeIf "gitdir:~/.composer/cex-wpvip-repo-registry/"] 28 | path = ~/.gitconfig-merck 29 | -------------------------------------------------------------------------------- /.config/micro/plug/lsp/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Robert Kunze 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.zshrc: -------------------------------------------------------------------------------- 1 | # If you come from bash you might have to change your $PATH. 2 | export PATH=$HOME/bin:$HOME/.local/bin:$PATH 3 | 4 | # Path to your oh-my-zsh installation. 5 | export ZSH="$HOME/.oh-my-zsh" 6 | 7 | # Set name of the theme to load --- if set to "random", it will 8 | # load a random theme each time oh-my-zsh is loaded, in which case, 9 | # to know which specific one was loaded, run: echo $RANDOM_THEME 10 | # See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes 11 | ZSH_THEME="" 12 | 13 | # Which plugins would you like to load? 14 | # Standard plugins can be found in $ZSH/plugins/ 15 | # Custom plugins may be added to $ZSH_CUSTOM/plugins/ 16 | # Example format: plugins=(rails git textmate ruby lighthouse) 17 | # Add wisely, as too many plugins slow down shell startup. 18 | plugins=( 19 | git 20 | artisan 21 | zsh-autosuggestions 22 | zsh-syntax-highlighting 23 | ) 24 | 25 | fpath+=${ZSH_CUSTOM:-${ZSH:-~/.oh-my-zsh}/custom}/plugins/zsh-completions/src 26 | 27 | source $ZSH/oh-my-zsh.sh 28 | 29 | # User configuration 30 | export EDITOR="vim" 31 | export PATH="$HOME/go/bin:$PATH" 32 | 33 | # Set personal aliases, overriding those provided by oh-my-zsh libs, 34 | # plugins, and themes. Aliases can be placed here, though oh-my-zsh 35 | # users are encouraged to define aliases within the ZSH_CUSTOM folder. 36 | # For a full list of active aliases, run `alias`. 37 | alias zshconfig="vim ~/.zshrc" 38 | alias ohmyzsh="vim ~/.oh-my-zsh" 39 | alias cat="bat" 40 | alias create-laravel-app="composer create-project laravel/laravel" 41 | 42 | # Setup SSH-Agent 43 | if [ -z "$SSH_AUTH_SOCK" ] ; then 44 | eval "$(ssh-agent -s)" > /dev/null 2>&1 45 | 46 | ssh-add ~/.ssh/id_personal > /dev/null 2>&1 47 | 48 | ssh-add ~/.ssh/id_decagon > /dev/null 2>&1 49 | 50 | ssh-add ~/.ssh/id_vertex > /dev/null 2>&1 51 | fi 52 | 53 | # Load Zoxide 54 | eval "$(zoxide init zsh)" 55 | 56 | # Load Starship 57 | eval "$(starship init zsh)" 58 | -------------------------------------------------------------------------------- /.config/micro/plug/lsp/repo.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "Name": "lsp", 3 | "Description": "Generic LSP Client for Micro", 4 | "Website": "https://github.com/AndCake/micro-plugin-lsp", 5 | "Tags": ["lsp"], 6 | "Versions": [ 7 | { 8 | "Version": "0.4.1", 9 | "Url": "https://github.com/AndCake/micro-plugin-lsp/archive/v0.4.1.zip", 10 | "Require": { 11 | "micro": ">=2.0.10" 12 | } 13 | }, 14 | { 15 | "Version": "0.4.2", 16 | "Url": "https://github.com/AndCake/micro-plugin-lsp/archive/v0.4.2.zip", 17 | "Require": { 18 | "micro": ">=2.0.8" 19 | } 20 | }, 21 | { 22 | "Version": "0.4.3", 23 | "Url": "https://github.com/AndCake/micro-plugin-lsp/archive/v0.4.3.zip", 24 | "Require": { 25 | "micro": ">=2.0.8" 26 | } 27 | }, 28 | { 29 | "Version": "0.5.0", 30 | "Url": "https://github.com/AndCake/micro-plugin-lsp/archive/v0.5.0.zip", 31 | "Require": { 32 | "micro": ">=2.0.8" 33 | } 34 | }, 35 | { 36 | "Version": "0.5.1", 37 | "Url": "https://github.com/AndCake/micro-plugin-lsp/archive/v0.5.1.zip", 38 | "Require": { 39 | "micro": ">=2.0.8" 40 | } 41 | }, 42 | { 43 | "Version": "0.5.2", 44 | "Url": "https://github.com/AndCake/micro-plugin-lsp/archive/v0.5.2.zip", 45 | "Require": { 46 | "micro": ">=2.0.8" 47 | } 48 | }, 49 | { 50 | "Version": "0.5.3", 51 | "Url": "https://github.com/AndCake/micro-plugin-lsp/archive/v0.5.3.zip", 52 | "Require": { 53 | "micro": ">=2.0.8" 54 | } 55 | }, 56 | { 57 | "Version": "0.6.0", 58 | "Url": "https://github.com/AndCake/micro-plugin-lsp/archive/v0.6.0.zip", 59 | "Require": { 60 | "micro": ">=2.0.8" 61 | } 62 | }, 63 | { 64 | "Version": "0.6.1", 65 | "Url": "https://github.com/AndCake/micro-plugin-lsp/archive/v0.6.1.zip", 66 | "Require": { 67 | "micro": ">=2.0.8" 68 | } 69 | }, 70 | { 71 | "Version": "0.6.2", 72 | "Url": "https://github.com/AndCake/micro-plugin-lsp/archive/v0.6.2.zip", 73 | "Require": { 74 | "micro": ">=2.0.8" 75 | } 76 | } 77 | ] 78 | }] 79 | -------------------------------------------------------------------------------- /.config/micro/plug/lsp/README.md: -------------------------------------------------------------------------------- 1 | # Micro Plugin LSP Client 2 | 3 | **Please note:** This software is very much not finished. It is more like a 4 | proof of concept and might break if you call it names. 5 | 6 | Provides LSP methods as actions to Micro that can subsequently be mapped to key 7 | bindings. 8 | 9 | Currently implemented methods: 10 | 11 | - textDocument/hover 12 | - textDocument/definition 13 | - textDocument/completion 14 | - textDocument/formatting 15 | - textDocument/references 16 | 17 | If possible, this plugin will register the following shortcuts: 18 | 19 | - Alt-k for hover 20 | - Alt-d for definition lookup 21 | - Alt-f for formatting 22 | - Alt-r for looking up references 23 | - Ctrl-space for completion 24 | 25 | ## Installation 26 | 27 | Clone this repo into micro's plug folder: 28 | 29 | ``` 30 | $ git clone https://github.com/AndCake/micro-plugin-lsp ~/.config/micro/plug/lsp 31 | ``` 32 | 33 | ## Configuration 34 | 35 | In your `settings.json`, you add the `lsp.server` option in order to enable 36 | using it for your languages' server. 37 | 38 | Example: 39 | 40 | ``` 41 | { 42 | "lsp.server": "python=pyls,go=gopls,typescript=deno lsp,rust=rls", 43 | "lsp.formatOnSave": true, 44 | "lsp.ignoreMessages": "LS message1 to ignore|LS message 2 to ignore|...", 45 | "lsp.tabcompletion": true, 46 | "lsp.ignoreTriggerCharacters": "completion,signature", 47 | "lsp.autocompleteDetails": false 48 | } 49 | ``` 50 | 51 | The format for the `lsp.server` value is a comma-separated list for each file 52 | type you want to boot up a language server: 53 | 54 | ``` 55 | =[=][,...] 56 | ``` 57 | 58 | You can also use an environment variable called `MICRO_LSP` to define the same 59 | information. If set, it will override the `lsp.server` from the `settings.json`. 60 | You can add a line such as the following to your shell profile (e.g. .bashrc): 61 | 62 | ``` 63 | export MICRO_LSP='python=pyls,go=gopls,typescript=deno lsp={"importMap":"import_map.json"},rust=rls' 64 | ``` 65 | 66 | If neither the MICRO_LSP nor the lsp.server is set, then the plugin falls back 67 | to the following settings: 68 | 69 | ``` 70 | 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 71 | ``` 72 | 73 | The option `lsp.autocompleteDetails` allows for showing all auto-completions in 74 | a horizontally split buffer view (true) instead of the status line (false). 75 | 76 | ## Testing 77 | 78 | This plugin has been tested briefly with the following language servers: 79 | 80 | - C++ [clangd](https://clangd.llvm.org) / 81 | [ccls](https://github.com/MaskRay/ccls) 82 | - go: [gopls](https://pkg.go.dev/golang.org/x/tools/gopls#section-readme) 83 | - markdown, JSON, typescript, javascript (including JSX/TSX): 84 | [deno](https://deno.land/) 85 | - python: pyls, [pylsp](https://github.com/python-lsp/python-lsp-server) 86 | - rust: [rls](https://github.com/rust-lang/rls) 87 | - lua: [lua-lsp](https://github.com/Alloyed/lua-lsp) 88 | - zig: [zls](https://github.com/zigtools/zls) 89 | 90 | ## Known issues 91 | 92 | Not all possible types of modification events to the file are currently being 93 | sent to the language server. Saving the file will re-synchronize it, though. 94 | -------------------------------------------------------------------------------- /.vimrc: -------------------------------------------------------------------------------- 1 | " Automatic installation of Vim-Plug 2 | if empty(glob('~/.vim/autoload/plug.vim')) 3 | silent execute '!curl -fLo ~/.vim/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim' 4 | autocmd VimEnter * PlugInstall --sync | source $MYVIMRC 5 | endif 6 | 7 | " Load Plugins 8 | call plug#begin() 9 | 10 | Plug 'tpope/vim-sensible' 11 | Plug 'tpope/vim-fugitive' 12 | Plug 'jiangmiao/auto-pairs' 13 | Plug 'github/copilot.vim' 14 | Plug 'dracula/vim', { 'as': 'dracula' } 15 | Plug 'neoclide/coc.nvim', {'branch': 'release'} 16 | 17 | call plug#end() 18 | 19 | " == begin:coc.nvim settings == 20 | 21 | " Set required extensions 22 | let g:coc_global_extensions = ['coc-json', 'coc-git', 'coc-css', 'coc-html', 'coc-emmet', 'coc-flutter', 'coc-go', 'coc-phpls', 'coc-prettier', 'coc-python', 'coc-rls', 'coc-spell-checker', 'coc-tsserver'] 23 | 24 | " TextEdit might fail if hidden is not set. 25 | set hidden 26 | 27 | " Some servers have issues with backup files, see #649. 28 | set nobackup 29 | set nowritebackup 30 | 31 | " Give more space for displaying messages. 32 | set cmdheight=2 33 | 34 | " Having longer updatetime (default is 4000 ms = 4 s) leads to noticeable 35 | " delays and poor user experience. 36 | set updatetime=300 37 | 38 | " Don't pass messages to |ins-completion-menu|. 39 | set shortmess+=c 40 | 41 | " Always show the signcolumn, otherwise it would shift the text each time 42 | " diagnostics appear/become resolved. 43 | if has("patch-8.1.1564") 44 | " Recently vim can merge signcolumn and number column into one 45 | set signcolumn=number 46 | else 47 | set signcolumn=yes 48 | endif 49 | 50 | " Make used for trigger completion, completion confirm, snippet expand and jump like VSCode. 51 | inoremap 52 | \ pumvisible() ? coc#_select_confirm() : 53 | \ coc#expandableOrJumpable() ? "\=coc#rpc#request('doKeymap', ['snippets-expand-jump',''])\" : 54 | \ check_back_space() ? "\" : 55 | \ coc#refresh() 56 | 57 | function! s:check_back_space() abort 58 | let col = col('.') - 1 59 | return !col || getline('.')[col - 1] =~# '\s' 60 | endfunction 61 | 62 | let g:coc_snippet_next = '' 63 | 64 | " Use to confirm completion, `u` means break undo chain at current 65 | " position. Coc only does snippet and additional edit on confirm. 66 | " could be remapped by other vim plugin, try `:verbose imap `. 67 | if exists('*complete_info') 68 | inoremap complete_info()["selected"] != "-1" ? "\" : "\u\" 69 | else 70 | inoremap pumvisible() ? "\" : "\u\" 71 | endif 72 | 73 | " Use `[g` and `]g` to navigate diagnostics 74 | " Use `:CocDiagnostics` to get all diagnostics of current buffer in location list. 75 | nmap [g (coc-diagnostic-prev) 76 | nmap ]g (coc-diagnostic-next) 77 | 78 | " GoTo code navigation. 79 | nmap gd (coc-definition) 80 | nmap gy (coc-type-definition) 81 | nmap gi (coc-implementation) 82 | nmap gr (coc-references) 83 | 84 | " Use K to show documentation in preview window. 85 | nnoremap K :call show_documentation() 86 | 87 | function! s:show_documentation() 88 | if (index(['vim','help'], &filetype) >= 0) 89 | execute 'h '.expand('') 90 | else 91 | call CocAction('doHover') 92 | endif 93 | endfunction 94 | 95 | " Highlight the symbol and its references when holding the cursor. 96 | autocmd CursorHold * silent call CocActionAsync('highlight') 97 | 98 | " Symbol renaming. 99 | nmap (coc-rename) 100 | 101 | " Formatting selected code. 102 | xmap f (coc-format-selected) 103 | nmap f (coc-format-selected) 104 | 105 | augroup typescriptAndJsonCmd 106 | autocmd! 107 | " Setup formatexpr specified filetype(s). 108 | autocmd FileType typescript,json setl formatexpr=CocAction('formatSelected') 109 | " Update signature help on jump placeholder. 110 | autocmd User CocJumpPlaceholder call CocActionAsync('showSignatureHelp') 111 | augroup end 112 | 113 | " Remap for do codeAction of selected region 114 | vmap a (coc-codeaction-selected) 115 | nmap a (coc-codeaction-selected) 116 | 117 | " Remap keys for applying codeAction to the current buffer. 118 | nmap ac (coc-codeaction) 119 | " Apply AutoFix to problem on the current line. 120 | nmap qf (coc-fix-current) 121 | 122 | " Map function and class text objects 123 | " NOTE: Requires 'textDocument.documentSymbol' support from the language server. 124 | xmap if (coc-funcobj-i) 125 | omap if (coc-funcobj-i) 126 | xmap af (coc-funcobj-a) 127 | omap af (coc-funcobj-a) 128 | xmap ic (coc-classobj-i) 129 | omap ic (coc-classobj-i) 130 | xmap ac (coc-classobj-a) 131 | omap ac (coc-classobj-a) 132 | 133 | " Use CTRL-S for selections ranges. 134 | " Requires 'textDocument/selectionRange' support of LS, ex: coc-tsserver 135 | nmap (coc-range-select) 136 | xmap (coc-range-select) 137 | 138 | " Add `:Format` command to format current buffer. 139 | command! -nargs=0 Format :call CocAction('format') 140 | 141 | " Add `:Fold` command to fold current buffer. 142 | command! -nargs=? Fold :call CocAction('fold', ) 143 | 144 | " Add `:OR` command for organize imports of the current buffer. 145 | command! -nargs=0 OR :call CocAction('runCommand', 'editor.action.organizeImport') 146 | 147 | " Mappings for CoCList 148 | " Show all diagnostics. 149 | nnoremap a :CocList diagnostics 150 | " Manage extensions. 151 | nnoremap e :CocList extensions 152 | " Show commands. 153 | nnoremap :CocList commands 154 | " Find symbol of current document. 155 | nnoremap o :CocList outline 156 | " Search workspace symbols. 157 | nnoremap s :CocList -I symbols 158 | " Do default action for next item. 159 | nnoremap j :CocNext 160 | " Do default action for previous item. 161 | nnoremap k :CocPrev 162 | " Resume latest coc list. 163 | nnoremap p :CocListResume 164 | 165 | " Organize imports on save for Go files 166 | autocmd BufWritePre *.go :call CocAction('runCommand', 'editor.action.organizeImport') 167 | 168 | " Add Prettier command 169 | command! -nargs=0 Prettier :CocCommand prettier.formatFile 170 | 171 | " == end:coc.nvim settings == 172 | 173 | " Enable mouse support 174 | set mouse=a 175 | 176 | " Required for vim to be iMproved 177 | set nocompatible 178 | 179 | " Determines filetype from name to allow intelligent auto-indenting, etc. 180 | filetype indent plugin on 181 | 182 | " Enable syntax highlighting 183 | syntax enable 184 | colorscheme dracula 185 | 186 | " Moving lines up (Alt + Up Arrow) and down (Alt + Down Arrow) 187 | noremap ddp 188 | noremap :call feedkeys(line('.') == 1 ? '' : 'ddkP') 189 | 190 | " Better command-line completion 191 | set wildmenu 192 | 193 | " Use case insensitive search except when using capital letters 194 | set ignorecase 195 | set smartcase 196 | 197 | " When opening a new line and no file-specific indenting is enabled, 198 | " keep same indent as the line you're currently on 199 | set autoindent 200 | 201 | " Stricter indent rules for C 202 | set cindent 203 | 204 | " Display line numbers on the left 205 | set number 206 | 207 | " Number of visual spaces per TAB 208 | set tabstop=2 209 | 210 | " Number of spaces in TAB when editing 211 | set softtabstop=2 212 | 213 | " Number of spaces indented when reindent operations (>> and <<) are used 214 | set shiftwidth=2 215 | 216 | " Convert TABs to spaces 217 | set expandtab 218 | 219 | " Enable intelligent tabbing and spacing for indentation and alignment 220 | set smarttab 221 | 222 | " Set different indentation for PHP 223 | autocmd BufEnter *.php setlocal shiftwidth=4 tabstop=4 softtabstop=4 224 | 225 | " Set nonumber and disable mouse for .crt and .key files 226 | autocmd BufEnter *.key,*.crt setlocal nonumber mouse= 227 | 228 | " Add mint file type 229 | autocmd BufNewFile,BufRead *.mint set filetype=mint 230 | -------------------------------------------------------------------------------- /.config/micro/plug/lsp/help/lsp.md: -------------------------------------------------------------------------------- 1 | # Micro Plugin LSP Client 2 | 3 | LSP is a Language Server Protocol client. Features include function signatures 4 | and jump to definition. 5 | 6 | This help page can be viewed in Micro editor with Ctrl-E 'help lsp' 7 | 8 | ## Features and Shortcuts 9 | 10 | - Show function signature on status bar (alt-K) (textDocument/hover) 11 | - Open function definition in a new tab (alt-D) (textDocument/definition) 12 | - Format document (alt-F) (textDocument/formatting) 13 | - Show references to the current symbol in a buffer (alt-R) 14 | (textDocument/references), pressing return on the reference line, the 15 | reference's location is opened in a new tab 16 | 17 | There is initial support for completion (ctrl-space) (textDocument/completion). 18 | 19 | ## Supported languages 20 | 21 | Installation instructions for Go and Python are provided below. LSP Plugin has 22 | been briefly tested with 23 | 24 | - C++: [clangd](https://clangd.llvm.org) / 25 | [ccls](https://github.com/MaskRay/ccls) 26 | - go: [gopls](https://pkg.go.dev/golang.org/x/tools/gopls#section-readme) 27 | - markdown, JSON, typescript, javascript (including JSX/TSX): 28 | [deno](https://deno.land/) 29 | - python: pyls and [pylsp](https://github.com/python-lsp/python-lsp-server) 30 | - rust: [rls](https://github.com/rust-lang/rls) 31 | - lua: [lua-lsp](https://github.com/Alloyed/lua-lsp) 32 | 33 | ## Install LSP plugin 34 | 35 | $ micro --plugin install lsp 36 | 37 | To configure the LSP Plugin, you can add two lines to settings.json 38 | 39 | $ micro settings.json 40 | 41 | Add lines 42 | 43 | ```json 44 | { 45 | "lsp.server": "python=pylsp,go=gopls,typescript=deno lsp={\"importMap\": \"./import_map.json\"}", 46 | "lsp.formatOnSave": true 47 | } 48 | ``` 49 | 50 | Remember to add comma to previous line. Depending on the language server, 51 | automatic code formating can be quite opinionated. In that case, you can simply 52 | set lsp.formatOnSave to false. 53 | 54 | For Python language server, the currently maintained fork is 'pylsp'. If you 55 | wish to use the Palantir version (last updated in 2020) instead, set 56 | "python=pyls" in lsp.server. 57 | 58 | If your lsp.server settings are autoremoved, you can 59 | 60 | $ export MICRO_LSP='python=pylsp,go=gopls,typescript=deno lsp={"importMap":"import_map.json"},rust=rls' 61 | 62 | The lsp.server default settings (if no others are defined) are: 63 | 64 | ``` 65 | 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 66 | ``` 67 | 68 | ## Install Language Server 69 | 70 | To support each language, LSP plugin uses language servers. To use LSP plugin, 71 | you must install at least one language server. 72 | 73 | If you want to quickly test LSP plugin, Go language server gopls is simple to 74 | install. 75 | 76 | ### gopls, Go language server 77 | 78 | You will need command 'gopls' 79 | 80 | $ gopls version 81 | golang.org/x/tools/gopls v0.7.3 82 | 83 | In Debian, this is installed with 84 | 85 | $ sudo apt-get update 86 | $ sudo apt-get -y install golang-go gopls 87 | 88 | To test it, write a short go program 89 | 90 | $ micro hello.go 91 | 92 | ```go 93 | package main 94 | 95 | import "fmt" 96 | 97 | func main() { 98 | fmt.Println("hello world") 99 | } 100 | ``` 101 | 102 | Move cursor over Println and press alt-k. The function signature is shown on the 103 | bottom of the screen, in Micro status bar. It shows you what parameters the 104 | function can take. The signature should look similar to this: "func 105 | fmt.Println(a ...interface{}) (n int, err error)Println formats using the 106 | default formats..." 107 | 108 | Can you see the function signature with alt-k? If you can, you have succesfully 109 | installed Micro LSP plugin and GoPLS language server. 110 | 111 | Keep your cursor over Println, and press alt-d. The file defining Println opens. 112 | In this case, it's fmt/print.go. As Go reference documentation is in code 113 | comments, this is very convenient. You can navigate between tabs with atl-, 114 | (alt-comma) and alt-. (alt - full stop). To close the tab, press Ctrl-Q. 115 | 116 | ### Markdown, JSON/JSONC, Typescript, Javascript 117 | 118 | The Deno LSP server will provide full support for Typescript and Javascript. 119 | Additionally, it supports formatting for Markdown and JSON files. The 120 | installation of this is fairly straight forward: 121 | 122 | On Mac/Linux: 123 | 124 | $ curl -fsSL https://deno.land/install.sh | sh 125 | 126 | On Powershell: 127 | 128 | $ iwr https://deno.land/install.ps1 -useb | iex 129 | 130 | ### typescript-language-server 131 | 132 | This LSP server will allow for Javascript as well as Typescript support. For 133 | using it, you first need to install it using NPM: 134 | 135 | $ npm install -g typescript-language-server typescript 136 | 137 | Once it has been installed, you can use it like so: 138 | 139 | $ micro hello.js 140 | 141 | Press ctrl-e and type in: 142 | 143 | set lsp.server "typescript=typescript-language-server --stdio,javascript=typescript-language-server --stdio" 144 | 145 | After you restarted micro, you can use the features for typescript and 146 | javascript accordingly. 147 | 148 | ### pylsp, Python language server 149 | 150 | Installing Python language server PyLSP is a bit more involved. 151 | 152 | You will need 'virtualenv' command to create virtual environments and 'pip' to 153 | install Python packages. You can also use one of the many other commands for 154 | keeping your 'pip' packages in order. 155 | 156 | In Debian, these are installed with 157 | 158 | $ sudo apt-get update 159 | $ sudo apt-get install python-pip virtualenv 160 | 161 | Create a new virtual environment 162 | 163 | $ mkdir somePythonProject; cd somePythonProject 164 | $ virtualenv -p python3 env/ 165 | $ source env/bin/activate 166 | 167 | Your prompt likely shows "(env)" to confirm you're inside your virtual 168 | environment. 169 | 170 | List the packages you want installed. 171 | 172 | $ micro requirements.txt 173 | 174 | This list is to provide the most useful suggestions. If you would like to get a 175 | lot more opinionated advice, such as adding two empty lines between functions, 176 | you could use "python-lsp-server[all]". The mypy package provides optional 177 | static type checking. requirements.txt: 178 | 179 | ``` 180 | python-lsp-server[rope,pyflakes,mccabe,pylsp-mypy] 181 | pylsp-mypy 182 | ``` 183 | 184 | And actually install 185 | 186 | $ pip install -r requirements.txt 187 | 188 | No you can test your Python environment 189 | 190 | $ micro hello.py 191 | 192 | ```python 193 | def helloWorld(): 194 | return a 195 | ``` 196 | 197 | Save with Ctrl-S. A red warning sign ">>" lists up in the gutter, on the left 198 | side of Micro. Move cursor to the line "return a". The status bar shows the 199 | warning: "undefined name 'a'". Well done, you have now installed Python LSP 200 | support for Micro. 201 | 202 | MyPy provides optional static type setting. You can write normally, and type 203 | checking is ignored. You can define types for some functions, and you get 204 | automatic warnings for incorrect use of types. This is how types are marked: 205 | 206 | ```python 207 | def square(x: int) -> int: 208 | return x*x 209 | ``` 210 | 211 | Depending on your project, taste and installed linters, pylsp sometimes shows 212 | warnings you would like to hide. Hiding messages is possible using 213 | lsp.ignoreMessages, explained in later in this help document. 214 | 215 | ### lua-lsp, Lua language server 216 | 217 | These are the initial installation instructions. This installation will support 218 | linter messages in the gutter (on the left of editing area) and jump to 219 | definition inside the same file (alt-D). All LSP features are not yet supported 220 | with Lua. 221 | 222 | Install 'luarocks' command using your package manager. For example, on Debian 223 | 224 | $ sudo apt-get update 225 | $ sudo apt-get -y install luarocks 226 | 227 | Use luarocks to install helper packages used by lua-lsp 228 | 229 | $ sudo luarocks install luacheck 230 | $ sudo luarocks install Formatter 231 | $ sudo luarocks install lcf 232 | 233 | Install lua-lsp, the Lua language server 234 | 235 | $ sudo luarocks install --server=ssh://luarocks.org/dev lua-lsp 236 | 237 | This command uses different URL from official lua-lsp instructions due to 238 | [a change in how packages are downloaded](https://github.com/Alloyed/lua-lsp/issues/45). 239 | This command uses ssh instead of http. 240 | 241 | To test it, open a Lua file 242 | 243 | $ micro $HOME/.config/micro/plug/lsp/main.lua 244 | 245 | Can you see some linter warnings ">>" in the gutter? Can you jump to functions 246 | inside the same file with Alt-D? Well done, you've installed Lua LSP support for 247 | micro. 248 | 249 | All features don't work yet with Lua LSP. 250 | 251 | ### zls, ZIG language server 252 | 253 | The ZIG language server provides formatting, goto definition, auto-completion as 254 | well as hover and references. It can be installed by following 255 | [these instruction](https://github.com/zigtools/zls). 256 | 257 | Once installed, open micro, press ctrl+e and type the following command: 258 | 259 | set lsp.server zig=zls 260 | 261 | Close micro again and open a zig file. 262 | 263 | ## Ignoring unhelpful messages 264 | 265 | In addition to providing assistance while coding, some language servers can show 266 | spurious, unnecessary or too oppinionated messages. Sometimes, it's not obvious 267 | how these messages are disable using language server settings. 268 | 269 | This plugin allows you to selectively ignore unwanted warnings while keeping 270 | others. This is done my matching the start of the message. By default, nothing 271 | is ignored. 272 | 273 | Consider a case where you're working with an external Python project that 274 | indents with tabs. When joining an existing project, you might not want to 275 | impose your own conventions to every code file. On the other hand, LSP support 276 | is not useful if nearly every line is marked with a warning. 277 | 278 | Moving the cursor to a line with the warning, you see that the line starts with 279 | "W191 indentation contains tabs". This, and similar unhelpful messages (in the 280 | context of your current project) can be ignored by editing 281 | ~/.config/micro/settings.json 282 | 283 | ```json 284 | { 285 | "lsp.ignoreMessages": "Skipping analyzing |W191 indentation contains tabs|E101 indentation contains mixed spaces and tabs|See https://mypy.readthedocs.io/en" 286 | } 287 | ``` 288 | 289 | As you now open the same file, you can see that warning "W191 indentation 290 | contains tabs" is no longer shown. Also the warning mark ">>" in the gutter is 291 | gone. Try referring to a variable that does not exist, and you can see a helpful 292 | warning appear. You have now disabled the warnings you don't need, while keeping 293 | the useful ones. 294 | 295 | ## See also 296 | 297 | [Official repostory](https://github.com/AndCake/micro-plugin-lsp) 298 | 299 | [Usage examples with screenshots](https://terokarvinen.com/2022/micro-editor-lsp-support-python-and-go-jump-to-definition-show-function-signature/) 300 | 301 | [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) 302 | 303 | [gopls - the Go language server](https://pkg.go.dev/golang.org/x/tools/gopls) 304 | 305 | [pylsp - Python LSP Server](https://github.com/python-lsp/python-lsp-server) 306 | 307 | [mypy - Optional Static Typing for Python](http://mypy-lang.org/) 308 | 309 | [rls - Rust Language Server](https://github.com/rust-lang/rls) 310 | 311 | [deno](https://deno.land/) 312 | 313 | [typescript-language-server](https://www.npmjs.com/package/typescript-language-server) 314 | 315 | [lua-lsp - A Lua language server](https://github.com/Alloyed/lua-lsp) 316 | -------------------------------------------------------------------------------- /.config/micro/plug/lsp/main.lua: -------------------------------------------------------------------------------- 1 | VERSION = "0.6.2" 2 | 3 | local micro = import("micro") 4 | local config = import("micro/config") 5 | local shell = import("micro/shell") 6 | local util = import("micro/util") 7 | local buffer = import("micro/buffer") 8 | local fmt = import("fmt") 9 | local go_os = import("os") 10 | local path = import("path") 11 | local filepath = import("path/filepath") 12 | 13 | local cmd = {} 14 | local id = {} 15 | local version = {} 16 | local currentAction = {} 17 | local capabilities = {} 18 | local filetype = '' 19 | local rootUri = '' 20 | local message = '' 21 | local completionCursor = 0 22 | local lastCompletion = {} 23 | local splitBP = nil 24 | local tabCount = 0 25 | 26 | local json = {} 27 | 28 | function toBytes(str) 29 | local result = {} 30 | for i=1,#str do 31 | local b = str:byte(i) 32 | if b < 32 then 33 | table.insert(result, b) 34 | end 35 | end 36 | return result 37 | end 38 | 39 | function getUriFromBuf(buf) 40 | if buf == nil then return; end 41 | local file = buf.AbsPath 42 | local uri = fmt.Sprintf("file://%s", file) 43 | return uri 44 | end 45 | 46 | function mysplit (inputstr, sep) 47 | if sep == nil then 48 | sep = "%s" 49 | end 50 | local t={} 51 | for str in string.gmatch(inputstr, "([^"..sep.."]+)") do 52 | table.insert(t, str) 53 | end 54 | return t 55 | end 56 | 57 | function parseOptions(inputstr) 58 | local t = {} 59 | inputstr = inputstr:gsub("[%w+_-]+=[^=,]+={.-}", function (str) 60 | table.insert(t, str) 61 | return ''; 62 | end) 63 | inputstr = inputstr:gsub("[%w+_-]+=[^=,]+", function (str) 64 | table.insert(t, str) 65 | return ''; 66 | end) 67 | return t 68 | end 69 | 70 | function startServer(filetype, callback) 71 | local wd, _ = go_os.Getwd() 72 | rootUri = fmt.Sprintf("file://%s", wd) 73 | local envSettings, _ = go_os.Getenv("MICRO_LSP") 74 | local settings = config.GetGlobalOption("lsp.server") 75 | 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" 76 | if envSettings ~= nil and #envSettings > 0 then 77 | settings = envSettings 78 | end 79 | if settings ~= nil and #settings > 0 then 80 | settings = settings .. "," .. fallback 81 | else 82 | settings = fallback 83 | end 84 | local server = parseOptions(settings) 85 | micro.Log("Server Options", server) 86 | for i in pairs(server) do 87 | local part = mysplit(server[i], "=") 88 | local run = mysplit(part[2], "%s") 89 | local initOptions = part[3] or '{}' 90 | local runCmd = table.remove(run, 1) 91 | local args = run 92 | if filetype == part[1] then 93 | local send = withSend(part[1]) 94 | if cmd[part[1]] ~= nil then return; end 95 | id[part[1]] = 0 96 | micro.Log("Starting server", part[1]) 97 | cmd[part[1]] = shell.JobSpawn(runCmd, args, onStdout(part[1]), onStderr, onExit(part[1]), {}) 98 | currentAction[part[1]] = { method = "initialize", response = function (bp, data) 99 | send("initialized", "{}", true) 100 | capabilities[filetype] = data.result and data.result.capabilities or {} 101 | callback(bp.Buf, filetype) 102 | end } 103 | 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)) 104 | return 105 | end 106 | end 107 | end 108 | 109 | function init() 110 | 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") 111 | config.RegisterCommonOption("lsp", "formatOnSave", true) 112 | config.RegisterCommonOption("lsp", "autocompleteDetails", false) 113 | config.RegisterCommonOption("lsp", "ignoreMessages", "") 114 | config.RegisterCommonOption("lsp", "tabcompletion", true) 115 | config.RegisterCommonOption("lsp", "ignoreTriggerCharacters", "completion") 116 | -- example to ignore all LSP server message starting with these strings: 117 | -- "lsp.ignoreMessages": "Skipping analyzing |See https://" 118 | 119 | config.MakeCommand("hover", hoverAction, config.NoComplete) 120 | config.MakeCommand("definition", definitionAction, config.NoComplete) 121 | config.MakeCommand("lspcompletion", completionAction, config.NoComplete) 122 | config.MakeCommand("format", formatAction, config.NoComplete) 123 | config.MakeCommand("references", referencesAction, config.NoComplete) 124 | 125 | config.TryBindKey("Alt-k", "command:hover", false) 126 | config.TryBindKey("Alt-d", "command:definition", false) 127 | config.TryBindKey("Alt-f", "command:format", false) 128 | config.TryBindKey("Alt-r", "command:references", false) 129 | config.TryBindKey("CtrlSpace", "command:lspcompletion", false) 130 | 131 | config.AddRuntimeFile("lsp", config.RTHelp, "help/lsp.md") 132 | 133 | -- @TODO register additional actions here 134 | end 135 | 136 | function withSend(filetype) 137 | return function (method, params, isNotification) 138 | if cmd[filetype] == nil then 139 | return 140 | end 141 | 142 | local msg = fmt.Sprintf('{"jsonrpc": "2.0", %s"method": "%s", "params": %s}', not isNotification and fmt.Sprintf('"id": %.0f, ', id[filetype]) or "", method, params) 143 | id[filetype] = id[filetype] + 1 144 | msg = fmt.Sprintf("Content-Length: %.0f\r\n\r\n%s", #msg, msg) 145 | --micro.Log("send", filetype, "sending", method or msg, msg) 146 | shell.JobSend(cmd[filetype], msg) 147 | end 148 | end 149 | 150 | function preRune(bp, r) 151 | if splitBP ~= nil then 152 | pcall(function () splitBP:Unsplit(); end) 153 | splitBP = nil 154 | local cur = bp.Buf:GetActiveCursor() 155 | cur:Deselect(false); 156 | cur:GotoLoc(buffer.Loc(cur.X + 1, cur.Y)) 157 | end 158 | end 159 | 160 | -- when a new character is types, the document changes 161 | function onRune(bp, r) 162 | local filetype = bp.Buf:FileType() 163 | if cmd[filetype] == nil then 164 | return 165 | end 166 | if splitBP ~= nil then 167 | pcall(function () splitBP:Unsplit(); end) 168 | splitBP = nil 169 | end 170 | 171 | local send = withSend(filetype) 172 | local uri = getUriFromBuf(bp.Buf) 173 | if r ~= nil then 174 | lastCompletion = {} 175 | end 176 | -- allow the document contents to be escaped properly for the JSON string 177 | local content = util.String(bp.Buf:Bytes()):gsub("\\", "\\\\"):gsub("\n", "\\n"):gsub("\r", "\\r"):gsub('"', '\\"'):gsub("\t", "\\t") 178 | -- increase change version 179 | version[uri] = (version[uri] or 0) + 1 180 | send("textDocument/didChange", fmt.Sprintf('{"textDocument": {"version": %.0f, "uri": "%s"}, "contentChanges": [{"text": "%s"}]}', version[uri], uri, content), true) 181 | local ignored = mysplit(config.GetGlobalOption("lsp.ignoreTriggerCharacters") or '', ",") 182 | if r and capabilities[filetype] then 183 | if not contains(ignored, "completion") and capabilities[filetype].completionProvider and capabilities[filetype].completionProvider.triggerCharacters and contains(capabilities[filetype].completionProvider.triggerCharacters, r) then 184 | completionAction(bp) 185 | elseif not contains(ignored, "signature") and capabilities[filetype].signatureHelpProvider and capabilities[filetype].signatureHelpProvider.triggerCharacters and contains(capabilities[filetype].signatureHelpProvider.triggerCharacters, r) then 186 | hoverAction(bp) 187 | end 188 | end 189 | end 190 | 191 | -- alias functions for any kind of change to the document 192 | -- @TODO: add missing ones 193 | function onBackspace(bp) onRune(bp); end 194 | function onCut(bp) onRune(bp); end 195 | function onCutLine(bp) onRune(bp); end 196 | function onDuplicateLine(bp) onRune(bp); end 197 | function onDeleteLine(bp) onRune(bp); end 198 | function onDelete(bp) onRune(bp); end 199 | function onUndo(bp) onRune(bp); end 200 | function onRedo(bp) onRune(bp); end 201 | function onIndent(bp) onRune(bp); end 202 | function onIndentSelection(bp) onRune(bp); end 203 | function onPaste(bp) onRune(bp); end 204 | function onSave(bp) onRune(bp); end 205 | 206 | function onEscape(bp) 207 | if splitBP ~= nil then 208 | pcall(function () splitBP:Unsplit(); end) 209 | splitBP = nil 210 | end 211 | end 212 | 213 | function preInsertNewline(bp) 214 | if bp.Buf.Path == "References found" then 215 | local cur = bp.Buf:GetActiveCursor() 216 | cur:SelectLine() 217 | local data = util.String(cur:GetSelection()) 218 | local file, line, character = data:match("(./[^:]+):([^:]+):([^:]+)") 219 | local doc, _ = file:gsub("^file://", "") 220 | buf, _ = buffer.NewBufferFromFile(doc) 221 | bp:AddTab() 222 | micro.CurPane():OpenBuffer(buf) 223 | buf:GetActiveCursor():GotoLoc(buffer.Loc(character * 1, line * 1)) 224 | micro.CurPane():Center() 225 | return false 226 | end 227 | end 228 | 229 | function preSave(bp) 230 | if config.GetGlobalOption("lsp.formatOnSave") then 231 | onRune(bp) 232 | formatAction(bp, function () 233 | bp:Save() 234 | end) 235 | end 236 | end 237 | 238 | function handleInitialized(buf, filetype) 239 | if cmd[filetype] == nil then return; end 240 | micro.Log("Found running lsp server for ", filetype, "firing textDocument/didOpen...") 241 | local send = withSend(filetype) 242 | local uri = getUriFromBuf(buf) 243 | local content = util.String(buf:Bytes()):gsub("\\", "\\\\"):gsub("\n", "\\n"):gsub("\r", "\\r"):gsub('"', '\\"'):gsub("\t", "\\t") 244 | send("textDocument/didOpen", fmt.Sprintf('{"textDocument": {"uri": "%s", "languageId": "%s", "version": 1, "text": "%s"}}', uri, filetype, content), true) 245 | end 246 | 247 | function onBufferOpen(buf) 248 | local filetype = buf:FileType() 249 | micro.Log("ONBUFFEROPEN", filetype) 250 | if filetype ~= "unknown" and rootUri == "" and not cmd[filetype] then return startServer(filetype, handleInitialized); end 251 | if cmd[filetype] then 252 | handleInitialized(buf, filetype) 253 | end 254 | end 255 | 256 | function contains(list, x) 257 | for _, v in pairs(list) do 258 | if v == x then return true; end 259 | end 260 | return false 261 | end 262 | 263 | function string.starts(String, Start) 264 | return string.sub(String, 1, #Start) == Start 265 | end 266 | 267 | function string.ends(String, End) 268 | return string.sub(String, #String - (#End - 1), #String) == End 269 | end 270 | 271 | function string.random(CharSet, Length, prefix) 272 | 273 | local _CharSet = CharSet or '.' 274 | 275 | if _CharSet == '' then 276 | return '' 277 | else 278 | local Result = prefix or "" 279 | math.randomseed(os.time()) 280 | for Loop = 1,Length do 281 | local char = math.random(1, #CharSet) 282 | Result = Result .. CharSet:sub(char,char) 283 | end 284 | 285 | return Result 286 | end 287 | end 288 | 289 | function string.parse(text) 290 | if not text:find('"jsonrpc":') then return {}; end 291 | local start,fin = text:find("\n%s*\n") 292 | local cleanedText = text 293 | if fin ~= nil then 294 | cleanedText = text:sub(fin) 295 | end 296 | local status, res = pcall(json.parse, cleanedText) 297 | if status then 298 | return res 299 | end 300 | return false 301 | end 302 | 303 | function isIgnoredMessage(msg) 304 | -- Return true if msg matches one of the ignored starts of messages 305 | -- Useful for linters that show spurious, hard to disable warnings 306 | local ignoreList = mysplit(config.GetGlobalOption("lsp.ignoreMessages"), "|") 307 | for i, ignore in pairs(ignoreList) do 308 | if string.match(msg, ignore) then -- match from start of string 309 | micro.Log("Ignore message: '", msg, "', because it matched: '", ignore, "'.") 310 | return true -- ignore this message, dont show to user 311 | end 312 | end 313 | return false -- show this message to user 314 | end 315 | 316 | function onStdout(filetype) 317 | return function (text) 318 | if text:starts("Content-Length:") then 319 | message = text 320 | else 321 | message = message .. text 322 | end 323 | if not text:ends("}") then 324 | return 325 | end 326 | local data = message:parse() 327 | if data == false then 328 | return 329 | end 330 | 331 | if data.method == "workspace/configuration" then 332 | -- actually needs to respond with the same ID as the received JSON 333 | local message = fmt.Sprintf('{"jsonrpc": "2.0", "id": %.0f, "result": [{"enable": true}]}', data.id) 334 | shell.JobSend(cmd[filetype], fmt.Sprintf('Content-Length: %.0f\n\n%s', #message, message)) 335 | elseif data.method == "textDocument/publishDiagnostics" or data.method == "textDocument\\/publishDiagnostics" then 336 | -- react to server-published event 337 | local bp = micro.CurPane().Buf 338 | bp:ClearMessages("lsp") 339 | bp:AddMessage(buffer.NewMessage("lsp", "", buffer.Loc(0, 10000000), buffer.Loc(0, 10000000), buffer.MTInfo)) 340 | local uri = getUriFromBuf(bp) 341 | if data.params.uri == uri then 342 | for _, diagnostic in ipairs(data.params.diagnostics) do 343 | local type = buffer.MTInfo 344 | if diagnostic.severity == 1 then 345 | type = buffer.MTError 346 | elseif diagnostic.severity == 2 then 347 | type = buffer.MTWarning 348 | end 349 | local mstart = buffer.Loc(diagnostic.range.start.character, diagnostic.range.start.line) 350 | local mend = buffer.Loc(diagnostic.range["end"].character, diagnostic.range["end"].line) 351 | 352 | if not isIgnoredMessage(diagnostic.message) then 353 | msg = buffer.NewMessage("lsp", diagnostic.message, mstart, mend, type) 354 | bp:AddMessage(msg) 355 | end 356 | end 357 | end 358 | elseif currentAction[filetype] and currentAction[filetype].method and not data.method and currentAction[filetype].response and data.jsonrpc then -- react to custom action event 359 | local bp = micro.CurPane() 360 | micro.Log("Received message for ", filetype, data) 361 | currentAction[filetype].response(bp, data) 362 | currentAction[filetype] = {} 363 | elseif data.method == "window/showMessage" or data.method == "window\\/showMessage" then 364 | if filetype == micro.CurPane().Buf:FileType() then 365 | micro.InfoBar():Message(data.params.message) 366 | else 367 | micro.Log(filetype .. " message " .. data.params.message) 368 | end 369 | elseif data.method == "window/logMessage" or data.method == "window\\/logMessage" then 370 | micro.Log(data.params.message) 371 | elseif message:starts("Content-Length:") then 372 | if message:find('"') and not message:find('"result":null') then 373 | micro.Log("Unhandled message 1", filetype, message) 374 | end 375 | else 376 | -- enable for debugging purposes 377 | micro.Log("Unhandled message 2", filetype, message) 378 | end 379 | end 380 | end 381 | 382 | function onStderr(text) 383 | micro.Log("ONSTDERR", text) 384 | --micro.InfoBar():Message(text) 385 | end 386 | 387 | function onExit(filetype) 388 | return function (str) 389 | currentAction[filetype] = nil 390 | cmd[filetype] = nil 391 | micro.Log("ONEXIT", filetype, str) 392 | end 393 | end 394 | 395 | -- the actual hover action request and response 396 | -- the hoverActionResponse is hooked up in 397 | function hoverAction(bp) 398 | local filetype = bp.Buf:FileType() 399 | if cmd[filetype] ~= nil then 400 | local send = withSend(filetype) 401 | local file = bp.Buf.AbsPath 402 | local line = bp.Buf:GetActiveCursor().Y 403 | local char = bp.Buf:GetActiveCursor().X 404 | currentAction[filetype] = { method = "textDocument/hover", response = hoverActionResponse } 405 | send(currentAction[filetype].method, fmt.Sprintf('{"textDocument": {"uri": "file://%s"}, "position": {"line": %.0f, "character": %.0f}}', file, line, char)) 406 | end 407 | end 408 | 409 | function hoverActionResponse(buf, data) 410 | if data.result and data.result.contents ~= nil and data.result.contents ~= "" then 411 | if data.result.contents.value then 412 | micro.InfoBar():Message(data.result.contents.value) 413 | elseif #data.result.contents > 0 then 414 | micro.InfoBar():Message(data.result.contents[1].value) 415 | end 416 | end 417 | end 418 | 419 | -- the definition action request and response 420 | function definitionAction(bp) 421 | local filetype = bp.Buf:FileType() 422 | if cmd[filetype] == nil then return; end 423 | 424 | local send = withSend(filetype) 425 | local file = bp.Buf.AbsPath 426 | local line = bp.Buf:GetActiveCursor().Y 427 | local char = bp.Buf:GetActiveCursor().X 428 | currentAction[filetype] = { method = "textDocument/definition", response = definitionActionResponse } 429 | send(currentAction[filetype].method, fmt.Sprintf('{"textDocument": {"uri": "file://%s"}, "position": {"line": %.0f, "character": %.0f}}', file, line, char)) 430 | end 431 | 432 | function definitionActionResponse(bp, data) 433 | local results = data.result or data.partialResult 434 | if results == nil then return; end 435 | local file = bp.Buf.AbsPath 436 | if results.uri ~= nil then 437 | -- single result 438 | results = { results } 439 | end 440 | if #results <= 0 then return; end 441 | local uri = (results[1].uri or results[1].targetUri) 442 | local doc = uri:gsub("^file://", "") 443 | local buf = bp.Buf 444 | if file ~= doc then 445 | -- it's from a different file, so open it as a new tab 446 | buf, _ = buffer.NewBufferFromFile(doc) 447 | bp:AddTab() 448 | micro.CurPane():OpenBuffer(buf) 449 | -- shorten the displayed name in status bar 450 | name = buf:GetName() 451 | local wd, _ = go_os.Getwd() 452 | if name:starts(wd) then 453 | buf:SetName("." .. name:sub(#wd + 1, #name + 1)) 454 | else 455 | if #name > 30 then 456 | buf:SetName("..." .. name:sub(-30, #name + 1)) 457 | end 458 | end 459 | end 460 | local range = results[1].range or results[1].targetSelectionRange 461 | buf:GetActiveCursor():GotoLoc(buffer.Loc(range.start.character, range.start.line)) 462 | bp:Center() 463 | end 464 | 465 | function completionAction(bp) 466 | local filetype = bp.Buf:FileType() 467 | local send = withSend(filetype) 468 | local file = bp.Buf.AbsPath 469 | local line = bp.Buf:GetActiveCursor().Y 470 | local char = bp.Buf:GetActiveCursor().X 471 | 472 | if lastCompletion[1] == file and lastCompletion[2] == line and lastCompletion[3] == char then 473 | completionCursor = completionCursor + 1 474 | else 475 | completionCursor = 0 476 | if bp.Cursor:HasSelection() then 477 | -- we have a selection 478 | -- assume we want to indent the selection 479 | bp:IndentSelection() 480 | return 481 | end 482 | if char == 0 then 483 | -- we are at the very first character of a line 484 | -- assume we want to indent 485 | bp:IndentLine() 486 | return 487 | end 488 | local cur = bp.Buf:GetActiveCursor() 489 | cur:SelectLine() 490 | local lineContent = util.String(cur:GetSelection()) 491 | cur:ResetSelection() 492 | cur:GotoLoc(buffer.Loc(char, line)) 493 | local startOfLine = "" .. lineContent:sub(1, char) 494 | if startOfLine:match("^%s+$") then 495 | -- we are at the beginning of a line 496 | -- assume we want to indent the line 497 | bp:IndentLine() 498 | return 499 | end 500 | end 501 | if cmd[filetype] == nil then return; end 502 | lastCompletion = {file, line, char} 503 | currentAction[filetype] = { method = "textDocument/completion", response = completionActionResponse } 504 | send(currentAction[filetype].method, fmt.Sprintf('{"textDocument": {"uri": "file://%s"}, "position": {"line": %.0f, "character": %.0f}}', file, line, char)) 505 | end 506 | 507 | table.filter = function(t, filterIter) 508 | local out = {} 509 | 510 | for k, v in pairs(t) do 511 | if filterIter(v, k, t) then table.insert(out, v) end 512 | end 513 | 514 | return out 515 | end 516 | 517 | function findCommon(input, list) 518 | local commonLen = 0 519 | local prefixList = {} 520 | local str = input.textEdit and input.textEdit.newText or input.label 521 | for i = 1,#str,1 do 522 | local prefix = str:sub(1, i) 523 | prefixList[prefix] = 0 524 | for idx, entry in ipairs(list) do 525 | local currentEntry = entry.textEdit and entry.textEdit.newText or entry.label 526 | if currentEntry:starts(prefix) then 527 | prefixList[prefix] = prefixList[prefix] + 1 528 | end 529 | end 530 | end 531 | local longest = "" 532 | for idx, entry in pairs(prefixList) do 533 | if entry >= #list then 534 | if #longest < #idx then 535 | longest = idx 536 | end 537 | end 538 | end 539 | if #list == 1 then 540 | return list[1].textEdit and list[1].textEdit.newText or list[1].label 541 | end 542 | return longest 543 | end 544 | 545 | function completionActionResponse(bp, data) 546 | local results = data.result 547 | if results == nil then 548 | return 549 | end 550 | if results.items then 551 | results = results.items 552 | end 553 | 554 | local xy = buffer.Loc(bp.Cursor.X, bp.Cursor.Y) 555 | local start = xy 556 | if bp.Cursor:HasSelection() then 557 | bp.Cursor:DeleteSelection() 558 | end 559 | 560 | local found = false 561 | local prefix = "" 562 | local reversed = "" 563 | -- if we have no defined ranges in the result 564 | -- try to find out what our prefix is we want to filter against 565 | if not results[1] or not results[1].textEdit or not results[1].textEdit.range then 566 | if capabilities[bp.Buf:FileType()] and capabilities[bp.Buf:FileType()].completionProvider and capabilities[bp.Buf:FileType()].completionProvider.triggerCharacters then 567 | local cur = bp.Buf:GetActiveCursor() 568 | cur:SelectLine() 569 | local lineContent = util.String(cur:GetSelection()) 570 | reversed = string.reverse(lineContent:gsub("\r?\n$", ""):sub(1, xy.X)) 571 | local triggerChars = capabilities[bp.Buf:FileType()].completionProvider.triggerCharacters 572 | for i = 1,#reversed,1 do 573 | local char = reversed:sub(i,i) 574 | -- try to find a trigger character or any other non-word character 575 | if contains(triggerChars, char) or contains({" ", ":", "/", "-", "\t", ";"}, char) then 576 | found = true 577 | start = buffer.Loc(#reversed - (i - 1), bp.Cursor.Y) 578 | bp.Cursor:SetSelectionStart(start) 579 | bp.Cursor:SetSelectionEnd(xy) 580 | prefix = util.String(cur:GetSelection()) 581 | bp.Cursor:DeleteSelection() 582 | bp.Cursor:ResetSelection() 583 | break 584 | end 585 | end 586 | if not found then 587 | prefix = lineContent:gsub("\r?\n$", '') 588 | end 589 | end 590 | -- if we have found a prefix 591 | if prefix ~= "" then 592 | -- filter it down to what is suggested by the prefix 593 | results = table.filter(results, function (entry) 594 | return entry.label:starts(prefix) 595 | end) 596 | end 597 | end 598 | 599 | table.sort(results, function (left, right) 600 | return (left.sortText or left.label) < (right.sortText or right.label) 601 | end) 602 | 603 | entry = results[(completionCursor % #results) + 1] 604 | -- if no matching results are found 605 | if entry == nil then 606 | -- reposition cursor and stop 607 | bp.Cursor:GotoLoc(xy) 608 | return 609 | end 610 | local commonStart = '' 611 | local toInsert = entry.textEdit and entry.textEdit.newText or entry.label 612 | local isTabCompletion = config.GetGlobalOption("lsp.tabcompletion") 613 | if isTabCompletion and not entry.textEdit then 614 | commonStart = findCommon(entry, results) 615 | bp.Buf:Insert(start, commonStart) 616 | if prefix ~= commonStart then 617 | return 618 | end 619 | start = buffer.Loc(start.X + #prefix, start.Y) 620 | else 621 | prefix = '' 622 | end 623 | 624 | if entry.textEdit and entry.textEdit.range then 625 | start = buffer.Loc(entry.textEdit.range.start.character, entry.textEdit.range.start.line) 626 | bp.Cursor:SetSelectionStart(start) 627 | bp.Cursor:SetSelectionEnd(xy) 628 | bp.Cursor:DeleteSelection() 629 | bp.Cursor:ResetSelection() 630 | elseif capabilities[bp.Buf:FileType()] and capabilities[bp.Buf:FileType()].completionProvider and capabilities[bp.Buf:FileType()].completionProvider.triggerCharacters then 631 | if not found then 632 | -- we found nothing - so assume we need the beginning of the line 633 | if reversed:starts(" ") or reversed:starts("\t") then 634 | -- if we end with some indentation, keep it 635 | start = buffer.Loc(#reversed, bp.Cursor.Y) 636 | else 637 | start = buffer.Loc(0, bp.Cursor.Y) 638 | end 639 | bp.Cursor:SetSelectionStart(start) 640 | bp.Cursor:SetSelectionEnd(xy) 641 | bp.Cursor:DeleteSelection() 642 | bp.Cursor:ResetSelection() 643 | end 644 | end 645 | local inserting = "" .. toInsert:gsub(prefix, "") 646 | bp.Buf:Insert(start, inserting) 647 | 648 | if #results > 1 then 649 | if entry.textEdit then 650 | bp.Cursor:GotoLoc(start) 651 | bp.Cursor:SetSelectionStart(start) 652 | else 653 | -- if we had to calculate everything outselves 654 | -- go back to the original location 655 | bp.Cursor:GotoLoc(xy) 656 | bp.Cursor:SetSelectionStart(xy) 657 | end 658 | bp.Cursor:SetSelectionEnd(buffer.Loc(start.X + #toInsert, start.Y)) 659 | else 660 | bp.Cursor:GotoLoc(buffer.Loc(start.X + #inserting, start.Y)) 661 | end 662 | 663 | local startLoc = buffer.Loc(0, 0) 664 | local endLoc = buffer.Loc(0, 0) 665 | local msg = '' 666 | local insertion = '' 667 | if entry.detail or entry.documentation then 668 | insertion = fmt.Sprintf("%s", entry.detail or entry.documentation or '') 669 | for idx, result in ipairs(results) do 670 | if #msg > 0 then 671 | msg = msg .. "\n" 672 | end 673 | local insertion = fmt.Sprintf("%s %s", result.detail or '', result.documentation or '') 674 | if idx == (completionCursor % #results) + 1 then 675 | local msglines = mysplit(msg, "\n") 676 | startLoc = buffer.Loc(0, #msglines) 677 | endLoc = buffer.Loc(#insertion - 1, #msglines) 678 | end 679 | msg = msg .. insertion 680 | end 681 | else 682 | insertion = entry.label 683 | for idx, result in ipairs(results) do 684 | if #msg > 0 then 685 | local msglines = mysplit(msg, "\n") 686 | local lastLine = msglines[#msglines] 687 | local len = #result.label + 4 688 | if #lastLine + len >= bp:GetView().Width then 689 | msg = msg .. "\n " 690 | else 691 | msg = msg .. ' ' 692 | end 693 | else 694 | msg = " " 695 | end 696 | if idx == (completionCursor % #results) + 1 then 697 | local msglines = mysplit(msg, "\n") 698 | local prefixLen = 0 699 | if #msglines > 0 then 700 | prefixLen = #msglines[#msglines] 701 | else 702 | prefixLen = #msg 703 | end 704 | startLoc = buffer.Loc(prefixLen or 0, #msglines - 1) 705 | endLoc = buffer.Loc(prefixLen + #result.label, #msglines - 1) 706 | end 707 | msg = msg .. result.label 708 | end 709 | end 710 | if config.GetGlobalOption("lsp.autocompleteDetails") then 711 | if not splitBP then 712 | local tmpName = ("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"):random(32) 713 | local logBuf = buffer.NewBuffer(msg, tmpName) 714 | splitBP = bp:HSplitBuf(logBuf) 715 | bp:NextSplit() 716 | else 717 | splitBP:SelectAll() 718 | splitBP.Cursor:DeleteSelection() 719 | splitBP.Cursor:ResetSelection() 720 | splitBP.Buf:insert(buffer.Loc(1, 1), msg) 721 | end 722 | splitBP.Cursor:ResetSelection() 723 | splitBP.Cursor:SetSelectionStart(startLoc) 724 | splitBP.Cursor:SetSelectionEnd(endLoc) 725 | else 726 | if entry.detail or entry.documentation then 727 | micro.InfoBar():Message(insertion) 728 | else 729 | local cleaned = " " .. msg:gsub("%s+", " ") 730 | local replaced, _ = cleaned:gsub(".*%s" .. insertion .. "%s?", " [" .. insertion .. "] ") 731 | micro.InfoBar():Message(replaced) 732 | end 733 | end 734 | end 735 | 736 | function formatAction(bp, callback) 737 | local filetype = bp.Buf:FileType() 738 | if cmd[filetype] == nil then return; end 739 | local send = withSend(filetype) 740 | local file = bp.Buf.AbsPath 741 | 742 | currentAction[filetype] = { method = "textDocument/formatting", response = formatActionResponse(callback) } 743 | send(currentAction[filetype].method, fmt.Sprintf('{"textDocument": {"uri": "file://%s"}, "options": {"tabSize": 4, "insertSpaces": true}}', file)) 744 | end 745 | 746 | function formatActionResponse(callback) 747 | return function (bp, data) 748 | if data.result == nil then return; end 749 | local edits = data.result 750 | -- make sure we apply the changes from back to front 751 | -- this allows for changes to not need position updates 752 | table.sort(edits, function (left, right) 753 | -- go by lines first 754 | return left.range['end'].line > right.range['end'].line or 755 | -- if lines match, go by end character 756 | left.range['end'].line == right.range['end'].line and left.range['end'].character > right.range['end'].character or 757 | -- if they match too, go by start character 758 | 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 759 | end) 760 | 761 | -- save original cursor position 762 | local xy = buffer.Loc(bp.Cursor.X, bp.Cursor.Y) 763 | for _idx, edit in ipairs(edits) do 764 | rangeStart = buffer.Loc(edit.range.start.character, edit.range.start.line) 765 | rangeEnd = buffer.Loc(edit.range['end'].character, edit.range['end'].line) 766 | -- apply each change 767 | bp.Cursor:GotoLoc(rangeStart) 768 | bp.Cursor:SetSelectionStart(rangeStart) 769 | bp.Cursor:SetSelectionEnd(rangeEnd) 770 | bp.Cursor:DeleteSelection() 771 | bp.Cursor:ResetSelection() 772 | 773 | if edit.newText ~= "" then 774 | bp.Buf:insert(rangeStart, edit.newText) 775 | end 776 | end 777 | -- put the cursor back where it was 778 | bp.Cursor:GotoLoc(xy) 779 | -- if any changes were applied 780 | if #edits > 0 then 781 | -- tell the server about the changed document 782 | onRune(bp) 783 | end 784 | 785 | if callback ~= nil then 786 | callback(bp) 787 | end 788 | end 789 | end 790 | 791 | -- the references action request and response 792 | function referencesAction(bp) 793 | local filetype = bp.Buf:FileType() 794 | if cmd[filetype] == nil then return; end 795 | 796 | local send = withSend(filetype) 797 | local file = bp.Buf.AbsPath 798 | local line = bp.Buf:GetActiveCursor().Y 799 | local char = bp.Buf:GetActiveCursor().X 800 | currentAction[filetype] = { method = "textDocument/references", response = referencesActionResponse } 801 | send(currentAction[filetype].method, fmt.Sprintf('{"textDocument": {"uri": "file://%s"}, "position": {"line": %.0f, "character": %.0f}, "context": {"includeDeclaration":true}}', file, line, char)) 802 | end 803 | 804 | function referencesActionResponse(bp, data) 805 | if data.result == nil then return; end 806 | local results = data.result or data.partialResult 807 | if results == nil or #results <= 0 then return; end 808 | 809 | local file = bp.Buf.AbsPath 810 | 811 | local msg = '' 812 | for _idx, ref in ipairs(results) do 813 | if msg ~= '' then msg = msg .. '\n'; end 814 | local doc = (ref.uri or ref.targetUri) 815 | msg = msg .. "." .. doc:sub(#rootUri + 1, #doc) .. ":" .. ref.range.start.line .. ":" .. ref.range.start.character 816 | end 817 | 818 | local logBuf = buffer.NewBuffer(msg, "References found") 819 | local splitBP = bp:HSplitBuf(logBuf) 820 | end 821 | 822 | -- 823 | -- @TODO implement additional functions here... 824 | -- 825 | 826 | 827 | 828 | -- 829 | -- JSON 830 | -- 831 | -- Internal functions. 832 | 833 | local function kind_of(obj) 834 | if type(obj) ~= 'table' then return type(obj) end 835 | local i = 1 836 | for _ in pairs(obj) do 837 | if obj[i] ~= nil then i = i + 1 else return 'table' end 838 | end 839 | if i == 1 then return 'table' else return 'array' end 840 | end 841 | 842 | local function escape_str(s) 843 | local in_char = {'\\', '"', '/', '\b', '\f', '\n', '\r', '\t'} 844 | local out_char = {'\\', '"', '/', 'b', 'f', 'n', 'r', 't'} 845 | for i, c in ipairs(in_char) do 846 | s = s:gsub(c, '\\' .. out_char[i]) 847 | end 848 | return s 849 | end 850 | 851 | -- Returns pos, did_find; there are two cases: 852 | -- 1. Delimiter found: pos = pos after leading space + delim; did_find = true. 853 | -- 2. Delimiter not found: pos = pos after leading space; did_find = false. 854 | -- This throws an error if err_if_missing is true and the delim is not found. 855 | local function skip_delim(str, pos, delim, err_if_missing) 856 | pos = pos + #str:match('^%s*', pos) 857 | if str:sub(pos, pos) ~= delim then 858 | if err_if_missing then 859 | error('Expected ' .. delim .. ' near position ' .. pos) 860 | end 861 | return pos, false 862 | end 863 | return pos + 1, true 864 | end 865 | 866 | -- Expects the given pos to be the first character after the opening quote. 867 | -- Returns val, pos; the returned pos is after the closing quote character. 868 | local function parse_str_val(str, pos, val) 869 | val = val or '' 870 | local early_end_error = 'End of input found while parsing string.' 871 | if pos > #str then error(early_end_error) end 872 | local c = str:sub(pos, pos) 873 | if c == '"' then return val, pos + 1 end 874 | if c ~= '\\' then return parse_str_val(str, pos + 1, val .. c) end 875 | -- We must have a \ character. 876 | local esc_map = {b = '\b', f = '\f', n = '\n', r = '\r', t = '\t'} 877 | local nextc = str:sub(pos + 1, pos + 1) 878 | if not nextc then error(early_end_error) end 879 | return parse_str_val(str, pos + 2, val .. (esc_map[nextc] or nextc)) 880 | end 881 | 882 | -- Returns val, pos; the returned pos is after the number's final character. 883 | local function parse_num_val(str, pos) 884 | local num_str = str:match('^-?%d+%.?%d*[eE]?[+-]?%d*', pos) 885 | local val = tonumber(num_str) 886 | if not val then error('Error parsing number at position ' .. pos .. '.') end 887 | return val, pos + #num_str 888 | end 889 | 890 | json.null = {} -- This is a one-off table to represent the null value. 891 | 892 | function json.parse(str, pos, end_delim) 893 | pos = pos or 1 894 | if pos > #str then error('Reached unexpected end of input.' .. str) end 895 | local pos = pos + #str:match('^%s*', pos) -- Skip whitespace. 896 | local first = str:sub(pos, pos) 897 | if first == '{' then -- Parse an object. 898 | local obj, key, delim_found = {}, true, true 899 | pos = pos + 1 900 | while true do 901 | key, pos = json.parse(str, pos, '}') 902 | if key == nil then return obj, pos end 903 | if not delim_found then error('Comma missing between object items.') end 904 | pos = skip_delim(str, pos, ':', true) -- true -> error if missing. 905 | obj[key], pos = json.parse(str, pos) 906 | pos, delim_found = skip_delim(str, pos, ',') 907 | end 908 | elseif first == '[' then -- Parse an array. 909 | local arr, val, delim_found = {}, true, true 910 | pos = pos + 1 911 | while true do 912 | val, pos = json.parse(str, pos, ']') 913 | if val == nil then return arr, pos end 914 | if not delim_found then error('Comma missing between array items.') end 915 | arr[#arr + 1] = val 916 | pos, delim_found = skip_delim(str, pos, ',') 917 | end 918 | elseif first == '"' then -- Parse a string. 919 | return parse_str_val(str, pos + 1) 920 | elseif first == '-' or first:match('%d') then -- Parse a number. 921 | return parse_num_val(str, pos) 922 | elseif first == end_delim then -- End of an object or array. 923 | return nil, pos + 1 924 | else -- Parse true, false, or null. 925 | local literals = {['true'] = true, ['false'] = false, ['null'] = json.null} 926 | for lit_str, lit_val in pairs(literals) do 927 | local lit_end = pos + #lit_str - 1 928 | if str:sub(pos, lit_end) == lit_str then return lit_val, lit_end + 1 end 929 | end 930 | local pos_info_str = 'position ' .. pos .. ': ' .. str:sub(pos, pos + 10) 931 | error('Invalid json syntax starting at ' .. pos_info_str .. ': ' .. str) 932 | end 933 | end 934 | --------------------------------------------------------------------------------