├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── ci.yml │ ├── docs.yml │ └── lint.yml ├── .gitignore ├── .ldoc ├── .luacheckrc ├── .stylua.toml ├── COPYING ├── Makefile ├── README.md ├── autoload ├── scnvim.vim └── scnvim │ ├── editor.vim │ └── statusline.vim ├── doc └── SCNvim.txt ├── ftdetect ├── filetype.lua └── supercollider.vim ├── indent └── supercollider.vim ├── lua ├── scnvim.lua └── scnvim │ ├── action.lua │ ├── commands.lua │ ├── config.lua │ ├── editor.lua │ ├── extensions.lua │ ├── health.lua │ ├── help.lua │ ├── install.lua │ ├── map.lua │ ├── path.lua │ ├── postwin.lua │ ├── sclang.lua │ ├── settings.lua │ ├── signature.lua │ ├── statusline.lua │ ├── udp.lua │ └── utils.lua ├── scide_scnvim ├── Classes │ ├── Document.sc │ ├── SCNvim.sc │ ├── SCNvimDoc │ │ ├── SCNvimDoc.sc │ │ ├── SCNvimDocRenderer.sc │ │ └── extSCNvim.sc │ └── SCNvimJSON.sc └── HelpSource │ └── Classes │ └── SCNvim.schelp ├── syntax ├── scdoc.vim ├── scnvim.vim └── supercollider.vim └── test ├── .gitignore ├── Makefile ├── spec ├── automated │ ├── action_spec.lua │ ├── commands_spec.lua │ ├── editor_spec.lua │ ├── extensions_spec.lua │ ├── install_spec.lua │ ├── map_spec.lua │ ├── path_spec.lua │ └── postwin_spec.lua ├── fixtures │ ├── file.lua │ └── lua │ │ └── scnvim │ │ └── _extensions │ │ └── unit-test.lua └── integration │ └── sclang_spec.lua └── test_init.vim /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Help improve scnvim 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | 13 | 14 | **Describe the bug** 15 | 16 | 17 | **Expected behavior** 18 | 19 | 20 | **Steps to reproduce** 21 | 22 | 23 | **Additional context** 24 | 25 | 26 | **Information** 27 | 28 | - Operating system 29 | 30 | 31 | - SuperCollider version 32 | 33 | 34 | - `nvim --version` 35 | 36 | 37 | - Package manager 38 | 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for scnvim 4 | title: "[FR]: " 5 | labels: feature request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | 13 | **Describe the solution you'd like** 14 | 15 | 16 | **Describe alternatives you've considered** 17 | 18 | 19 | **Additional context** 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # adapted from https://github.com/nvim-telescope/telescope.nvim/blob/master/.github/workflows/ci.yml 2 | name: unit tests 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | 10 | jobs: 11 | unit_tests: 12 | name: unit tests 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | include: 18 | - os: ubuntu-latest 19 | url: https://github.com/neovim/neovim/releases/download/v0.10.1/nvim-linux64.tar.gz 20 | - os: macos-latest 21 | url: https://github.com/neovim/neovim/releases/download/v0.10.1/nvim-macos-arm64.tar.gz 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Prepare 25 | run: | 26 | mkdir -p _neovim 27 | curl -sL ${{ matrix.url }} | tar xzf - --strip-components=1 -C "${PWD}/_neovim" 28 | - name: Run tests 29 | run: | 30 | export PATH="${PWD}/_neovim/bin:${PATH}" 31 | export VIM="${PWD}/_neovim/share/nvim/runtime" 32 | nvim --version 33 | SCNVIM_CI=1 make unit_tests 34 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches-ignore: 9 | - main 10 | 11 | jobs: 12 | docs: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: leafo/gh-actions-lua@v8.0.0 17 | with: 18 | luaVersion: "5.1.5" 19 | - uses: leafo/gh-actions-luarocks@v4.0.0 20 | 21 | - name: Build 22 | run: luarocks install ldoc 23 | 24 | - name: Deploy 25 | run: make doc 26 | - uses: peaceiris/actions-gh-pages@v3 27 | with: 28 | github_token: ${{ secrets.GITHUB_TOKEN }} 29 | publish_dir: ./docs 30 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint and style check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | luacheck: 11 | name: luacheck 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Prepare 16 | run: | 17 | sudo apt-get update 18 | sudo apt-get install -y luarocks 19 | sudo luarocks install luacheck 20 | - name: Lint 21 | run: luacheck lua ftdetect test/spec 22 | stylua: 23 | name: stylua 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - uses: JohnnyMorganz/stylua-action@v1 28 | with: 29 | token: ${{ secrets.GITHUB_TOKEN }} 30 | version: 0.15.1 31 | args: --color always --check lua ftdetect test/spec 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | private/ 2 | doc/tags 3 | docs 4 | -------------------------------------------------------------------------------- /.ldoc: -------------------------------------------------------------------------------- 1 | project = 'scnvim' 2 | title = 'scnvim reference' 3 | file = 'lua' 4 | dir = 'docs' 5 | format = 'markdown' 6 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | globals = { 'vim' } 2 | -------------------------------------------------------------------------------- /.stylua.toml: -------------------------------------------------------------------------------- 1 | column_width = 120 2 | line_endings = "Unix" 3 | indent_type = "Spaces" 4 | indent_width = 2 5 | quote_style = "AutoPreferSingle" 6 | call_parentheses = "None" 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | source = lua ftdetect test/spec 2 | 3 | all: stylua luacheck 4 | 5 | format: 6 | stylua --verify $(source) 7 | 8 | luacheck: 9 | luacheck $(source) 10 | 11 | stylua: 12 | stylua --color always --check $(source) 13 | 14 | doc: 15 | ldoc lua -c .ldoc . 16 | 17 | test: 18 | make --directory test 19 | 20 | unit_tests: 21 | make --directory test automated 22 | 23 | .PHONY: luacheck stylua doc test 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scnvim 2 | 3 | [Neovim][neovim] frontend for [SuperCollider][supercollider]. 4 | 5 | [![unit tests](https://github.com/davidgranstrom/scnvim/actions/workflows/ci.yml/badge.svg)](https://github.com/davidgranstrom/scnvim/actions/workflows/ci.yml) 6 | [![lint and style check](https://github.com/davidgranstrom/scnvim/actions/workflows/lint.yml/badge.svg)](https://github.com/davidgranstrom/scnvim/actions/workflows/lint.yml) 7 | [![docs](https://github.com/davidgranstrom/scnvim/actions/workflows/docs.yml/badge.svg)](https://github.com/davidgranstrom/scnvim/actions/workflows/docs.yml) 8 | 9 | ## Table of content 10 | 11 | * [Features](#features) 12 | * [Installation](#installation) 13 | * [Usage](#usage) 14 | * [Documentation](#documentation) 15 | * [Extensions](#extensions) 16 | * [Supported platforms](#supported-platforms) 17 | 18 | ## Features 19 | 20 | * Post window output is displayed in a scratch buffer 21 | - Uses a split or a floating window for display 22 | - Navigate/move/copy etc. as with any other window 23 | - Toggle back if hidden automatically on errors 24 | * Automatic display of function signatures 25 | * Status line widgets 26 | - Display SuperCollider server status in the status line 27 | * Snippet generator 28 | - Generates snippets for creation methods in SCClassLibrary. 29 | * Can be used with Neovim [GUI frontends](https://github.com/neovim/neovim/wiki/Related-projects#gui) 30 | * Supports [on-demand loading](https://github.com/junegunn/vim-plug#on-demand-loading-of-plugins) 31 | * Context aware (block or line) evaluation (like `Cmd-Enter` in ScIDE) 32 | * Flashy eval flash (configurable) 33 | * Partial `Document` support (e.g. `thisProcess.nowExecutingPath`, `.load` etc.) 34 | * Plain text help system for SuperCollider documentation 35 | - Evaluate code examples inside the help buffer 36 | 37 | ## Installation 38 | 39 | ### Requirements 40 | 41 | * [Neovim][neovim] >= 0.7 42 | * [SuperCollider][supercollider] 43 | 44 | ### Install 45 | 46 | * Using [packer.nvim](https://github.com/wbthomason/packer.nvim) 47 | 48 | ```lua 49 | use { 'davidgranstrom/scnvim' } 50 | ``` 51 | 52 | * Using [vim-plug](https://github.com/junegunn/vim-plug) 53 | 54 | ```vim 55 | Plug 'davidgranstrom/scnvim' 56 | ``` 57 | 58 | ### Verify 59 | 60 | Run `:checkhealth scnvim` to verify that the installation was successful. 61 | 62 | ## Usage 63 | 64 | ### Configuration 65 | 66 | `scnvim` uses `lua` for configuration. Below is an example that you can copy 67 | and paste to your `init.lua`. 68 | 69 | If you are using `init.vim` for configuration you will need to surround the 70 | call to `scnvim.setup` in a `lua-heredoc`: 71 | 72 | ```vim 73 | " file: init.vim 74 | lua << EOF 75 | require('scnvim').setup({}) 76 | EOF 77 | ``` 78 | 79 | ### Example 80 | 81 | ```lua 82 | local scnvim = require 'scnvim' 83 | local map = scnvim.map 84 | local map_expr = scnvim.map_expr 85 | 86 | scnvim.setup({ 87 | keymaps = { 88 | [''] = map('editor.send_line', {'i', 'n'}), 89 | [''] = { 90 | map('editor.send_block', {'i', 'n'}), 91 | map('editor.send_selection', 'x'), 92 | }, 93 | [''] = map('postwin.toggle'), 94 | [''] = map('postwin.toggle', 'i'), 95 | [''] = map('postwin.clear', {'n', 'i'}), 96 | [''] = map('signature.show', {'n', 'i'}), 97 | [''] = map('sclang.hard_stop', {'n', 'x', 'i'}), 98 | ['st'] = map('sclang.start'), 99 | ['sk'] = map('sclang.recompile'), 100 | [''] = map_expr('s.boot'), 101 | [''] = map_expr('s.meter'), 102 | }, 103 | editor = { 104 | highlight = { 105 | color = 'IncSearch', 106 | }, 107 | }, 108 | postwin = { 109 | float = { 110 | enabled = true, 111 | }, 112 | }, 113 | }) 114 | ``` 115 | 116 | ### Start 117 | 118 | Open a new file in `nvim` with a `.scd` or `.sc` extension and type `:SCNvimStart` to start SuperCollider. 119 | 120 | ### Commands 121 | 122 | | Command | Description | 123 | |:-----------------------|:---------------------------------------------------------------| 124 | | `SCNvimStart` | Start SuperCollider | 125 | | `SCNvimStop` | Stop SuperCollider | 126 | | `SCNvimRecompile` | Recompile SCClassLibrary | 127 | | `SCNvimGenerateAssets` | Generate tags, syntax, snippets etc. | 128 | | `SCNvimHelp ` | Open help for \ (By default mapped to `K`) | 129 | | `SCNvimStatusLine` | Start to poll server status to be displayed in the status line | 130 | 131 | ### Additional setup 132 | 133 | Run `:SCNvimGenerateAssets` after starting SuperCollider to generate syntax highlighting and tags. 134 | 135 | The plugin should work "out of the box", but if you want even more fine-grained 136 | control please have a look at the [configuration 137 | section](https://github.com/davidgranstrom/scnvim/wiki/Configuration) in the 138 | wiki. 139 | 140 | ## Documentation 141 | 142 | * `:help scnvim` for detailed documentation. 143 | * [API documentation](https://davidgranstrom.github.io/scnvim/) 144 | 145 | ## Extensions 146 | 147 | The extension system provides additional functionalities and integrations. If 148 | you have made a scnvim extension, please open a PR and add it to this list! 149 | 150 | * [fzf-sc](https://github.com/madskjeldgaard/fzf-sc) 151 | - Combine the magic of fuzzy searching with the magic of SuperCollider in Neovim 152 | * [nvim-supercollider-piano](https://github.com/madskjeldgaard/nvim-supercollider-piano) 153 | - Play SuperCollider synths using your (computer) keyboard in neovim! 154 | * [scnvim-tmux](https://github.com/davidgranstrom/scnvim-tmux) 155 | - Redirect post window ouput to a tmux pane. 156 | * [scnvim-logger](https://github.com/davidgranstrom/scnvim-logger) 157 | - Log post window output to a file (example scnvim extension) 158 | * [telescope-scdoc](https://github.com/davidgranstrom/telescope-scdoc.nvim) 159 | - Use Telescope to fuzzy find documentation 160 | 161 | ## Supported platforms 162 | 163 | * Linux 164 | * macOS 165 | * Windows (tested with `nvim-qt` and `nvim.exe` in Windows PowerShell) 166 | 167 | ### Note to Windows users 168 | 169 | The path to `sclang.exe` needs to be specified in the config: 170 | 171 | ```lua 172 | local scnvim = require('scnvim') 173 | scnvim.setup({ 174 | sclang = { 175 | cmd = 'C:/Program Files/SuperCollider-3.12.2/sclang.exe' 176 | }, 177 | }) 178 | ``` 179 | 180 | Modify the `sclang.cmd` to point to where SuperCollider is installed on your system. 181 | 182 | Additionally, to be able to boot the server you will need to add the following to `startup.scd`: 183 | 184 | ```supercollider 185 | if (\SCNvim.asClass.notNil) { 186 | Server.program = (Platform.resourceDir +/+ "scsynth.exe").quote; 187 | } 188 | ``` 189 | 190 | ## License 191 | 192 | ```plain 193 | scnvim - Neovim frontend for SuperCollider 194 | Copyright © 2018 David Granström 195 | 196 | This program is free software: you can redistribute it and/or modify 197 | it under the terms of the GNU General Public License as published by 198 | the Free Software Foundation, either version 3 of the License, or 199 | (at your option) any later version. 200 | 201 | This program is distributed in the hope that it will be useful, 202 | but WITHOUT ANY WARRANTY; without even the implied warranty of 203 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 204 | GNU General Public License for more details. 205 | 206 | You should have received a copy of the GNU General Public License 207 | along with this program. If not, see . 208 | ``` 209 | 210 | [neovim]: https://github.com/neovim/neovim 211 | [supercollider]: https://github.com/supercollider/supercollider 212 | -------------------------------------------------------------------------------- /autoload/scnvim.vim: -------------------------------------------------------------------------------- 1 | " some configs might break because this function was removed 2 | function scnvim#install() 3 | echohl WarningMsg | echo '[scnvim] scnvim#install() has been removed. For more info: https://github.com/davidgranstrom/scnvim' | echohl None 4 | endfunction 5 | -------------------------------------------------------------------------------- /autoload/scnvim/editor.vim: -------------------------------------------------------------------------------- 1 | " File: scnvim/autoload/editor.vim 2 | " Author: David Granström 3 | " Description: scnvim editor helpers 4 | " Note: Will be deprecated and moved to editor.lua in the future. 5 | 6 | scriptencoding utf-8 7 | 8 | function! s:skip_pattern() abort 9 | return 'synIDattr(synID(line("."), col("."), 0), "name") ' . 10 | \ '=~? "scLineComment\\|scComment\\|scString\\|scSymbol"' 11 | endfunction 12 | 13 | function! s:find_match(start, end, flags) abort 14 | return searchpairpos(a:start, '', a:end, a:flags, s:skip_pattern()) 15 | endfunction 16 | 17 | function! scnvim#editor#get_block() abort 18 | " initialize to invalid ranges 19 | let start_pos = [0, 0] 20 | let end_pos = [0, 0] 21 | let forward_flags = 'nW' 22 | let backward_flags = 'nbW' 23 | " searchpairpos starts the search from the cursor so save where we are 24 | " right now and restore the cursor after the search 25 | let c_curpos = getcurpos() 26 | " move to first column 27 | call setpos('.', [0, c_curpos[1], 1, 0]) 28 | let [xs, ys] = s:find_match('(', ')', backward_flags) 29 | let start_pos = [xs, ys] 30 | if xs == 0 && ys == 0 31 | " we are already standing on the opening brace 32 | let start_pos = [line('.'), col('.')] 33 | else 34 | while xs > 0 && ys > 0 35 | call setpos('.', [0, xs, ys, 0]) 36 | let start_pos = [xs, ys] 37 | let [xs, ys] = s:find_match('(', ')', backward_flags) 38 | endwhile 39 | endif 40 | call setpos('.', [0, start_pos[0], start_pos[1], 0]) 41 | let end_pos = s:find_match('(', ')', forward_flags) 42 | " restore cursor 43 | call setpos('.', c_curpos) 44 | return [start_pos[0], end_pos[0]] 45 | endfunction 46 | 47 | " Could be replaced with `vim.region` later 48 | function! scnvim#editor#get_visual_selection() abort 49 | " update the ' marks before proceeding 50 | exe "normal! \" 51 | exe "normal! gv" 52 | let [lnum1, col1] = getpos("'<")[1:2] 53 | let [lnum2, col2] = getpos("'>")[1:2] 54 | if &selection ==# 'exclusive' 55 | let col2 -= 1 56 | endif 57 | let lines = getline(lnum1, lnum2) 58 | if !empty(lines) 59 | let lines[-1] = lines[-1][:col2 - 1] 60 | let lines[0] = lines[0][col1 - 1:] 61 | endif 62 | return { 63 | \ 'lines': lines, 64 | \ 'line_start': lnum1, 65 | \ 'line_end': lnum2, 66 | \ 'col_start': col1, 67 | \ 'col_end': col2, 68 | \ } 69 | endfunction 70 | -------------------------------------------------------------------------------- /autoload/scnvim/statusline.vim: -------------------------------------------------------------------------------- 1 | " File: autoload/scnvim/statusline.vim 2 | " Author: David Granström 3 | " Description: Status line functions 4 | 5 | scriptencoding utf-8 6 | 7 | " Kept for convenience for statusline formatting 8 | function! scnvim#statusline#server_status() abort 9 | return luaeval('require"scnvim.statusline".get_server_status()') 10 | endfunction 11 | -------------------------------------------------------------------------------- /doc/SCNvim.txt: -------------------------------------------------------------------------------- 1 | *SCNvim.txt* For nvim version 0.7.0 or later. 2 | 3 | Neovim frontend for SuperCollider 4 | 5 | SCNvim *scnvim* 6 | 7 | 1. Description..............................|scnvim-description| 8 | 1.1 Acknowledgments 9 | 2. Setup....................................|scnvim-setup| 10 | 3. Keymaps..................................|scnvim-keymaps| 11 | 4. Commands.................................|scnvim-commands| 12 | 5. Help system..............................|scnvim-help-system| 13 | 6. Configuration............................|scnvim-configuration| 14 | 7. License..................................|scnvim-license| 15 | 16 | ============================================================================== 17 | DESCRIPTION *scnvim-description* 18 | 19 | SCNvim is a SuperCollider editor frontend for Neovim. It provides ways to 20 | evaluate code and inspect output from the post window among other features 21 | such as automatic function signature hints and more. The goal of this plugin 22 | is to create an integrated development environment for SuperCollider in 23 | Neovim. 24 | 25 | Acknowledgments~ 26 | 27 | This plugin borrows some code and ideas from the original scvim 28 | implementation, for example the code for indent and syntax highlighting. 29 | 30 | ============================================================================== 31 | SETUP *scnvim-setup* 32 | 33 | Enable the plugin by calling the setup function in your *init.lua* 34 | > 35 | local scnvim = require('scnvim') 36 | scnvim.setup() 37 | < 38 | This plugin uses lua for configuration, so you may need to surround the above 39 | statement using a |lua-heredoc| if you are using vim script (i.e. *init.vim* ) 40 | 41 | Run `:checkhealth scnvim` to check that the installation was succesful. 42 | 43 | ============================================================================== 44 | KEYMAPS *scnvim-keymaps* 45 | 46 | No keymaps are defined by default. Use the `scnvim.setup` function to set 47 | your mappings. The *keywordprg* option is set by scnvim, use `K` to open 48 | documentation for the word under the cursor. 49 | > 50 | local map = scnvim.map 51 | scnvim.setup { 52 | keymaps = { 53 | [''] = map('editor.send_line', {'i', 'n'}), 54 | [''] = { 55 | map('editor.send_block', {'i', 'n'}), 56 | map('editor.send_selection', 'x'), 57 | }, 58 | [''] = map('postwin.toggle'), 59 | [''] = map('postwin.toggle', 'i'), 60 | [''] = map('postwin.clear', {'n', 'i'}), 61 | [''] = map('signature.show', {'n', 'i'}), 62 | [''] = map('sclang.hard_stop', {'n', 'x', 'i'}), 63 | ['st'] = map(scnvim.start), 64 | ['sk'] = map(scnvim.recompile), 65 | [''] = map_expr('s.boot'), 66 | [''] = map_expr('s.meter'), 67 | }, 68 | } 69 | < 70 | The `map` helper function can take a string or a function as input. 71 | 72 | When the input is a string it will be parsed as `module.function` to create 73 | the keymap. If the input is a function it will simply execute that function. 74 | 75 | The following modules can be used to create a keymap from a string: 76 | 77 | * `editor` 78 | * `postwin` 79 | * `sclang` 80 | * `scnvim` 81 | * `signature` 82 | 83 | String example~ 84 | > 85 | [''] = scnvim.map('postwin.toggle') 86 | < 87 | This will create a keymap for `` for the `toggle` function from the 88 | `postwin` module. See the reference documentation for an overview of all 89 | available functions in their respective module: 90 | 91 | https://davidgranstrom.github.io/scnvim/ 92 | 93 | Function example~ 94 | 95 | If the `map` helper receives a function it will execute that function when 96 | pressing the key. 97 | > 98 | ['st'] = scnvim.map(scnvim.start) 99 | < 100 | Here we're using the `scnvim.start` function. But its also possible to map 101 | arbitrary functions. 102 | > 103 | [''] = scnvim.map(function() 104 | vim.cmd('SCNvimGenerateAssets') 105 | end) 106 | 107 | Map expressions~ 108 | 109 | The `map_expr` helper let's you map SuperCollider code. 110 | > 111 | local map_expr = scnvim.map_expr 112 | [''] = map_expr('s.meter') 113 | < 114 | Note The `scnvim.map` helper object is a convenience to setup keymaps in the 115 | config. But it is also possible to manage mappings manually using the scnvim 116 | API with |vim.keymap.set|. 117 | 118 | ============================================================================== 119 | COMMANDS *scnvim-commands* 120 | 121 | Command Description 122 | ------- ----------- 123 | `SCNvimStart` Start SuperCollider 124 | `SCNvimStop` Stop SuperCollider 125 | `SCNvimRecompile` Recompile SCClassLibrary 126 | `SCNvimReboot` Reboot sclang interpreter 127 | `SCNvimGenerateAssets` Generate syntax highlightning and snippets 128 | `SCNvimHelp` Open HelpBrowser or window split for 129 | `SCNvimStatusLine` Display server status in 'statusline' if configured. 130 | 131 | ============================================================================== 132 | HELP SYSTEM *scnvim-help-system* 133 | 134 | Press `K` to view documentation for the word under the cursor. By default help 135 | files will be opened in the `HelpBrowser`. Read the sections below if you want 136 | to display documentation in nvim. 137 | 138 | View documentation in nvim~ 139 | 140 | SCNvim can be configured to render SCDoc help files (.schelp) to plain text 141 | and display the result inside nvim. In order to do so, the scnvim help system 142 | needs a program to convert HTML help files into plain text e.g. `pandoc[1]` 143 | 144 | Set the path to your converter program in the config passed to `scnvim.setup` 145 | > 146 | scnvim.setup{ 147 | documentation = { 148 | cmd = '/opt/homebrew/bin/pandoc', 149 | }, 150 | } 151 | < 152 | 153 | Customization~ 154 | 155 | The help system is configured to use `pandoc` by default. To use another 156 | program you will also need to supply the command line arguments needed to 157 | perform the HTML to plain text conversion. The example below is for the 158 | `html2text` program. `$1` is a placeholder for the *input file* and `$2` is a 159 | placeholder for the *output file*, these are replaced automatically. 160 | > 161 | scnvim.setup{ 162 | documentation = { 163 | cmd = '/usr/local/bin/html2text', 164 | args = {'$1', '-o', '$2'} 165 | }, 166 | } 167 | < 168 | Useful hints ~ 169 | 170 | Use the `K` command to open a split window with the documentation of the word 171 | under the cursor. The 'quickfix' window is used to display methods, press enter 172 | on a match to jump to the corresponding help file. Read `:h quickfix` to learn 173 | more about how the 'quickfix' window works. 174 | 175 | To see an overview (outline) of the content of the help file press `gO`. This 176 | will open a window local quickfix window (a 'location-list'), use this list to 177 | jump to different sections of the document. 178 | 179 | [1]: https://pandoc.org/ 180 | 181 | ============================================================================== 182 | CONFIGURATION *scnvim-configuration* 183 | 184 | All user configuration is handled by the `scnvim.setup` function. 185 | 186 | The default values are defined and documented in `lua/scnvim/config.lua` in the 187 | root directory of this plugin. 188 | 189 | You can also view an up-to-date HTML version of the documentation by visiting 190 | this link: https://davidgranstrom.github.io/scnvim/modules/scnvim.config.html 191 | 192 | Example configuration~ 193 | > 194 | scnvim.setup { 195 | mapping = { 196 | [''] = scnvim.map.send_line({'i', 'n'}), 197 | [''] = { 198 | scnvim.map.send_block({'i', 'n'}), 199 | scnvim.map.send_selection('x'), 200 | }, 201 | [''] = scnvim.map.postwin_toggle('n'), 202 | [''] = scnvim.map.postwin_toggle('i'), 203 | [''] = scnvim.map.postwin_clear({'n', 'i'}), 204 | [''] = scnvim.map.show_signature({'n', 'i'}), 205 | [''] = scnvim.map.hard_stop({'n', 'x', 'i'}), 206 | ['st'] = scnvim.map(scnvim.start), 207 | ['sk'] = scnvim.map(scnvim.recompile), 208 | }, 209 | editor = { 210 | highlight = { 211 | color = 'IncSearch', 212 | type = 'fade', 213 | }, 214 | }, 215 | documentation = { 216 | cmd = '/opt/homebrew/bin/pandoc', 217 | }, 218 | postwin = { 219 | float = { 220 | enabled = true, 221 | }, 222 | }, 223 | } 224 | < 225 | ============================================================================== 226 | LICENSE *scnvim-license* 227 | 228 | scnvim - SuperCollider integration for Neovim 229 | Copyright © 2018 David Granström 230 | 231 | This program is free software: you can redistribute it and/or modify 232 | it under the terms of the GNU General Public License as published by 233 | the Free Software Foundation, either version 3 of the License, or 234 | (at your option) any later version. 235 | 236 | This program is distributed in the hope that it will be useful, 237 | but WITHOUT ANY WARRANTY; without even the implied warranty of 238 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 239 | GNU General Public License for more details. 240 | 241 | You should have received a copy of the GNU General Public License 242 | along with this program. If not, see . 243 | 244 | vim:tw=78:et:ft=help:norl: 245 | -------------------------------------------------------------------------------- /ftdetect/filetype.lua: -------------------------------------------------------------------------------- 1 | if vim.g.do_filetype_lua == 1 then 2 | vim.filetype.add { 3 | extension = { 4 | schelp = 'scdoc', 5 | }, 6 | } 7 | end 8 | -------------------------------------------------------------------------------- /ftdetect/supercollider.vim: -------------------------------------------------------------------------------- 1 | if !exists('g:do_filetype_lua') 2 | autocmd BufEnter,BufWinEnter,BufNewFile,BufRead *.schelp set filetype=scdoc 3 | endif 4 | -------------------------------------------------------------------------------- /indent/supercollider.vim: -------------------------------------------------------------------------------- 1 | " Copied from sc_indent.vim 2 | " https://github.com/supercollider/scvim 3 | " License GPLv3 4 | " 5 | 6 | if exists ('b:did_scvim_indent') 7 | finish 8 | endif 9 | 10 | let b:did_scvim_indent = 1 11 | 12 | setlocal indentexpr=GetSCIndent() 13 | setlocal indentkeys+=0),0],0} 14 | 15 | if exists ('*GetSCIndent') 16 | finish 17 | endif 18 | 19 | function! GetSCIndent() 20 | let curr_line = getline(v:lnum) 21 | let lnum = prevnonblank(v:lnum - 1) 22 | 23 | if lnum == 0 24 | return 0 25 | endif 26 | 27 | let prev_line = getline(lnum) 28 | 29 | let ind = indent(lnum) 30 | 31 | " don't create indentation for code blocks 32 | if prev_line =~# '^($' 33 | return ind 34 | end 35 | 36 | if prev_line =~# '\(\/\/.*\)\@\'] = map('editor.send_line', {'i', 'n'}), 25 | --- [''] = { 26 | --- map('editor.send_block', {'i', 'n'}), 27 | --- map('editor.send_selection', 'x'), 28 | --- }, 29 | --- ['st'] = map('sclang.start'), 30 | --- ['sk'] = map('sclang.recompile'), 31 | --- } 32 | keymaps = {}, 33 | 34 | --- table 35 | ---@table default.documentation 36 | ---@field cmd Absolute path to the render program (e.g. /opt/homebrew/bin/pandoc) 37 | ---@field args (default: `{ '$1', '--from', 'html', '--to', 'plain', '-o', '$2' }`) 38 | --- 39 | --- Arguments given to the render program. The default args are for 40 | ---`pandoc`. Any program that can convert html into plain text should work. The 41 | --- string $1 will be replaced with the input file path and $2 will be replaced 42 | --- with the output file path. 43 | --- 44 | ---@field horizontal (default: true) Open the help window as a horizontal split 45 | ---@field direction (default: 'top') Direction of the split: 'top', 'right', 'bot', 'left' 46 | ---@field keymaps (default: true) If true apply user keymaps to the help 47 | --- window. Use a table value for explicit mappings. 48 | documentation = { 49 | cmd = nil, 50 | args = { '$1', '--from', 'html', '--to', 'plain', '-o', '$2' }, 51 | horizontal = true, 52 | direction = 'top', 53 | keymaps = true, 54 | }, 55 | 56 | --- table 57 | ---@table default.postwin 58 | ---@field highlight (default: true) Use syntax colored post window output. 59 | ---@field auto_toggle_error (default: true) Auto-toggle post window on errors. 60 | ---@field scrollback (default: 5000) The number of lines to save in the post window history. 61 | ---@field horizontal (default: false) Open the post window as a horizontal split 62 | ---@field direction (default: 'right') Direction of the split: 'top', 'right', 'bot', 'left' 63 | ---@field size Use a custom initial size 64 | ---@field fixed_size Use a fixed size for the post window. The window will always use this size if closed. 65 | ---@field keymaps (default: true) If true apply user keymaps to the help 66 | --- window. Use a table value for explicit mappings. 67 | postwin = { 68 | highlight = true, 69 | auto_toggle_error = true, 70 | scrollback = 5000, 71 | horizontal = false, 72 | direction = 'right', 73 | size = nil, 74 | fixed_size = nil, 75 | keymaps = nil, 76 | 77 | --- table 78 | ---@table default.postwin.float 79 | ---@field enabled (default: false) Use a floating post window. 80 | ---@field row (default: 0) The row position, can be a function. 81 | ---@field col (default: vim.o.columns) The column position, can be a function. 82 | ---@field width (default: 64) The width, can be a function. 83 | ---@field height (default: 14) The height, can be a function. 84 | ---@field callback (default: `function(id) vim.api.nvim_win_set_option(id, 'winblend', 10) end`) 85 | --- Callback that runs whenever the floating window was opened. 86 | --- Can be used to set window local options. 87 | float = { 88 | enabled = false, 89 | row = 0, 90 | col = function() 91 | return vim.o.columns 92 | end, 93 | width = 64, 94 | height = 14, 95 | --- table 96 | ---@table default.postwin.float.config 97 | ---@field border (default: 'single') 98 | ---@field ... See `:help nvim_open_win` for possible values 99 | config = { 100 | border = 'single', 101 | }, 102 | callback = function(id) 103 | vim.api.nvim_win_set_option(id, 'winblend', 10) 104 | end, 105 | }, 106 | }, 107 | 108 | --- table 109 | ---@table default.editor 110 | ---@field force_ft_supercollider (default: true) Treat .sc files as supercollider. 111 | --- If false, use nvim's native ftdetect. 112 | editor = { 113 | force_ft_supercollider = true, 114 | 115 | --- table 116 | ---@table editor.highlight 117 | ---@field color (default: `TermCursor`) Highlight group for the flash color. 118 | --- Use a table for custom colors: 119 | --- `color = { guifg = 'black', guibg = 'white', ctermfg = 'black', ctermbg = 'white' }` 120 | ---@field type (default: 'flash') Highligt flash type: 'flash', 'fade' or 'none' 121 | 122 | --- table 123 | ---@table editor.highlight.flash 124 | ---@field duration (default: 100) The duration of the flash in ms. 125 | ---@field repeats (default: 2) The number of repeats. 126 | 127 | --- table 128 | ---@table editor.highlight.fade 129 | ---@field duration (default: 375) The duration of the flash in ms. 130 | highlight = { 131 | color = 'TermCursor', 132 | type = 'flash', 133 | flash = { 134 | duration = 100, 135 | repeats = 2, 136 | }, 137 | fade = { 138 | duration = 375, 139 | }, 140 | }, 141 | 142 | --- table 143 | ---@table editor.signature 144 | ---@field float (default: true) Show function signatures in a floating window 145 | ---@field auto (default: true) Show function signatures while typing in insert mode 146 | ---@field config 147 | signature = { 148 | float = true, 149 | auto = true, 150 | config = {}, -- TODO: can we use vim.diagnostic instead..? 151 | }, 152 | }, 153 | 154 | --- table 155 | ---@table snippet 156 | 157 | --- table 158 | ---@table snippet.engine 159 | ---@field name Name of the snippet engine 160 | ---@field options Table of engine specific options (note, currently not in use) 161 | snippet = { 162 | engine = { 163 | name = 'luasnip', 164 | options = { 165 | descriptions = true, 166 | }, 167 | }, 168 | -- mul_add = false, -- Include mul/add arguments for UGens 169 | -- style = 'default', -- 'compact' = do not put spaces between args, etc. 170 | }, 171 | 172 | --- table 173 | ---@table statusline 174 | ---@field poll_interval (default: 1) The interval to update the status line widgets in seconds. 175 | statusline = { 176 | poll_interval = 1, 177 | }, 178 | 179 | --- table 180 | ---@table extensions 181 | extensions = {}, 182 | } 183 | 184 | local M = {} 185 | 186 | setmetatable(M, { 187 | __index = function(self, key) 188 | local config = rawget(self, 'config') 189 | if config then 190 | return config[key] 191 | end 192 | return default[key] 193 | end, 194 | }) 195 | 196 | --- Merge the user configuration with the default values. 197 | ---@param config The user configuration 198 | function M.resolve(config) 199 | config = config or {} 200 | M.config = vim.tbl_deep_extend('keep', config, default) 201 | end 202 | 203 | return M 204 | -------------------------------------------------------------------------------- /lua/scnvim/editor.lua: -------------------------------------------------------------------------------- 1 | --- Connects nvim and the sclang process. 2 | --- Applies autocommands and forwards text to be evaluated to the sclang process. 3 | ---@module scnvim.editor 4 | 5 | local sclang = require 'scnvim.sclang' 6 | local config = require 'scnvim.config' 7 | local commands = require 'scnvim.commands' 8 | local settings = require 'scnvim.settings' 9 | local action = require 'scnvim.action' 10 | local api = vim.api 11 | local uv = vim.loop 12 | local M = {} 13 | 14 | --- Actions 15 | ---@section actions 16 | 17 | --- Action that runs to highlight buffer content sent to sclang. 18 | --- The default function depends on the user config. 19 | ---@param start Start range ({row, col} zero indexed) 20 | ---@param finish End range ({row, col} zero indexed) 21 | M.on_highlight = action.new(function(start, finish) end) -- luacheck: ignore 22 | 23 | --- Action that runs when buffer content is sent to sclang. 24 | --- The default is to send the content as a string to sclang. 25 | ---@param lines Table with the lines. 26 | ---@param callback Optional callback function. 27 | M.on_send = action.new(function(lines, callback) 28 | if callback then 29 | lines = callback(lines) 30 | end 31 | sclang.send(table.concat(lines, '\n')) 32 | end) 33 | 34 | --- Functions 35 | ---@section functions 36 | 37 | --- Get a range of lines. 38 | ---@param lstart Start index. 39 | ---@param lend End index. 40 | ---@return A table with strings. 41 | local function get_range(lstart, lend) 42 | return api.nvim_buf_get_lines(0, lstart - 1, lend, false) 43 | end 44 | 45 | --- Flash once. 46 | ---@param start Start range. 47 | ---@param finish End range. 48 | ---@param delay How long to highlight the text. 49 | local function flash_once(start, finish, delay) 50 | local ns = api.nvim_create_namespace 'scnvim_flash' 51 | vim.highlight.range(0, ns, 'SCNvimEval', start, finish, { inclusive = true }) 52 | vim.defer_fn(function() 53 | api.nvim_buf_clear_namespace(0, ns, 0, -1) 54 | end, delay) 55 | end 56 | 57 | --- Apply a flashing effect to a text region. 58 | ---@param start starting position (tuple {line,col} zero indexed) 59 | ---@param finish finish position (tuple {line,col} zero indexed) 60 | ---@local 61 | local function flash_region(start, finish) 62 | local duration = config.editor.highlight.flash.duration 63 | local repeats = config.editor.highlight.flash.repeats 64 | if duration == 0 or repeats == 0 then 65 | return 66 | end 67 | local delta = duration / repeats 68 | flash_once(start, finish, delta) 69 | if repeats > 1 then 70 | local count = 0 71 | local timer = uv.new_timer() 72 | timer:start( 73 | duration, 74 | duration, 75 | vim.schedule_wrap(function() 76 | flash_once(start, finish, delta) 77 | count = count + 1 78 | if count == repeats - 1 then 79 | timer:stop() 80 | end 81 | end) 82 | ) 83 | end 84 | end 85 | 86 | --- Apply a fading effect to a text region. 87 | ---@param start starting position (tuple {line,col} zero indexed) 88 | ---@param finish finish position (tuple {line,col} zero indexed) 89 | ---@local 90 | local function fade_region(start, finish) 91 | local lstart = start[1] 92 | local lend = finish[1] 93 | local width = 1 94 | if vim.fn.mode() == 'v' then 95 | width = finish[2] 96 | else 97 | local lines = get_range(lstart + 1, lend + 1) 98 | for _, line in ipairs(lines) do 99 | if #line > width then 100 | width = #line 101 | end 102 | end 103 | end 104 | local curwin = api.nvim_get_current_win() 105 | local buf = M.fade_buf or api.nvim_create_buf(false, true) 106 | local options = { 107 | relative = 'win', 108 | win = curwin, 109 | width = width > 0 and width or 1, 110 | height = lend - lstart + 1, 111 | bufpos = { lstart - 1, 0 }, 112 | focusable = false, 113 | style = 'minimal', 114 | border = 'none', 115 | anchor = lstart > 0 and 'NW' or 'SE', 116 | } 117 | local id = api.nvim_open_win(buf, false, options) 118 | api.nvim_win_set_option(id, 'winhl', 'Normal:' .. 'SCNvimEval') 119 | local timer = uv.new_timer() 120 | local rate = 50 121 | local accum = 0 122 | local duration = math.floor(config.editor.highlight.fade.duration) 123 | timer:start( 124 | 0, 125 | rate, 126 | vim.schedule_wrap(function() 127 | accum = accum + rate 128 | if accum > duration then 129 | accum = duration 130 | end 131 | local value = math.pow(accum / duration, 2.5) 132 | api.nvim_win_set_option(id, 'winblend', math.floor(100 * value)) 133 | if accum >= duration then 134 | timer:stop() 135 | api.nvim_win_close(id, true) 136 | end 137 | end) 138 | ) 139 | M.fade_buf = buf 140 | end 141 | 142 | --- Applies keymaps from the user configuration. 143 | local function apply_keymaps(mappings) 144 | for key, value in pairs(mappings) do 145 | -- handle list of keymaps to same key 146 | if value[1] ~= nil then 147 | for _, v in ipairs(value) do 148 | local opts = { 149 | buffer = true, 150 | desc = v.options.desc, 151 | } 152 | vim.keymap.set(v.modes, key, v.fn, opts) 153 | end 154 | else 155 | local opts = { 156 | buffer = true, 157 | desc = value.options.desc, 158 | } 159 | vim.keymap.set(value.modes, key, value.fn, opts) 160 | end 161 | end 162 | end 163 | 164 | --- Create a highlight command 165 | ---@return The highlight ex command string 166 | ---@local 167 | local function create_hl_group() 168 | local color = config.editor.highlight.color 169 | if type(color) == 'string' then 170 | color = string.format('highlight default link SCNvimEval %s', color) 171 | elseif type(color) == 'table' then 172 | color = string.format( 173 | 'highlight default SCNvimEval guifg=%s guibg=%s ctermfg=%s ctermbg=%s', 174 | color.guifg or 'black', 175 | color.guibg or 'white', 176 | color.ctermfg or 'black', 177 | color.ctermbg or 'white' 178 | ) 179 | end 180 | vim.cmd(color) 181 | return color 182 | end 183 | 184 | --- Create autocommands 185 | local function create_autocmds() 186 | local id = api.nvim_create_augroup('scnvim_editor', { clear = true }) 187 | api.nvim_create_autocmd('VimLeavePre', { 188 | group = id, 189 | desc = 'Stop sclang on Nvim exit', 190 | pattern = '*', 191 | callback = sclang.stop, 192 | }) 193 | api.nvim_create_autocmd({ 'BufEnter', 'BufNewFile', 'BufRead' }, { 194 | group = id, 195 | desc = 'Set the document path in sclang', 196 | pattern = { '*.scd', '*.sc', '*.quark' }, 197 | callback = sclang.set_current_path, 198 | }) 199 | api.nvim_create_autocmd('FileType', { 200 | group = id, 201 | desc = 'Apply commands', 202 | pattern = 'supercollider', 203 | callback = commands, 204 | }) 205 | api.nvim_create_autocmd('FileType', { 206 | group = id, 207 | desc = 'Apply settings', 208 | pattern = 'supercollider', 209 | callback = settings, 210 | }) 211 | api.nvim_create_autocmd('FileType', { 212 | group = id, 213 | pattern = 'supercollider', 214 | desc = 'Apply keymaps', 215 | callback = function() 216 | apply_keymaps(config.keymaps) 217 | end, 218 | }) 219 | local doc_maps = config.documentation.keymaps 220 | if doc_maps then 221 | api.nvim_create_autocmd('FileType', { 222 | group = id, 223 | pattern = 'help.supercollider', 224 | desc = 'Apply keymaps for the help window', 225 | callback = function() 226 | doc_maps = type(doc_maps) == 'table' and doc_maps or config.keymaps 227 | apply_keymaps(doc_maps) 228 | end, 229 | }) 230 | end 231 | local postwin_maps = config.postwin.keymaps 232 | if postwin_maps then 233 | api.nvim_create_autocmd('FileType', { 234 | group = id, 235 | desc = 'Apply keymaps for the post window', 236 | pattern = 'scnvim', 237 | callback = function() 238 | postwin_maps = type(postwin_maps) == 'table' and postwin_maps or config.keymaps 239 | apply_keymaps(postwin_maps) 240 | end, 241 | }) 242 | end 243 | if config.editor.signature.auto then 244 | api.nvim_create_autocmd('InsertCharPre', { 245 | group = id, 246 | desc = 'Insert mode function signature', 247 | pattern = { '*.scd', '*.sc', '*.quark' }, 248 | callback = require('scnvim.signature').ins_show, 249 | }) 250 | end 251 | if config.editor.force_ft_supercollider then 252 | api.nvim_create_autocmd({ 253 | 'BufNewFile', 254 | 'BufRead', 255 | 'BufEnter', 256 | 'BufWinEnter', 257 | }, { 258 | group = id, 259 | desc = 'Set *.sc to filetype supercollider', 260 | pattern = '*.sc', 261 | command = 'set filetype=supercollider', 262 | }) 263 | end 264 | local hl_cmd = create_hl_group() 265 | api.nvim_create_autocmd('ColorScheme', { 266 | group = id, 267 | desc = 'Reapply custom highlight group', 268 | pattern = '*', 269 | command = hl_cmd, 270 | }) 271 | end 272 | 273 | --- Setup function. 274 | --- 275 | --- Called from the main module. 276 | ---@see scnvim 277 | ---@local 278 | function M.setup() 279 | create_autocmds() 280 | local highlight = config.editor.highlight 281 | if highlight.type == 'flash' then 282 | M.on_highlight:replace(flash_region) 283 | elseif highlight.type == 'fade' then 284 | M.on_highlight:replace(fade_region) 285 | else -- none 286 | M.on_highlight:replace(function() end) 287 | end 288 | end 289 | 290 | --- Get the current line and send it to sclang. 291 | ---@param cb An optional callback function. 292 | ---@param flash Highlight the selected text 293 | function M.send_line(cb, flash) 294 | flash = flash == nil and true or flash 295 | local linenr = api.nvim_win_get_cursor(0)[1] 296 | local line = get_range(linenr, linenr) 297 | M.on_send(line, cb) 298 | if flash then 299 | local start = { linenr - 1, 0 } 300 | local finish = { linenr - 1, #line[1] } 301 | M.on_highlight(start, finish) 302 | end 303 | end 304 | 305 | --- Get the current block of code and send it to sclang. 306 | ---@param cb An optional callback function. 307 | ---@param flash Highlight the selected text 308 | function M.send_block(cb, flash) 309 | flash = flash == nil and true or flash 310 | local lstart, lend = unpack(vim.fn['scnvim#editor#get_block']()) 311 | if lstart == lend or lstart == 0 or lend == 0 then 312 | M.send_line(cb, flash) 313 | return 314 | end 315 | local lines = get_range(lstart, lend) 316 | local last_line = lines[#lines] 317 | local block_end = string.find(last_line, ')') 318 | lines[#lines] = last_line:sub(1, block_end) 319 | M.on_send(lines, cb) 320 | if flash then 321 | local start = { lstart - 1, 0 } 322 | local finish = { lend - 1, 0 } 323 | M.on_highlight(start, finish) 324 | end 325 | end 326 | 327 | --- Send a visual selection. 328 | ---@param cb An optional callback function. 329 | ---@param flash Highlight the selected text 330 | function M.send_selection(cb, flash) 331 | flash = flash == nil and true or flash 332 | local ret = vim.fn['scnvim#editor#get_visual_selection']() 333 | M.on_send(ret.lines, cb) 334 | if flash then 335 | local start = { ret.line_start - 1, ret.col_start - 1 } 336 | local finish = { ret.line_end - 1, ret.col_end - 1 } 337 | M.on_highlight(start, finish) 338 | end 339 | end 340 | 341 | return M 342 | -------------------------------------------------------------------------------- /lua/scnvim/extensions.lua: -------------------------------------------------------------------------------- 1 | --- Extensions. 2 | --- API used to manage and register third party scnvim extensions. 3 | --- Heavily inspired by the extension model used by telescope.nvim 4 | --- https://github.com/nvim-telescope/telescope.nvim 5 | ---@module scnvim.extensions 6 | ---@local 7 | local config = require 'scnvim.config' 8 | local path = require 'scnvim.path' 9 | local M = {} 10 | 11 | M._health = {} 12 | M._linked = {} 13 | 14 | local function link_classes(name) 15 | local ok, root_dir, sc_ext_dir 16 | ok, root_dir = pcall(path.get_plugin_root_dir, name) 17 | if not ok then 18 | return root_dir 19 | end 20 | ok, sc_ext_dir = pcall(path.get_user_extension_dir) 21 | if not ok then 22 | return sc_ext_dir 23 | end 24 | local source_dir = path.concat(root_dir, 'supercollider') 25 | local dest_dir = path.concat(sc_ext_dir, 'scnvim-extensions') 26 | if path.exists(source_dir) then 27 | vim.fn.mkdir(dest_dir, 'p') 28 | local link_path = path.concat(dest_dir, name) 29 | path.link(source_dir, link_path) 30 | return link_path 31 | end 32 | return nil 33 | end 34 | 35 | local function load_extension(name) 36 | local ok, ext = pcall(require, 'scnvim._extensions.' .. name) 37 | if not ok then 38 | error(string.format('[scnvim] "%s" was not found', name)) 39 | end 40 | if ext.setup then 41 | local ext_config = config.extensions[name] or {} 42 | ext.setup(ext_config, config) 43 | end 44 | return ext 45 | end 46 | 47 | M.manager = setmetatable({}, { 48 | __index = function(t, k) 49 | local ext = load_extension(k) 50 | t[k] = ext.exports or {} 51 | M._health[k] = ext.health 52 | M._linked[k] = link_classes(k) 53 | return t[k] 54 | end, 55 | }) 56 | 57 | --- Register an extension. 58 | ---@param ext The extension to register. 59 | ---@return The extension. 60 | function M.register(ext) 61 | return ext 62 | end 63 | 64 | --- Load an extension. 65 | ---@param name The extension to load. 66 | ---@return The exported extension API. 67 | function M.load(name) 68 | return M.manager[name] 69 | end 70 | 71 | --- Run an exported function. 72 | --- This function is called by `SCNvimExt` user command and probably not so useful on its own. 73 | ---@param tbl Table returned by `nvim_buf_create_user_command` 74 | function M.run_user_command(tbl) 75 | local name, cmd = unpack(vim.split(tbl.fargs[1], '.', { plain = true, trimempty = true })) 76 | local ext = M.manager[name] 77 | if not ext then 78 | error(string.format('Extension "%s" is not installed', name)) 79 | end 80 | if ext[cmd] then 81 | ext[cmd](select(2, unpack(tbl.fargs))) 82 | else 83 | error(string.format('Could not find exported function "%s"', cmd)) 84 | end 85 | end 86 | 87 | local function get_keys(t) 88 | local keys = {} 89 | for name in pairs(t) do 90 | keys[#keys + 1] = name 91 | end 92 | table.sort(keys) 93 | return keys 94 | end 95 | 96 | local function filter(t, str) 97 | if not str or #str == 0 then 98 | return t 99 | end 100 | return vim.tbl_filter(function(s) 101 | return s:match(str) ~= nil 102 | end, t) 103 | end 104 | 105 | --- User command completion callback 106 | ---@local 107 | function M.cmd_complete(arglead, cmdline, cursorpos) -- luacheck: ignore 108 | local extensions = filter(get_keys(M.manager), arglead) 109 | local fullmatch = arglead:match '(.*%.)' 110 | if fullmatch then 111 | local ext_name = fullmatch:sub(1, -2) 112 | local meth_name = vim.split(arglead, '.', { plain = true })[2] 113 | local exports = filter(get_keys(M.manager[ext_name]), meth_name) 114 | for i, fname in ipairs(exports) do 115 | exports[i] = ext_name .. '.' .. fname 116 | end 117 | return exports 118 | end 119 | return extensions 120 | end 121 | 122 | return M 123 | -------------------------------------------------------------------------------- /lua/scnvim/health.lua: -------------------------------------------------------------------------------- 1 | --- Perform health checks. 2 | ---@module scnvim.health 3 | ---@usage :checkhealth scnvim 4 | ---@local 5 | 6 | local health = vim.health or require 'health' 7 | local install = require 'scnvim.install' 8 | local sclang = require 'scnvim.sclang' 9 | local config = require 'scnvim.config' 10 | local extensions = require 'scnvim.extensions' 11 | 12 | local M = {} 13 | 14 | local function check_nvim_version() 15 | local supported = vim.fn.has 'nvim-0.7' == 1 16 | if not supported then 17 | health.error 'scnvim needs nvim version 0.7 or higher.' 18 | health.info 'if you are unable to upgrade, use the `0.6-compat` branch' 19 | else 20 | local v = vim.version() 21 | health.ok(string.format('nvim version %d.%d.%d', v.major, v.minor, v.patch)) 22 | end 23 | end 24 | 25 | local function check_classes_installed() 26 | local class_path = install.check() 27 | if not class_path then 28 | health.error 'scnvim classes are not installed.' 29 | health.info 'use `ensure_installed = true` in the scnvim setup function' 30 | else 31 | health.ok('scnvim classes are installed: ' .. class_path) 32 | end 33 | end 34 | 35 | local function check_keymaps() 36 | if vim.tbl_count(config.keymaps) == 0 then 37 | health.info 'no keymaps defined' 38 | else 39 | health.ok 'keymaps are defined' 40 | end 41 | end 42 | 43 | local function check_documentation() 44 | local doc = config.documentation 45 | if not doc.cmd then 46 | health.info 'using HelpBrowser for documentation' 47 | else 48 | local exe_path = vim.fn.exepath(doc.cmd) 49 | if exe_path ~= '' then 50 | health.ok(doc.cmd) 51 | end 52 | if doc.args then 53 | local vin = false 54 | local vout = false 55 | for _, arg in ipairs(doc.args) do 56 | if arg == '$1' then 57 | vin = true 58 | elseif arg == '$2' then 59 | vout = true 60 | end 61 | end 62 | if vin and vout then 63 | health.ok(vim.inspect(doc.args)) 64 | elseif vout and not vin then 65 | health.error 'argument list is missing input placeholder ($1)' 66 | elseif vin and not vout then 67 | health.error 'argument list is missing output placeholder ($2)' 68 | else 69 | health.error 'argument list is missing both input and output placeholders ($1/$2)' 70 | end 71 | end 72 | if doc.on_open then 73 | health.info 'using external function for on_open' 74 | end 75 | if doc.on_select then 76 | health.info 'using external function for on_select' 77 | end 78 | end 79 | end 80 | 81 | local function check_sclang() 82 | local ok, ret = pcall(sclang.find_sclang_executable) 83 | if ok then 84 | health.ok('sclang executable: ' .. ret) 85 | else 86 | health.error(ret) 87 | end 88 | end 89 | 90 | local function check_extensions() 91 | local installed = {} 92 | for name, _ in pairs(extensions.manager) do 93 | installed[#installed + 1] = name 94 | end 95 | table.sort(installed) 96 | for _, name in ipairs(installed) do 97 | local health_check = extensions._health[name] 98 | if health_check then 99 | health.start(string.format('extension: "%s"', name)) 100 | health_check() 101 | local link = extensions._linked[name] 102 | if link then 103 | health.ok(string.format('installed classes "%s"', link)) 104 | else 105 | health.ok 'no classes to install' 106 | end 107 | else 108 | health.ok(string.format('No health check for "%s"', name)) 109 | end 110 | end 111 | end 112 | 113 | function M.check() 114 | health.start 'scnvim' 115 | check_nvim_version() 116 | check_sclang() 117 | check_classes_installed() 118 | check_keymaps() 119 | health.start 'scnvim documentation' 120 | check_documentation() 121 | health.start 'scnvim extensions' 122 | check_extensions() 123 | end 124 | 125 | return M 126 | -------------------------------------------------------------------------------- /lua/scnvim/help.lua: -------------------------------------------------------------------------------- 1 | --- Help system. 2 | --- Convert schelp files into plain text by using an external program (e.g. pandoc) and display them in nvim. 3 | --- Uses the built-in HelpBrowser if no `config.documentation.cmd` is found. 4 | --- 5 | --- Users and plugin authors can override `config.documentation.on_open` and 6 | ---`config.documentation.on_select` callbacks to display help files or method 7 | --- results. 8 | ---@module scnvim.help 9 | ---@see scnvim.config 10 | 11 | local sclang = require 'scnvim.sclang' 12 | local config = require 'scnvim.config' 13 | local _path = require 'scnvim.path' 14 | local utils = require 'scnvim.utils' 15 | local action = require 'scnvim.action' 16 | 17 | local uv = vim.loop 18 | local api = vim.api 19 | local win_id = 0 20 | local M = {} 21 | 22 | --- Actions 23 | ---@section actions 24 | 25 | --- Action that runs when a help file is opened. 26 | --- The default is to open a split buffer. 27 | ---@param err nil on success or reason of error 28 | ---@param uri Help file URI 29 | ---@param pattern (optional) move cursor to line matching regex pattern 30 | M.on_open = action.new(function(err, uri, pattern) 31 | if err then 32 | utils.print(err) 33 | return 34 | end 35 | local is_open = vim.fn.win_gotoid(win_id) == 1 36 | local expr = string.format('edit %s', uri) 37 | if pattern then 38 | local subject = vim.fn.fnamemodify(uri, ':t:r') 39 | expr = string.format('edit +/^\\\\(%s\\\\)\\\\?%s %s', subject, pattern, uri) 40 | end 41 | if is_open then 42 | vim.cmd(expr) 43 | else 44 | local horizontal = config.documentation.horizontal 45 | local direction = config.documentation.direction 46 | if direction == 'top' or direction == 'left' then 47 | direction = 'leftabove' 48 | elseif direction == 'right' or direction == 'bot' then 49 | direction = 'rightbelow' 50 | else 51 | error '[scnvim] invalid config.documentation.direction' 52 | end 53 | local win_cmd = string.format('%s %s | %s', direction, horizontal and 'split' or 'vsplit', expr) 54 | vim.cmd(win_cmd) 55 | win_id = vim.fn.win_getid() 56 | end 57 | end) 58 | 59 | --- Get the render arguments with correct input and output file paths. 60 | ---@param input_path The input path to use. 61 | ---@param output_path The output path to use. 62 | ---@return A table with '$1' and '$2' replaced by @p input_path and @p output_path 63 | local function get_render_args(input_path, output_path) 64 | local args = vim.deepcopy(config.documentation.args) 65 | for index, str in ipairs(args) do 66 | if str == '$1' then 67 | args[index] = str:gsub('$1', input_path) 68 | end 69 | if str == '$2' then 70 | args[index] = str:gsub('$2', output_path) 71 | end 72 | end 73 | return args 74 | end 75 | 76 | --- Render a schelp file into vim help format. 77 | --- Uses config.documentation.cmd as the renderer. 78 | ---@param subject The subject to render (e.g. SinOsc) 79 | ---@param on_done A callback that receives the path to the rendered help file as its single argument 80 | --- TODO: cache. compare timestamp of help source with rendered .txt 81 | local function render_help_file(subject, on_done) 82 | local cmd = string.format('SCNvim.getHelpUri("%s")', subject) 83 | sclang.eval(cmd, function(input_path) 84 | local basename = input_path:gsub('%.html%.scnvim', '') 85 | local output_path = basename .. '.txt' 86 | local args = get_render_args(input_path, output_path) 87 | local options = { 88 | args = args, 89 | hide = true, 90 | } 91 | local prg = config.documentation.cmd 92 | uv.spawn( 93 | prg, 94 | options, 95 | vim.schedule_wrap(function(code) 96 | if code ~= 0 then 97 | error(string.format('%s error: %d', prg, code)) 98 | end 99 | local ret = uv.fs_unlink(input_path) 100 | if not ret then 101 | print('[scnvim] Could not unlink ' .. input_path) 102 | end 103 | on_done(output_path) 104 | end) 105 | ) 106 | end) 107 | end 108 | 109 | --- Helper function for the default browser implementation 110 | ---@param index The item to get from the quickfix list 111 | local function open_from_quickfix(index) 112 | local list = vim.fn.getqflist() 113 | local item = list[index] 114 | if item then 115 | local uri = vim.fn.bufname(item.bufnr) 116 | local subject = vim.fn.fnamemodify(uri, ':t:r') 117 | if uv.fs_stat(uri) then 118 | M.on_open(nil, uri, item.text) 119 | else 120 | render_help_file(subject, function(result) 121 | M.on_open(nil, result, item.text) 122 | end) 123 | end 124 | end 125 | end 126 | 127 | --- Action that runs when selecting documentation for a method. 128 | --- The default is to present the results in the quickfix window. 129 | ---@param err nil if no error otherwise string 130 | ---@param results Table with results 131 | M.on_select = action.new(function(err, results) 132 | if err then 133 | print(err) 134 | return 135 | end 136 | local id = api.nvim_create_augroup('scnvim_qf_conceal', { clear = true }) 137 | api.nvim_create_autocmd('BufWinEnter', { 138 | group = id, 139 | desc = 'Apply quickfix conceal', 140 | pattern = 'quickfix', 141 | callback = function() 142 | vim.cmd [[syntax match SCNvimConcealResults /^.*Help\/\|.txt\||.*|\|/ conceal]] 143 | vim.opt_local.conceallevel = 2 144 | vim.opt_local.concealcursor = 'nvic' 145 | end, 146 | }) 147 | vim.fn.setqflist(results) 148 | vim.cmd [[ copen ]] 149 | vim.keymap.set('n', '', function() 150 | local linenr = api.nvim_win_get_cursor(0)[1] 151 | open_from_quickfix(linenr) 152 | end, { buffer = true }) 153 | end) 154 | 155 | --- Find help files for a method 156 | ---@param name Method name to find. 157 | ---@param target_dir The help target dir (SCDoc.helpTargetDir) 158 | ---@return A table with method entries that is suitable for the quickfix list. 159 | local function find_methods(name, target_dir) 160 | local path = vim.fn.expand(target_dir) 161 | local docmap = M.get_docmap(_path.concat(path, 'docmap.json')) 162 | local results = {} 163 | for _, value in pairs(docmap) do 164 | for _, method in ipairs(value.methods) do 165 | local match = utils.str_match_exact(method, name) 166 | if match then 167 | local destpath = _path.concat(path, value.path .. '.txt') 168 | table.insert(results, { 169 | filename = destpath, 170 | text = string.format('.%s', name), 171 | }) 172 | end 173 | end 174 | end 175 | return results 176 | end 177 | 178 | --- Functions 179 | ---@section functions 180 | 181 | --- Get a table with a documentation overview 182 | ---@param target_dir The target help directory (SCDoc.helpTargetDir) 183 | ---@return A JSON formatted string 184 | function M.get_docmap(target_dir) 185 | if M.docmap then 186 | return M.docmap 187 | end 188 | local stat = uv.fs_stat(target_dir) 189 | assert(stat, 'Could not find docmap.json') 190 | local fd = uv.fs_open(target_dir, 'r', 0) 191 | local size = stat.size 192 | local file = uv.fs_read(fd, size, 0) 193 | local ok, result = pcall(vim.fn.json_decode, file) 194 | uv.fs_close(fd) 195 | if not ok then 196 | error(result) 197 | end 198 | return result 199 | end 200 | 201 | --- Open a help file. 202 | ---@param subject The help subject (SinOsc, tanh, etc.) 203 | function M.open_help_for(subject) 204 | if not sclang.is_running() then 205 | M.on_open 'sclang not running' 206 | return 207 | end 208 | 209 | if not config.documentation.cmd then 210 | local cmd = string.format('HelpBrowser.openHelpFor("%s")', subject) 211 | sclang.send(cmd, true) 212 | return 213 | end 214 | 215 | local is_class = subject:sub(1, 1):match '%u' 216 | if is_class then 217 | render_help_file(subject, function(result) 218 | M.on_open(nil, result) 219 | end) 220 | else 221 | sclang.eval('SCDoc.helpTargetDir', function(dir) 222 | local results = find_methods(subject, dir) 223 | local err = nil 224 | if #results == 0 then 225 | err = 'No results for ' .. tostring(subject) 226 | end 227 | M.on_select(err, results) 228 | end) 229 | end 230 | end 231 | 232 | --- Render all help files. 233 | ---@param callback Run this callback on completion. 234 | ---@param include_extensions Include SCClassLibrary extensions. 235 | ---@param concurrent_jobs Number of parallel jobs (default: 8) 236 | function M.render_all(callback, include_extensions, concurrent_jobs) 237 | include_extensions = include_extensions or true 238 | concurrent_jobs = concurrent_jobs or 8 239 | if not config.documentation.cmd then 240 | error '[scnvim] `config.documentation.cmd` must be defined' 241 | end 242 | local cmd = string.format('SCNvimDoc.renderAll(%s)', include_extensions) 243 | sclang.eval(cmd, function() 244 | sclang.eval('SCDoc.helpTargetDir', function(help_path) 245 | local sc_help_dir = _path.concat(help_path, 'Classes') 246 | 247 | local threads = {} 248 | local active_threads = 0 249 | local is_done = false 250 | 251 | local function schedule(n) 252 | if is_done then 253 | return 254 | end 255 | active_threads = n 256 | for i = 1, n do 257 | local thread = threads[i] 258 | if not thread then 259 | print '[scnvim] Help file conversion finished.' 260 | if callback then 261 | callback() 262 | end 263 | break 264 | end 265 | coroutine.resume(thread) 266 | table.remove(threads, i) 267 | end 268 | end 269 | 270 | local function on_done(exit_code, filename) 271 | if exit_code ~= 0 then 272 | local err = string.format('ERROR: Could not convert help file %s (code: %d)', filename, exit_code) 273 | utils.print(err) 274 | end 275 | active_threads = active_threads - 1 276 | local last_run = active_threads > #threads 277 | if active_threads == 0 or last_run and not is_done then 278 | local num_jobs = concurrent_jobs 279 | if last_run then 280 | num_jobs = #threads 281 | is_done = true 282 | end 283 | schedule(num_jobs) 284 | end 285 | end 286 | 287 | local handle = assert(uv.fs_scandir(sc_help_dir), 'Could not open SuperCollider help directory.') 288 | repeat 289 | local filename, type = uv.fs_scandir_next(handle) 290 | if type == 'file' and vim.endswith(filename, 'scnvim') then 291 | local basename = filename:gsub('%.html%.scnvim', '') 292 | local input_path = _path.concat(sc_help_dir, filename) 293 | local output_path = _path.concat(sc_help_dir, basename .. '.txt') 294 | local options = { 295 | args = get_render_args(input_path, output_path), 296 | hide = true, 297 | } 298 | local co = coroutine.create(function() 299 | uv.spawn(config.documentation.cmd, options, function(code) 300 | local ret = uv.fs_unlink(input_path) 301 | if not ret then 302 | utils.print('ERROR: Could not unlink ' .. input_path) 303 | end 304 | on_done(code, input_path) 305 | end) 306 | end) 307 | threads[#threads + 1] = co 308 | end 309 | until not filename 310 | 311 | print '[scnvim] Converting help files (this might take a while..)' 312 | schedule(concurrent_jobs) 313 | end) 314 | end) 315 | end 316 | 317 | return M 318 | -------------------------------------------------------------------------------- /lua/scnvim/install.lua: -------------------------------------------------------------------------------- 1 | ---@module scnvim.install 2 | ---@local 3 | 4 | local path = require 'scnvim.path' 5 | local M = {} 6 | 7 | local function get_link_target() 8 | local destination = path.get_user_extension_dir() 9 | vim.fn.mkdir(destination, 'p') 10 | return destination .. '/scide_scnvim' 11 | end 12 | 13 | --- Install the scnvim classes 14 | ---@local 15 | function M.install() 16 | local source = path.concat(path.get_plugin_root_dir(), 'scide_scnvim') 17 | local destination = get_link_target() 18 | path.link(source, destination) 19 | end 20 | 21 | --- Uninstall the scnvim classes 22 | ---@local 23 | function M.uninstall() 24 | local destination = get_link_target() 25 | path.unlink(destination) 26 | end 27 | 28 | --- Check if classes are linked 29 | ---@return Absolute path to Extensions/scide_scnvim 30 | ---@local 31 | function M.check() 32 | local link_target = get_link_target() 33 | return path.is_symlink(link_target) and link_target or nil 34 | end 35 | 36 | return M 37 | -------------------------------------------------------------------------------- /lua/scnvim/map.lua: -------------------------------------------------------------------------------- 1 | --- Helper object to define a keymap. 2 | --- Usually used exported from the scnvim module 3 | ---@module scnvim.map 4 | ---@see scnvim.editor 5 | ---@see scnvim 6 | ---@usage map('module.fn', { modes }) 7 | ---@usage scnvim.map('editor.send_line', {'i', 'n'}) 8 | ---@usage map(function() print 'hi' end) 9 | 10 | --- Valid modules. 11 | ---@table modules 12 | ---@field editor 13 | ---@field postwin 14 | ---@field sclang 15 | ---@field signature 16 | local modules = { 17 | 'editor', 18 | 'postwin', 19 | 'sclang', 20 | 'scnvim', 21 | 'signature', 22 | } 23 | 24 | local function validate(str) 25 | local module, fn = unpack(vim.split(str, '.', { plain = true })) 26 | if not fn then 27 | error(string.format('"%s" is not a valid input string to map', str), 0) 28 | end 29 | local res = vim.tbl_filter(function(m) 30 | return module == m 31 | end, modules) 32 | local valid_module = #res == 1 33 | if not valid_module then 34 | error(string.format('"%s" is not a valid module to map', module), 0) 35 | end 36 | if module ~= 'scnvim' then 37 | module = 'scnvim.' .. module 38 | end 39 | return module, fn 40 | end 41 | 42 | local map = setmetatable({}, { 43 | __call = function(_, fn, modes, options) 44 | modes = type(modes) == 'string' and { modes } or modes 45 | modes = modes or { 'n' } 46 | options = options or { 47 | desc = type(fn) == 'string' and ('scnvim: ' .. fn) or 'scnvim keymap', 48 | } 49 | if type(fn) == 'string' then 50 | local module, cmd = validate(fn) 51 | local wrapper = function() 52 | if module == 'scnvim.editor' then 53 | require(module)[cmd](options.callback, options.flash) 54 | else 55 | require(module)[cmd]() 56 | end 57 | end 58 | return { modes = modes, fn = wrapper, options = options } 59 | elseif type(fn) == 'function' then 60 | return { modes = modes, fn = fn, options = options } 61 | end 62 | end, 63 | }) 64 | 65 | local map_expr = function(expr, modes, options) 66 | modes = type(modes) == 'string' and { modes } or modes 67 | options = options or {} 68 | options.silent = options.silent == nil and true or options.silent 69 | options.desc = options.desc or 'sclang: ' .. expr 70 | return map(function() 71 | require('scnvim.sclang').send(expr, options.silent) 72 | end, modes, options) 73 | end 74 | 75 | return { 76 | map = map, 77 | map_expr = map_expr, 78 | } 79 | -------------------------------------------------------------------------------- /lua/scnvim/path.lua: -------------------------------------------------------------------------------- 1 | --- Path and platform related functions. 2 | --- '/' is the path separator for all platforms. 3 | ---@module scnvim.path 4 | 5 | local M = {} 6 | local uv = vim.loop 7 | local config = require 'scnvim.config' 8 | 9 | --- Get the host system 10 | ---@return 'windows', 'macos' or 'linux' 11 | function M.get_system() 12 | local sysname = uv.os_uname().sysname 13 | if sysname:match 'Windows' then 14 | return 'windows' 15 | elseif sysname:match 'Darwin' then 16 | return 'macos' 17 | else 18 | return 'linux' 19 | end 20 | end 21 | 22 | --- Returns true if current system is Windows otherwise false 23 | M.is_windows = (M.get_system() == 'windows') 24 | 25 | --- Get the scnvim cache directory. 26 | ---@return The absolute path to the cache directory 27 | function M.get_cache_dir() 28 | local cache_path = M.concat(vim.fn.stdpath 'cache', 'scnvim') 29 | cache_path = M.normalize(cache_path) 30 | vim.fn.mkdir(cache_path, 'p') 31 | return cache_path 32 | end 33 | 34 | --- Check if a path exists 35 | ---@param path The path to test 36 | ---@return True if the path exists otherwise false 37 | function M.exists(path) 38 | return uv.fs_stat(path) ~= nil 39 | end 40 | 41 | --- Check if a file is a symbolic link. 42 | ---@param path The path to test. 43 | ---@return True if the path is a symbolic link otherwise false 44 | function M.is_symlink(path) 45 | local stat = uv.fs_lstat(path) 46 | if stat then 47 | return stat.type == 'link' 48 | end 49 | return false 50 | end 51 | 52 | --- Get the path to a generated assset. 53 | --- 54 | --- * snippets 55 | --- * syntax 56 | --- * tags 57 | --- 58 | ---@param name The asset to get. 59 | ---@return Absolute path to the asset 60 | ---@usage path.get_asset 'snippets' 61 | function M.get_asset(name) 62 | local cache_dir = M.get_cache_dir() 63 | if name == 'snippets' then 64 | local filename = 'scnvim_snippets.lua' 65 | if config.snippet.engine.name == 'ultisnips' then 66 | filename = 'supercollider.snippets' 67 | end 68 | return M.concat(cache_dir, filename) 69 | elseif name == 'syntax' then 70 | return M.concat(cache_dir, 'classes.vim') 71 | elseif name == 'tags' then 72 | return M.concat(cache_dir, 'tags') 73 | end 74 | error '[scnvim] wrong asset type' 75 | end 76 | 77 | --- Concatenate items using the path separator. 78 | ---@param ... items to concatenate into a path 79 | ---@usage 80 | --- local cache_dir = path.get_cache_dir() 81 | --- local res = path.concat(cache_dir, 'subdir', 'file.txt') 82 | --- print(res) -- /Users/usr/.cache/nvim/scnvim/subdir/file.txt 83 | function M.concat(...) 84 | local items = { ... } 85 | return table.concat(items, '/') 86 | end 87 | 88 | --- Normalize a path to use Unix style separators: '/'. 89 | ---@param path The path to normalize. 90 | ---@return The normalized path. 91 | function M.normalize(path) 92 | return (path:gsub('\\', '/')) 93 | end 94 | 95 | --- Get the root dir of a plugin. 96 | ---@param plugin_name Optional plugin name, use nil to get scnvim root dir. 97 | ---@return Absolute path to the plugin root dir. 98 | function M.get_plugin_root_dir(plugin_name) 99 | plugin_name = plugin_name or 'scnvim' 100 | local paths = vim.api.nvim_list_runtime_paths() 101 | for _, path in ipairs(paths) do 102 | local index = path:find(plugin_name) 103 | if index and path:sub(index, -1) == plugin_name then 104 | return M.normalize(path) 105 | end 106 | end 107 | error(string.format('Could not get root dir for %s', plugin_name)) 108 | end 109 | 110 | --- Get the SuperCollider user extension directory. 111 | ---@return Platform specific user extension directory. 112 | function M.get_user_extension_dir() 113 | local sysname = M.get_system() 114 | local home_dir = uv.os_homedir() 115 | local xdg = uv.os_getenv 'XDG_DATA_HOME' 116 | if xdg then 117 | return xdg .. '/SuperCollider/Extensions' 118 | end 119 | if sysname == 'windows' then 120 | return M.normalize(home_dir) .. '/AppData/Local/SuperCollider/Extensions' 121 | elseif sysname == 'linux' then 122 | return home_dir .. '/.local/share/SuperCollider/Extensions' 123 | elseif sysname == 'macos' then 124 | return home_dir .. '/Library/Application Support/SuperCollider/Extensions' 125 | end 126 | error '[scnvim] could not get SuperCollider Extensions dir' 127 | end 128 | 129 | --- Create a symbolic link. 130 | ---@param source Absolute path to the source. 131 | ---@param destination Absolute path to the destination. 132 | function M.link(source, destination) 133 | if not uv.fs_stat(destination) then 134 | uv.fs_symlink(source, destination, { dir = true, junction = true }) 135 | end 136 | end 137 | 138 | --- Remove a symbolic link. 139 | ---@param link_path Absolute path for the file to unlink. 140 | function M.unlink(link_path) 141 | if M.is_symlink(link_path) then 142 | uv.fs_unlink(link_path) 143 | end 144 | end 145 | 146 | return M 147 | -------------------------------------------------------------------------------- /lua/scnvim/postwin.lua: -------------------------------------------------------------------------------- 1 | --- Post window. 2 | --- The interpreter's post window. 3 | ---@module scnvim.postwin 4 | 5 | local path = require 'scnvim.path' 6 | local config = require 'scnvim.config' 7 | local action = require 'scnvim.action' 8 | local api = vim.api 9 | local M = {} 10 | 11 | --- Actions 12 | ---@section actions 13 | 14 | --- Action for when the post window opens. 15 | --- The default is to apply the post window settings. 16 | M.on_open = action.new(function() 17 | vim.opt_local.buftype = 'nofile' 18 | vim.opt_local.bufhidden = 'hide' 19 | vim.opt_local.swapfile = false 20 | local decorations = { 21 | 'number', 22 | 'relativenumber', 23 | 'modeline', 24 | 'wrap', 25 | 'cursorline', 26 | 'cursorcolumn', 27 | 'foldenable', 28 | 'list', 29 | } 30 | for _, s in ipairs(decorations) do 31 | vim.opt_local[s] = false 32 | end 33 | vim.opt_local.colorcolumn = '' 34 | vim.opt_local.foldcolumn = '0' 35 | vim.opt_local.winfixwidth = true 36 | vim.opt_local.tabstop = 4 37 | end) 38 | 39 | --- Functions 40 | ---@section functions 41 | 42 | --- Test that the post window buffer is valid. 43 | ---@return True if the buffer is valid otherwise false. 44 | ---@private 45 | local function buf_is_valid() 46 | return M.buf and api.nvim_buf_is_loaded(M.buf) 47 | end 48 | 49 | --- Create a scratch buffer for the post window output. 50 | ---@return A buffer handle. 51 | ---@private 52 | local function create() 53 | if buf_is_valid() then 54 | return M.buf 55 | end 56 | local buf = api.nvim_create_buf(true, true) 57 | api.nvim_buf_set_option(buf, 'filetype', 'scnvim') 58 | api.nvim_buf_set_name(buf, '[scnvim]') 59 | M.buf = buf 60 | return buf 61 | end 62 | 63 | --- Save the last size of the post window. 64 | --@private 65 | local function save_last_size() 66 | if not config.postwin.float.enabled then 67 | if not config.postwin.horizontal then 68 | M.last_size = api.nvim_win_get_width(M.win) 69 | else 70 | M.last_size = api.nvim_win_get_height(M.win) 71 | end 72 | end 73 | end 74 | 75 | local function resolve(v) 76 | if type(v) == 'function' then 77 | return v() 78 | end 79 | return v 80 | end 81 | 82 | --- Open a floating post window 83 | ---@private 84 | local function open_float() 85 | local width = resolve(config.postwin.float.width) 86 | local height = resolve(config.postwin.float.height) 87 | local row = resolve(config.postwin.float.row) 88 | local col = resolve(config.postwin.float.col) 89 | local options = { 90 | relative = 'editor', 91 | anchor = 'NE', 92 | row = row, 93 | col = col, 94 | width = math.floor(width), 95 | height = math.floor(height), 96 | border = 'single', 97 | style = 'minimal', 98 | } 99 | options = vim.tbl_deep_extend('keep', config.postwin.float.config, options) 100 | local id = api.nvim_open_win(M.buf, false, options) 101 | local callback = config.postwin.float.callback 102 | if callback then 103 | callback(id) 104 | end 105 | return id 106 | end 107 | 108 | --- Open a post window as a split 109 | ---@private 110 | local function open_split() 111 | local horizontal = config.postwin.horizontal 112 | local direction = config.postwin.direction 113 | if direction == 'top' or direction == 'left' then 114 | direction = 'topleft' 115 | elseif direction == 'right' or direction == 'bot' then 116 | direction = 'botright' 117 | else 118 | error '[scnvim] invalid config.postwin.direction' 119 | end 120 | local win_cmd = string.format('%s %s', direction, horizontal and 'split' or 'vsplit') 121 | vim.cmd(win_cmd) 122 | local id = api.nvim_get_current_win() 123 | local size 124 | if config.postwin.fixed_size then 125 | size = config.postwin.fixed_size 126 | else 127 | size = M.last_size or config.postwin.size 128 | end 129 | if horizontal then 130 | api.nvim_win_set_height(id, math.floor(size or vim.o.lines / 3)) 131 | else 132 | api.nvim_win_set_width(id, math.floor(size or vim.o.columns / 2)) 133 | end 134 | api.nvim_win_set_buf(id, M.buf) 135 | vim.cmd [[ wincmd p ]] 136 | return id 137 | end 138 | 139 | --- Open the post window. 140 | ---@return A window handle. 141 | function M.open() 142 | if M.is_open() then 143 | return M.win 144 | end 145 | if not buf_is_valid() then 146 | create() 147 | end 148 | if config.postwin.float.enabled then 149 | M.win = open_float() 150 | else 151 | M.win = open_split() 152 | end 153 | vim.api.nvim_win_call(M.win, M.on_open) 154 | return M.win 155 | end 156 | 157 | --- Test if the window is open. 158 | ---@return True if open otherwise false. 159 | function M.is_open() 160 | return M.win ~= nil and api.nvim_win_is_valid(M.win) 161 | end 162 | 163 | --- Close the post window. 164 | function M.close() 165 | if M.is_open() then 166 | save_last_size() 167 | api.nvim_win_close(M.win, false) 168 | M.win = nil 169 | end 170 | end 171 | 172 | --- Destroy the post window. 173 | --- Calling this function closes the post window and deletes the buffer. 174 | function M.destroy() 175 | if M.is_open() then 176 | -- This call can fail if its the last window 177 | pcall(api.nvim_win_close, M.win, true) 178 | M.win = nil 179 | end 180 | if buf_is_valid() then 181 | api.nvim_buf_delete(M.buf, { force = true }) 182 | M.buf = nil 183 | end 184 | M.last_size = nil 185 | end 186 | 187 | --- Toggle the post window. 188 | function M.toggle() 189 | if M.is_open() then 190 | M.close() 191 | else 192 | M.open() 193 | end 194 | end 195 | 196 | --- Clear the post window buffer. 197 | function M.clear() 198 | if buf_is_valid() then 199 | api.nvim_buf_set_lines(M.buf, 0, -1, true, {}) 200 | end 201 | end 202 | 203 | --- Open the post window and move to it. 204 | function M.focus() 205 | local win = M.open() 206 | vim.fn.win_gotoid(win) 207 | end 208 | 209 | --- Print a line to the post window. 210 | ---@param line The line to print. 211 | function M.post(line) 212 | if not buf_is_valid() then 213 | return 214 | end 215 | 216 | local auto_toggle_error = config.postwin.auto_toggle_error 217 | local scrollback = config.postwin.scrollback 218 | 219 | local found_error = line:match '^ERROR' 220 | if found_error and auto_toggle_error then 221 | if not M.is_open() then 222 | M.open() 223 | end 224 | end 225 | 226 | if path.is_windows then 227 | line = line:gsub('\r', '') 228 | end 229 | vim.api.nvim_buf_set_lines(M.buf, -1, -1, true, { line }) 230 | 231 | local num_lines = vim.api.nvim_buf_line_count(M.buf) 232 | if scrollback > 0 and num_lines > scrollback then 233 | vim.api.nvim_buf_set_lines(M.buf, 0, 1, true, {}) 234 | num_lines = vim.api.nvim_buf_line_count(M.buf) 235 | end 236 | 237 | if M.is_open() then 238 | vim.api.nvim_win_set_cursor(M.win, { num_lines, 0 }) 239 | end 240 | end 241 | 242 | return M 243 | -------------------------------------------------------------------------------- /lua/scnvim/sclang.lua: -------------------------------------------------------------------------------- 1 | --- Sclang wrapper. 2 | ---@module scnvim.sclang 3 | 4 | local postwin = require 'scnvim.postwin' 5 | local udp = require 'scnvim.udp' 6 | local path = require 'scnvim.path' 7 | local config = require 'scnvim.config' 8 | local action = require 'scnvim.action' 9 | 10 | local uv = vim.loop 11 | local M = {} 12 | 13 | local cmd_char = { 14 | interpret_print = string.char(0x0c), 15 | interpret = string.char(0x1b), 16 | recompile = string.char(0x18), 17 | } 18 | 19 | --- Utilities 20 | 21 | local on_stdout = function() 22 | local stack = { '' } 23 | return function(err, data) 24 | assert(not err, err) 25 | if data then 26 | table.insert(stack, data) 27 | local str = table.concat(stack, '') 28 | local got_line = vim.endswith(str, '\n') 29 | if got_line then 30 | local lines = vim.gsplit(str, '\n') 31 | for line in lines do 32 | if line ~= '' then 33 | M.on_output(line) 34 | end 35 | end 36 | stack = { '' } 37 | end 38 | end 39 | end 40 | end 41 | 42 | local function safe_close(handle) 43 | if handle and not handle:is_closing() then 44 | handle:close() 45 | end 46 | end 47 | 48 | --- Actions 49 | ---@section actions 50 | 51 | --- Action that runs before sclang is started. 52 | --- The default is to open the post window. 53 | M.on_init = action.new(function() 54 | postwin.open() 55 | end) 56 | 57 | --- Action that runs on sclang exit 58 | --- The default is to destory the post window. 59 | ---@param code The exit code 60 | ---@param signal Terminating signal 61 | M.on_exit = action.new(function(code, signal) -- luacheck: no unused args 62 | postwin.destroy() 63 | end) 64 | 65 | --- Action that runs on sclang output. 66 | --- The default is to print a line to the post window. 67 | ---@param line A complete line of sclang output. 68 | M.on_output = action.new(function(line) 69 | postwin.post(line) 70 | end) 71 | 72 | --- Functions 73 | ---@section functions 74 | 75 | function M.find_sclang_executable() 76 | if config.sclang.cmd then 77 | return config.sclang.cmd 78 | end 79 | local exe_path = vim.fn.exepath 'sclang' 80 | if exe_path ~= '' then 81 | return exe_path 82 | end 83 | local system = path.get_system() 84 | if system == 'macos' then 85 | local app = 'SuperCollider.app/Contents/MacOS/sclang' 86 | local locations = { '/Applications', '/Applications/SuperCollider' } 87 | for _, loc in ipairs(locations) do 88 | local app_path = string.format('%s/%s', loc, app) 89 | if vim.fn.executable(app_path) ~= 0 then 90 | return app_path 91 | end 92 | end 93 | elseif system == 'windows' then -- luacheck: ignore 94 | -- TODO: a default path for Windows 95 | elseif system == 'linux' then -- luacheck: ignore 96 | -- TODO: a default path for Windows 97 | end 98 | error 'Could not find `sclang`. Add `sclang.path` to your configuration.' 99 | end 100 | 101 | local function on_exit(code, signal) 102 | M.stdin:shutdown() 103 | M.stdout:read_stop() 104 | M.stderr:read_stop() 105 | safe_close(M.stdin) 106 | safe_close(M.stdout) 107 | safe_close(M.stderr) 108 | safe_close(M.proc) 109 | M.on_exit(code, signal) 110 | M.proc = nil 111 | end 112 | 113 | local function start_process() 114 | M.stdin = uv.new_pipe(false) 115 | M.stdout = uv.new_pipe(false) 116 | M.stderr = uv.new_pipe(false) 117 | local sclang = M.find_sclang_executable() 118 | local options = {} 119 | options.stdio = { 120 | M.stdin, 121 | M.stdout, 122 | M.stderr, 123 | } 124 | options.cwd = vim.fn.expand '%:p:h' 125 | for _, arg in ipairs(config.sclang.args) do 126 | if arg:match '-i' then 127 | error '[scnvim] invalid sclang argument "-i"' 128 | end 129 | if arg:match '-d' then 130 | error '[scnvim] invalid sclang argument "-d"' 131 | end 132 | end 133 | options.args = { '-i', 'scnvim', '-d', options.cwd, unpack(config.sclang.args) } 134 | options.hide = true 135 | return uv.spawn(sclang, options, vim.schedule_wrap(on_exit)) 136 | end 137 | 138 | --- Set the current document path 139 | ---@local 140 | function M.set_current_path() 141 | if M.is_running() then 142 | local curpath = vim.fn.expand '%:p' 143 | curpath = vim.fn.escape(curpath, [[ \]]) 144 | curpath = string.format('SCNvim.currentPath = "%s"', curpath) 145 | M.send(curpath, true) 146 | end 147 | end 148 | 149 | --- Start polling the server status 150 | ---@local 151 | function M.poll_server_status() 152 | local cmd = string.format('SCNvim.updateStatusLine(%d)', config.statusline.poll_interval) 153 | M.send(cmd, true) 154 | end 155 | 156 | --- Generate assets. tags syntax etc. 157 | ---@param on_done Optional callback that runs when all assets have been created. 158 | function M.generate_assets(on_done) 159 | assert(M.is_running(), '[scnvim] sclang not running') 160 | local format = config.snippet.engine.name 161 | local expr = string.format([[SCNvim.generateAssets("%s", "%s")]], path.get_cache_dir(), format) 162 | M.eval(expr, on_done) 163 | end 164 | 165 | --- Send a "hard stop" to the interpreter. 166 | function M.hard_stop() 167 | M.send('thisProcess.stop', true) 168 | end 169 | 170 | --- Check if the process is running. 171 | ---@return True if running otherwise false. 172 | function M.is_running() 173 | return M.proc and M.proc:is_active() or false 174 | end 175 | 176 | --- Send code to the interpreter. 177 | ---@param data The code to send. 178 | ---@param silent If true will not echo output to the post window. 179 | function M.send(data, silent) 180 | silent = silent or false 181 | if M.is_running() then 182 | M.stdin:write { 183 | data, 184 | not silent and cmd_char.interpret_print or cmd_char.interpret, 185 | } 186 | end 187 | end 188 | 189 | --- Evaluate a SuperCollider expression and return the result to lua. 190 | ---@param expr The expression to evaluate. 191 | ---@param cb The callback with a single argument that contains the result. 192 | function M.eval(expr, cb) 193 | vim.validate { 194 | expr = { expr, 'string' }, 195 | cb = { cb, 'function' }, 196 | } 197 | expr = vim.fn.escape(expr, '"') 198 | local id = udp.push_eval_callback(cb) 199 | local cmd = string.format('SCNvim.eval("%s", "%s");', expr, id) 200 | M.send(cmd, true) 201 | end 202 | 203 | --- Start the sclang process. 204 | function M.start() 205 | if M.is_running() then 206 | vim.notify('sclang already started', vim.log.levels.INFO) 207 | return 208 | end 209 | 210 | M.on_init() 211 | 212 | M.proc = start_process() 213 | assert(M.proc, 'Could not start sclang process') 214 | 215 | local port = udp.start_server() 216 | assert(port > 0, 'Could not start UDP server') 217 | M.send(string.format('SCNvim.port = %d', port), true) 218 | M.set_current_path() 219 | 220 | local onread = on_stdout() 221 | M.stdout:read_start(vim.schedule_wrap(onread)) 222 | M.stderr:read_start(vim.schedule_wrap(onread)) 223 | end 224 | 225 | --- Stop the sclang process. 226 | function M.stop(_, callback) 227 | if not M.is_running() then 228 | return 229 | end 230 | udp.stop_server() 231 | M.send('0.exit', true) 232 | local timer = uv.new_timer() 233 | timer:start(1000, 0, function() 234 | if M.proc then 235 | local ret = M.proc:kill 'sigkill' 236 | if ret == 0 then 237 | timer:close() 238 | if callback then 239 | vim.schedule(callback) 240 | end 241 | M.proc = nil 242 | end 243 | else 244 | -- process exited during timer loop 245 | timer:close() 246 | if callback then 247 | vim.schedule(callback) 248 | end 249 | end 250 | end) 251 | end 252 | 253 | function M.reboot() 254 | M.stop(nil, M.start) 255 | end 256 | 257 | --- Recompile the class library. 258 | function M.recompile() 259 | if not M.is_running() then 260 | vim.notify('sclang not started', vim.log.levels.ERROR) 261 | return 262 | end 263 | M.send(cmd_char.recompile, true) 264 | M.send(string.format('SCNvim.port = %d', udp.port), true) 265 | M.set_current_path() 266 | end 267 | 268 | return M 269 | -------------------------------------------------------------------------------- /lua/scnvim/settings.lua: -------------------------------------------------------------------------------- 1 | --- Settings 2 | --- 3 | --- Returns a single function that applies default settings. 4 | ---@module scnvim.settings 5 | ---@local 6 | 7 | local config = require 'scnvim.config' 8 | local path = require 'scnvim.path' 9 | 10 | return function() 11 | -- tags 12 | local tags_file = path.get_asset 'tags' 13 | if path.exists(tags_file) then 14 | vim.opt_local.tags:append(tags_file) 15 | end 16 | 17 | -- help system 18 | vim.opt_local.keywordprg = ':SCNvimHelp' 19 | 20 | -- comments 21 | vim.opt_local.commentstring = '//%s' 22 | 23 | if not config.editor.signature.float then 24 | -- disable showmode to be able to see the printed signature 25 | vim.opt_local.showmode = false 26 | vim.opt_local.shortmess:append 'c' 27 | end 28 | 29 | -- matchit 30 | -- TODO: are these really needed? 31 | vim.api.nvim_buf_set_var(0, 'match_skip', 's:scComment|scString|scSymbol') 32 | vim.api.nvim_buf_set_var(0, 'match_words', '(:),[:],{:}') 33 | end 34 | -------------------------------------------------------------------------------- /lua/scnvim/signature.lua: -------------------------------------------------------------------------------- 1 | --- Signature help. 2 | ---@module scnvim.signature 3 | ---@local 4 | 5 | --- TODO: refactor to use vim.diagnostic? 6 | 7 | local sclang = require 'scnvim.sclang' 8 | local config = require 'scnvim.config' 9 | local api = vim.api 10 | local lsp_util = vim.lsp.util 11 | local hint_winid = nil 12 | 13 | local M = {} 14 | 15 | local function get_method_signature(object, cb) 16 | local cmd = string.format('SCNvim.methodArgs("%s")', object) 17 | sclang.eval(cmd, cb) 18 | end 19 | 20 | local function is_outside_of_statment(line, line_to_cursor) 21 | local line_endswith = vim.endswith(line, ')') or vim.endswith(line, ';') 22 | local curs_line_endswith = vim.endswith(line_to_cursor, ')') or vim.endswith(line_to_cursor, ';') 23 | return line_endswith and curs_line_endswith 24 | end 25 | 26 | local function extract_objects_helper(str) 27 | local objects = vim.split(str, '(', true) 28 | -- split arguments 29 | objects = vim.tbl_map(function(s) 30 | return vim.split(s, ',', true) 31 | end, objects) 32 | objects = vim.tbl_flatten(objects) 33 | objects = vim.tbl_map(function(s) 34 | -- filter out empty strings (nvim 0.5.1 compatability fix, use 35 | -- vim.split(..., {trimempty = true}) for nvim 0.6) 36 | if s == '' then 37 | return nil 38 | end 39 | -- filter out strings 40 | s = vim.trim(s) 41 | if s:sub(1, 1) == '"' then 42 | return nil 43 | end 44 | -- filter out trailing parens (from insert mode) 45 | s = s:gsub('%)', '') 46 | local obj_start = s:find '%u' 47 | return obj_start and s:sub(obj_start, -1) 48 | end, objects) 49 | objects = vim.tbl_filter(function(s) 50 | return s ~= nil 51 | end, objects) 52 | local len = #objects 53 | if len > 0 then 54 | return vim.trim(objects[len]) 55 | end 56 | return '' 57 | end 58 | 59 | local function get_line_info() 60 | local _, col = unpack(api.nvim_win_get_cursor(0)) 61 | local line = api.nvim_get_current_line() 62 | local line_to_cursor = line:sub(1, col + 1) 63 | return line, line_to_cursor 64 | end 65 | 66 | local function extract_object() 67 | local line, line_to_cursor = get_line_info() 68 | -- outside of any statement 69 | if is_outside_of_statment(line, line_to_cursor) then 70 | return '' 71 | end 72 | -- inside a multiline call 73 | if not line_to_cursor:find '%(' then 74 | local lnum = vim.fn.searchpair('(', '', ')', 'bnzW') 75 | if lnum > 0 then 76 | local ok, res = pcall(api.nvim_buf_get_lines, 0, lnum - 1, lnum, true) 77 | if ok then 78 | line_to_cursor = res[1] 79 | end 80 | end 81 | end 82 | -- ignore completed calls 83 | local ignore = line_to_cursor:match '%((.*)%)' 84 | if ignore then 85 | ignore = ignore .. ')' 86 | line_to_cursor = line_to_cursor:gsub(vim.pesc(ignore), '') 87 | end 88 | line_to_cursor = line_to_cursor:match '.*%(' 89 | return extract_objects_helper(line_to_cursor) 90 | end 91 | 92 | local function ins_extract_object() 93 | local _, line_to_cursor = get_line_info() 94 | return extract_objects_helper(line_to_cursor) 95 | end 96 | 97 | local function show_signature(object) 98 | if object ~= '' then 99 | local float = config.editor.signature.float 100 | local float_conf = config.editor.signature.config 101 | get_method_signature(object, function(res) 102 | local signature = res:match '%((.+)%)' 103 | if signature then 104 | if float then 105 | local _, id = lsp_util.open_floating_preview({ signature }, 'supercollider', float_conf) 106 | hint_winid = id 107 | else 108 | print(signature) 109 | end 110 | end 111 | end) 112 | end 113 | end 114 | 115 | local function close_signature() 116 | vim.api.nvim_win_close(hint_winid, false) 117 | hint_winid = nil 118 | end 119 | 120 | --- Show signature from normal mode 121 | function M.show() 122 | local ok, object = pcall(extract_object) 123 | if ok then 124 | pcall(show_signature, object) 125 | end 126 | end 127 | 128 | --- Show signature from insert mode 129 | function M.ins_show() 130 | if vim.v.char == '(' then 131 | local ok, object = pcall(ins_extract_object) 132 | if ok then 133 | pcall(show_signature, object) 134 | end 135 | end 136 | end 137 | 138 | -- Close signature hint window 139 | function M.close() 140 | if hint_winid ~= nil and vim.api.nvim_win_is_valid(hint_winid) then 141 | pcall(close_signature) 142 | end 143 | end 144 | 145 | -- Toggle signature hint window 146 | function M.toggle() 147 | if hint_winid ~= nil and vim.api.nvim_win_is_valid(hint_winid) then 148 | pcall(close_signature) 149 | else 150 | M.show() 151 | end 152 | end 153 | 154 | return M 155 | -------------------------------------------------------------------------------- /lua/scnvim/statusline.lua: -------------------------------------------------------------------------------- 1 | --- Status line. 2 | ---@module scnvim.statusline 3 | local M = {} 4 | 5 | local widgets = { 6 | statusline = '', 7 | } 8 | 9 | --- Set the server status 10 | ---@param str The server status string. 11 | function M.set_server_status(str) 12 | widgets.statusline = str 13 | end 14 | 15 | --- Get the server status. 16 | ---@return A string containing the server status. 17 | function M.get_server_status() 18 | return widgets.statusline or '' 19 | end 20 | 21 | return M 22 | -------------------------------------------------------------------------------- /lua/scnvim/udp.lua: -------------------------------------------------------------------------------- 1 | --- UDP 2 | --- Receive data from sclang as UDP datagrams. 3 | --- The data should be in the form of JSON formatted strings. 4 | ---@module scnvim.udp 5 | ---@local 6 | 7 | local uv = vim.loop 8 | local M = {} 9 | 10 | local HOST = '127.0.0.1' 11 | local PORT = 0 12 | local eval_callbacks = {} 13 | local callback_id = '0' 14 | 15 | --- UDP handlers. 16 | --- Run the matching function in this table for the incoming 'action' parameter. 17 | local Handlers = {} 18 | 19 | --- Evaluate a piece of lua code sent from sclang 20 | ---@local 21 | function Handlers.luaeval(codestring) 22 | if not codestring then 23 | return 24 | end 25 | local func = loadstring(codestring) 26 | local ok, result = pcall(func) 27 | if not ok then 28 | print('[scnvim] luaeval: ' .. result) 29 | end 30 | end 31 | 32 | --- Receive data from sclang 33 | ---@local 34 | function Handlers.eval(object) 35 | assert(object) 36 | local callback = eval_callbacks[object.id] 37 | if callback then 38 | callback(object.result) 39 | eval_callbacks[object.id] = nil 40 | end 41 | end 42 | 43 | --- Callback for UDP datagrams 44 | local function on_receive(err, chunk) 45 | assert(not err, err) 46 | if chunk then 47 | local ok, object = pcall(vim.fn.json_decode, chunk) 48 | if not ok then 49 | error('[scnvim] Could not decode json chunk: ' .. object) 50 | end 51 | local func = Handlers[object.action] 52 | assert(func, '[scnvim] Unrecognized handler') 53 | func(object.args) 54 | end 55 | end 56 | 57 | --- Start the UDP server. 58 | function M.start_server() 59 | local handle = uv.new_udp 'inet' 60 | assert(handle, 'Could not create UDP handle') 61 | handle:bind(HOST, PORT, { reuseaddr = true }) 62 | handle:recv_start(vim.schedule_wrap(on_receive)) 63 | M.port = handle:getsockname().port 64 | M.udp = handle 65 | return M.port 66 | end 67 | 68 | --- Stop the UDP server. 69 | function M.stop_server() 70 | if M.udp then 71 | M.udp:recv_stop() 72 | if not M.udp:is_closing() then 73 | M.udp:close() 74 | end 75 | M.udp = nil 76 | end 77 | end 78 | 79 | --- Push a callback to be evaluated later. 80 | --- Utility function for the scnvim.eval API. 81 | ---@local 82 | function M.push_eval_callback(cb) 83 | callback_id = tostring(tonumber(callback_id) + 1) 84 | eval_callbacks[callback_id] = cb 85 | return callback_id 86 | end 87 | 88 | return M 89 | -------------------------------------------------------------------------------- /lua/scnvim/utils.lua: -------------------------------------------------------------------------------- 1 | --- Utility functions. 2 | ---@module scnvim.utils 3 | 4 | local M = {} 5 | local _path = require 'scnvim.path' 6 | 7 | --- Returns the content of a lua file on disk 8 | ---@param path The path to the file to load 9 | function M.load_file(path) 10 | -- this check is here because loadfile will read from stdin if nil 11 | if not path then 12 | error '[scnvim] no path to read' 13 | end 14 | local func, err = loadfile(path) 15 | if not func then 16 | error(err) 17 | end 18 | local ok, content = pcall(func) 19 | if not ok then 20 | error(content) 21 | end 22 | return content 23 | end 24 | 25 | --- Match an exact occurence of word 26 | --- (replacement for \b word boundary) 27 | ---@param input The input string 28 | ---@param word The word to match 29 | ---@return True if word matches, otherwise false 30 | function M.str_match_exact(input, word) 31 | return string.find(input, '%f[%a]' .. word .. '%f[%A]') ~= nil 32 | end 33 | 34 | --- Print a highlighted message to the command line. 35 | ---@param message The message to print. 36 | ---@param hlgroup The highlight group to use. Default = ErrorMsg 37 | function M.print(message, hlgroup) 38 | local expr = string.format('echohl %s | echom "[scnvim] " . "%s" | echohl None', hlgroup or 'ErrorMsg', message) 39 | vim.cmd(expr) 40 | end 41 | 42 | --- Get the content of the auto generated snippet file. 43 | ---@return A table with the snippets. 44 | function M.get_snippets() 45 | return M.load_file(_path.get_asset 'snippets') 46 | end 47 | 48 | return M 49 | -------------------------------------------------------------------------------- /scide_scnvim/Classes/Document.sc: -------------------------------------------------------------------------------- 1 | // nvim Document implementation 2 | // 3 | // The following code is copied/adapted from the Document implementation found in ScIDE.sc 4 | // License GPLv3 5 | 6 | Document { 7 | classvar netAddr; 3 | classvar <>currentPath; 4 | classvar <>port; 5 | 6 | *sendJSON {|data| 7 | var json; 8 | if (netAddr.isNil) { 9 | netAddr = NetAddr("127.0.0.1", SCNvim.port); 10 | }; 11 | json = SCNvimJSON.stringify(data); 12 | if (json.notNil) { 13 | netAddr.sendRaw(json); 14 | } { 15 | "[scnvim] could not encode to json: %".format(data).warn; 16 | } 17 | } 18 | 19 | *luaeval{|luacode| 20 | SCNvim.sendJSON((action: "luaeval", args: luacode)) 21 | } 22 | 23 | *eval {|expr, callback_id| 24 | var result = expr.interpret; 25 | result = (action: "eval", args: (result: result, id: callback_id)); 26 | SCNvim.sendJSON(result); 27 | } 28 | 29 | *updateStatusLine {arg interval=1; 30 | var stlFunc = { 31 | var serverStatus, data; 32 | var peakCPU, avgCPU, numUGens, numSynths; 33 | var server = Server.default; 34 | var cmd; 35 | if (server.serverRunning) { 36 | peakCPU = server.peakCPU.trunc(0.01); 37 | avgCPU = server.avgCPU.trunc(0.01); 38 | numUGens = "%u".format(server.numUGens); 39 | numSynths = "%s".format(server.numSynths); 40 | serverStatus = "%\\% %\\% % %".format( 41 | peakCPU, avgCPU, numUGens, numSynths 42 | ); 43 | cmd = "require'scnvim.statusline'.set_server_status('%')".format(serverStatus); 44 | SCNvim.luaeval(cmd); 45 | } 46 | }; 47 | SkipJack(stlFunc, interval, name: "scnvim_statusline"); 48 | } 49 | 50 | *generateAssets {|cacheDir, snippetFormat = "ultisnips"| 51 | var tagsPath = cacheDir +/+ "tags"; 52 | var syntaxPath = cacheDir +/+ "classes.vim"; 53 | var snippetPath = cacheDir; 54 | case 55 | {snippetFormat == "ultisnips"} 56 | { 57 | snippetPath = snippetPath +/+ "supercollider.snippets"; 58 | } 59 | {snippetFormat == "snippets.nvim" or: { snippetFormat == "luasnip" }} 60 | { 61 | snippetPath = snippetPath +/+ "scnvim_snippets.lua"; 62 | } 63 | { 64 | "Unrecognized snippet format: '%'".format(snippetFormat).warn; 65 | snippetPath = nil; 66 | }; 67 | Routine.run { 68 | SCNvim.generateTags(tagsPath); 69 | SCNvim.generateSyntax(syntaxPath); 70 | if (snippetPath.notNil) { 71 | SCNvim.generateSnippets(snippetPath, snippetFormat); 72 | } 73 | }; 74 | } 75 | 76 | *generateSyntax {arg outputPath; 77 | var path, file, classes; 78 | classes = Class.allClasses.collect {|class| 79 | class.asString ++ " "; 80 | }; 81 | path = outputPath.standardizePath; 82 | file = File.open(path, "w"); 83 | file.write("syn keyword scObject "); 84 | file.putAll(classes); 85 | file.close; 86 | "Generated syntax file: %".format(path).postln; 87 | } 88 | 89 | // copied from SCVim.sc 90 | // modified to produce a sorted tags file 91 | // GPLv3 license 92 | *generateTags {arg outputPath; 93 | var tagPath, tagFile; 94 | var tags = []; 95 | 96 | tagPath = outputPath ? "~/.sctags"; 97 | tagPath = tagPath.standardizePath; 98 | 99 | tagFile = File.open(tagPath, "w"); 100 | 101 | tagFile.write('!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/'.asString ++ Char.nl); 102 | tagFile.write("!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/" ++ Char.nl); 103 | tagFile.write("!_TAG_PROGRAM_AUTHOR Stephen Lumenta /stephen.lumenta@gmail.com/" ++ Char.nl); 104 | tagFile.write("!_TAG_PROGRAM_NAME SCNVim.sc//" ++ Char.nl); 105 | tagFile.write("!_TAG_PROGRAM_URL https://github.com/davidgranstrom/scnvim" ++ Char.nl); 106 | tagFile.write("!_TAG_PROGRAM_VERSION 2.0//" ++ Char.nl); 107 | 108 | Class.allClasses.do {arg klass; 109 | var klassName, klassFilename, klassSearchString; 110 | var result; 111 | 112 | klassName = klass.asString; 113 | klassFilename = klass.filenameSymbol; 114 | // use a symbol and convert to string to avoid the "open ended 115 | // string" error on class lib compiliation 116 | klassSearchString = '/^%/;"%%'.asString.format(klassName, Char.tab, "c"); 117 | 118 | result = klassName ++ Char.tab ++ klassFilename ++ Char.tab ++ klassSearchString ++ Char.nl; 119 | tags = tags.add(result); 120 | 121 | klass.methods.do {arg meth; 122 | var methName, methFilename, methSearchString; 123 | methName = meth.name; 124 | methFilename = meth.filenameSymbol; 125 | methSearchString = '/% {/;"%%'.asString.format(methName, Char.tab, "m"); 126 | result = methName ++ Char.tab ++ methFilename ++ Char.tab ++ methSearchString ++ Char.nl; 127 | tags = tags.add(result); 128 | } 129 | }; 130 | 131 | tags = tags.sort; 132 | tagFile.putAll(tags); 133 | tagFile.close; 134 | "Generated tags file: %".format(tagPath).postln; 135 | } 136 | 137 | *generateSnippets {arg outputPath, snippetFormat; 138 | var file, path; 139 | var snippets = []; 140 | 141 | path = outputPath ? "~/.scsnippets"; 142 | path = path.standardizePath; 143 | file = File.open(path, "w"); 144 | snippetFormat = snippetFormat ? "ultisnips"; 145 | 146 | Class.allClasses.do {arg klass; 147 | var className, argList, signature; 148 | if (klass.asString.beginsWith("Meta_").not) { 149 | // collect all creation methods 150 | klass.class.methods.do {arg meth; 151 | var index, snippet; 152 | var snippetName; 153 | // classvars with getter/setters produces an error 154 | // since we're only interested in creation methods we skip them 155 | try { 156 | snippetName = "%.%".format(klass, meth.name); 157 | signature = Help.methodArgs(snippetName); 158 | }; 159 | 160 | if (signature.notNil and:{signature.isEmpty.not}) { 161 | index = signature.find("("); 162 | className = signature[..index - 1]; 163 | className = className.replace("*", ".").replace(" ", ""); 164 | 165 | if(snippetFormat == "luasnip", { 166 | 167 | // LuaSnip 168 | argList = signature[index..]; 169 | argList = argList.replace("(", "").replace(")", ""); 170 | argList = argList.split($,); 171 | argList = argList.collect {|a, i| 172 | var scArg = a.replace(" ", "").split($:); 173 | var scArgName = scArg[0]; 174 | var scArgVal = scArg[1]; 175 | var snipArgument; 176 | var escapeChars; 177 | 178 | // Create argument name as text node 179 | snipArgument = "t(\"%:\"),".format(scArgName); 180 | 181 | // Escape all characters that might break the snippet 182 | escapeChars = [ 183 | $\\, 184 | $", 185 | $', 186 | ]; 187 | 188 | escapeChars.do {|char| 189 | scArgVal = scArgVal.asString.replace(char, "\\" ++ char.asString); 190 | }; 191 | 192 | // Create argument default value as insert node 193 | snipArgument = snipArgument ++ "i(%, %)".format(i+1, scArgVal.quote); 194 | 195 | // Only add text node with comma if not last item 196 | if(i+1 != argList.size, { 197 | snipArgument = snipArgument ++ ",t(\", \")" 198 | }); 199 | 200 | snipArgument 201 | }; 202 | 203 | argList = "t(\"(\")," ++ argList.join(", ") ++ ", t(\")\"),"; 204 | snippet = "t(\"%\"),".format(className) ++ argList; 205 | 206 | // Not sure why this is necessary but some snippets generate new lines? 207 | snippet = snippet.replace(Char.nl, ""); 208 | 209 | }, { 210 | // UltiSnips, Snippets.nvim 211 | argList = signature[index..]; 212 | argList = argList.replace("(", "").replace(")", ""); 213 | argList = argList.split($,); 214 | argList = argList.collect {|a, i| 215 | "${%:%}".format(i+1, a) 216 | }; 217 | argList = "(" ++ argList.join(", ") ++ ")"; 218 | snippet = className ++ argList; 219 | 220 | }); 221 | 222 | case 223 | {snippetFormat == "ultisnips"} { 224 | snippet = "snippet %\n%\nendsnippet\n".format(snippetName, snippet); 225 | } 226 | {snippetFormat == "snippets.nvim"} { 227 | snippet = "['%'] = [[%]];\n".format(snippetName, snippet); 228 | } 229 | {snippetFormat == "luasnip"} { 230 | var description = "Snippet for %, auto generated by SCNvim".format(snippetName); 231 | snippet = "s( {trig = \"%\", name = \"%\", dscr = \"%\" }, {%}),".format(snippetName, snippetName, description, snippet); 232 | }; 233 | snippets = snippets.add(snippet ++ Char.nl); 234 | }; 235 | }; 236 | }; 237 | }; 238 | 239 | case 240 | {snippetFormat == "ultisnips"} { 241 | file.write("# SuperCollider snippets" ++ Char.nl); 242 | file.write("# Snippet generator: SCNvim.sc" ++ Char.nl); 243 | file.putAll(snippets); 244 | } 245 | {snippetFormat == "luasnip"} { 246 | file.write("-- SuperCollider snippets for LuaSnip" ++ Char.nl); 247 | file.write("-- Snippet generator: SCNvim.sc" ++ Char.nl); 248 | file.write("local ls = require'luasnip'" ++ Char.nl); 249 | file.write("local s = ls.snippet " ++ Char.nl); 250 | file.write("local i = ls.insert_node" ++ Char.nl); 251 | file.write("local t = ls.text_node" ++ Char.nl); 252 | file.write("local snippets = {" ++ Char.nl); 253 | file.putAll(snippets); 254 | file.write("}" ++ Char.nl); 255 | file.write("return snippets"); 256 | } 257 | {snippetFormat == "snippets.nvim" } { 258 | file.write("-- SuperCollider snippets for Snippets.nvim" ++ Char.nl); 259 | file.write("-- Snippet generator: SCNvim.sc" ++ Char.nl); 260 | file.write("local snippets = {" ++ Char.nl); 261 | file.putAll(snippets); 262 | file.write("}" ++ Char.nl); 263 | file.write("return snippets"); 264 | }; 265 | file.close; 266 | "Generated snippets file: %".format(path).postln; 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /scide_scnvim/Classes/SCNvimDoc/SCNvimDoc.sc: -------------------------------------------------------------------------------- 1 | SCNvimDoc : SCDoc { 2 | *exportDocMapJson {|path| 3 | var f, numItems; 4 | f = File.open(path,"w"); 5 | numItems = this.documents.size - 1; 6 | f << "{\n"; 7 | this.documents.do {|doc, i| 8 | doc.toJSON(f, i >= numItems); 9 | }; 10 | f << "}\n"; 11 | f.close; 12 | } 13 | 14 | *parseFileMetaData {|dir,path| 15 | var fullPath = dir +/+ path; 16 | var subpath = path.drop(-7); 17 | var entry, x = this.prParseFile(fullPath, 2); 18 | if(x.isNil) {^nil}; 19 | entry = SCNvimDocEntry(x, subpath); 20 | entry.isExtension = (dir != this.helpSourceDir); 21 | entry.fullPath = fullPath; 22 | entry.mtime = File.mtime(fullPath); 23 | 24 | if(dir.beginsWith(Platform.userExtensionDir +/+ "quarks")) { 25 | entry.categories = entry.categories ++ ["Quarks>"++dir.dirname.basename]; 26 | }; 27 | 28 | ^entry; 29 | } 30 | 31 | *indexAllDocuments { |clearCache=false| 32 | var now = Main.elapsedTime; 33 | var key, doc; 34 | var nonHelpFiles; 35 | var undocClasses = Class.allClasses.reject(_.isMetaClass).collectAs({|c|c.name},IdentitySet); 36 | var additions = Dictionary(); 37 | this.checkVersion(clearCache); 38 | this.postMsg("Indexing help-files...",0); 39 | documents = Dictionary(); // or use IdDict and symbols as keys? 40 | helpSourceDirs = nil; // force re-scan of HelpSource folders 41 | this.helpSourceDirs.do {|dir| 42 | PathName(dir).filesDo {|f| 43 | case 44 | {f.fullPath.endsWith(".ext.schelp")} { 45 | f = f.fullPath; 46 | key = f[dir.size+1 ..].drop(-11).replace("\\","/"); 47 | additions[key] = additions[key].add(f); 48 | } 49 | {f.extension=="schelp"} { 50 | doc = this.parseFileMetaData(dir, f.fullPath.drop(dir.size+1)); 51 | doc !? { 52 | documents[doc.path] = doc; 53 | if(doc.isClassDoc) { 54 | undocClasses.remove(doc.title.asSymbol); 55 | } 56 | } 57 | } 58 | { 59 | f = f.fullPath; 60 | nonHelpFiles = nonHelpFiles.add([f,f.drop(dir.size+1)]); 61 | }; 62 | } 63 | }; 64 | this.postMsg("Handling"+additions.size+"document additions...",1); 65 | additions.pairsDo {|key, val| 66 | doc = documents[key]; 67 | if(doc.notNil) { 68 | doc.setAdditions(val); 69 | } { 70 | warn("SCDoc: Additions % for non-existent help file".format(val)); 71 | } 72 | }; 73 | this.postMsg("Indexing undocumented methods...",1); 74 | documents.do {|d| 75 | if(d.isClassDoc) { d.indexUndocumentedMethods }; 76 | }; 77 | this.postMsg("Adding entries for"+undocClasses.size+"undocumented classes...",1); 78 | undocClasses.do {|x| 79 | doc = SCNvimDocEntry.newUndocClass(x); 80 | documents[doc.path] = doc; 81 | }; 82 | this.postMsg("Copying"+nonHelpFiles.size+"non-help files...",1); 83 | nonHelpFiles.do {|x| 84 | var dest = SCDoc.helpTargetDir+/+x[1]; 85 | var folder = dest.dirname; 86 | File.mkdir(folder); 87 | if(File.exists(dest).not or: {File.mtime(x[0]) > File.mtime(dest)}) { 88 | File.delete(dest); 89 | File.copy(x[0],dest); 90 | }; 91 | }; 92 | if(Help.respondsTo('tree')) { 93 | this.postMsg("Indexing old helpfiles..."); 94 | this.indexOldHelp; 95 | }; 96 | this.postMsg("Exporting docmap.js...",1); 97 | this.exportDocMapJson(this.helpTargetDir +/+ "docmap.json"); 98 | this.postMsg("Indexed % documents in % seconds".format(documents.size,round(Main.elapsedTime-now,0.01)),0); 99 | NotificationCenter.notify(SCDoc, \didIndexAllDocs); 100 | } 101 | 102 | *findHelpFile {|str| 103 | var old, sym, pfx = SCDoc.helpTargetUrl; 104 | 105 | if(str.isNil or: {str.isEmpty}) { ^pfx ++ "/Help.txt" }; 106 | if(this.documents[str].notNil) { ^pfx ++ "/" ++ str ++ ".html.scnvim" }; 107 | 108 | sym = str.asSymbol; 109 | if(sym.asClass.notNil) { 110 | ^pfx ++ (if(this.documents["Classes/"++str].isUndocumentedClass) { 111 | (old = if(Help.respondsTo('findHelpFile'),{Help.findHelpFile(str)})) !? { 112 | "/OldHelpWrapper.html#"++old++"?"++SCDoc.helpTargetUrl ++ "/Classes/" ++ str ++ ".html.scnvim" 113 | } 114 | } ?? { "/Classes/" ++ str ++ ".html.scnvim" }); 115 | }; 116 | 117 | if(str.last == $_) { str = str.drop(-1) }; 118 | ^str; 119 | } 120 | 121 | *prepareHelpForURL {|url| 122 | var path, targetBasePath, pathIsCaseInsensitive; 123 | var subtarget, src, c, cmd, doc, destExist, destMtime; 124 | var verpath = this.helpTargetDir +/+ "version"; 125 | path = url.asLocalPath; 126 | 127 | // detect old helpfiles and wrap them in OldHelpWrapper 128 | if(url.scheme == "sc") { ^URI(SCNvimDoc.findHelpFile(path)); }; 129 | 130 | // just pass through remote url's 131 | if(url.scheme != "file") {^url}; 132 | 133 | targetBasePath = SCDoc.helpTargetDir; 134 | if (thisProcess.platform.name === \windows) 135 | { targetBasePath = targetBasePath.replace("/","\\") }; 136 | pathIsCaseInsensitive = thisProcess.platform.name === \windows; 137 | 138 | // detect old helpfiles and wrap them in OldHelpWrapper 139 | if( 140 | /* 141 | // this didn't work for quarks due to difference between registered old help path and the quarks symlink in Extensions. 142 | // we could use File.realpath(path) below but that would double the execution time, 143 | // so let's just assume any local file outside helpTargetDir is an old helpfile. 144 | block{|break| 145 | Help.do {|key, path| 146 | if(url.endsWith(path)) { 147 | break.value(true) 148 | } 149 | }; false 150 | }*/ 151 | compare( 152 | path [..(targetBasePath.size-1)], 153 | targetBasePath, 154 | pathIsCaseInsensitive 155 | ) != 0 156 | ) { 157 | ^SCDoc.getOldWrapUrl(url) 158 | }; 159 | 160 | if(destExist = File.exists(path)) 161 | { 162 | destMtime = File.mtime(path); 163 | }; 164 | 165 | if(path.endsWith(".html.scnvim")) { 166 | subtarget = path.drop(this.helpTargetDir.size+1).drop(-12).replace("\\","/"); 167 | doc = this.documents[subtarget]; 168 | doc !? { 169 | if(doc.isUndocumentedClass) { 170 | if(doc.mtime == 0) { 171 | this.renderUndocClass(doc); 172 | doc.mtime = 1; 173 | }; 174 | ^url; 175 | }; 176 | if(File.mtime(doc.fullPath)>doc.mtime) { // src changed after indexing 177 | this.postMsg("% changed, re-indexing documents".format(doc.path),2); 178 | this.indexAllDocuments; 179 | ^this.prepareHelpForURL(url); 180 | }; 181 | if(destExist.not 182 | or: {doc.mtime>destMtime} 183 | or: {doc.additions.detect {|f| File.mtime(f)>destMtime}.notNil} 184 | or: {File.mtime(this.helpTargetDir +/+ "scdoc_version")>destMtime} 185 | or: {doc.klass.notNil and: {File.mtime(doc.klass.filenameSymbol.asString)>destMtime}} 186 | ) { 187 | this.parseAndRender(doc); 188 | }; 189 | ^url; 190 | }; 191 | }; 192 | 193 | if(destExist) { 194 | ^url; 195 | }; 196 | 197 | warn("SCDoc: Broken link:" + url.asString); 198 | ^nil; 199 | } 200 | 201 | } 202 | 203 | SCNvimDocEntry : SCDocEntry { 204 | destPath { 205 | ^SCDoc.helpTargetDir +/+ path ++ ".html.scnvim"; 206 | } 207 | 208 | makeMethodList { 209 | var list; 210 | docimethods.do {|name| 211 | list = list.add("-"++name.asString); 212 | }; 213 | doccmethods.do {|name| 214 | list = list.add("*"++name.asString); 215 | }; 216 | undocimethods.do {|name| 217 | list = list.add("?-"++name.asString); 218 | }; 219 | undoccmethods.do {|name| 220 | list = list.add("?*"++name.asString); 221 | }; 222 | docmethods.do {|name| 223 | list = list.add("."++name.asString); 224 | }; 225 | ^list; 226 | } 227 | 228 | // overriden to output valid json 229 | prJSONString {|stream, key, x| 230 | if (x.isNil) { x = "" }; 231 | x = x.escapeChar(92.asAscii); // backslash 232 | x = x.escapeChar(34.asAscii); // double quote 233 | stream << "\"" << key << "\": \"" << x << "\",\n"; 234 | } 235 | 236 | // overriden to output valid json 237 | prJSONList {|stream, key, v, lastItem| 238 | var delimiter = if(lastItem.notNil and:{lastItem}, "", ","); 239 | if (v.isNil) { v = "" }; 240 | stream << "\"" << key << "\": [ " << v.collect{|x|"\""++x.escapeChar(34.asAscii)++"\""}.join(",") << " ]%\n".format(delimiter); 241 | } 242 | 243 | toJSON {|stream, lastItem| 244 | var delimiter = if(lastItem.notNil and:{lastItem}, "", ","); 245 | var inheritance = []; 246 | var numItems; 247 | 248 | stream << "\"" << path.escapeChar(34.asAscii) << "\": {\n"; 249 | 250 | this.prJSONString(stream, "title", title); 251 | this.prJSONString(stream, "path", path); 252 | this.prJSONString(stream, "summary", summary); 253 | this.prJSONString(stream, "installed", if(isExtension,"extension","standard")); //FIXME: also 'missing'.. better to have separate extension and missing booleans.. 254 | this.prJSONString(stream, "categories", if(categories.notNil) {categories.join(", ")} {""}); // FIXME: export list instead 255 | this.prJSONList(stream, "keywords", keywords); 256 | this.prJSONList(stream, "related", related); 257 | 258 | this.prJSONList(stream, "methods", this.makeMethodList, klass.isNil); 259 | 260 | if (oldHelp.notNil) { 261 | this.prJSONString(stream, "oldhelp", oldHelp); 262 | }; 263 | 264 | if (klass.notNil) { 265 | var keys = #[ "superclasses", "subclasses", "implementor" ]; 266 | klass.superclasses !? { 267 | inheritance = inheritance.add(klass.superclasses.collect {|c| 268 | c.name.asString 269 | }); 270 | }; 271 | klass.subclasses !? { 272 | inheritance = inheritance.add(klass.subclasses.collect {|c| 273 | c.name.asString 274 | }); 275 | }; 276 | implKlass !? { 277 | inheritance = inheritance.add(implKlass.name.asString); 278 | }; 279 | 280 | numItems = inheritance.size - 1; 281 | inheritance.do {|item, i| 282 | this.prJSONList(stream, keys[i], item, i >= numItems); 283 | }; 284 | }; 285 | 286 | stream << "}%\n".format(delimiter); 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /scide_scnvim/Classes/SCNvimDoc/SCNvimDocRenderer.sc: -------------------------------------------------------------------------------- 1 | SCNvimDocRenderer : SCDocHTMLRenderer { 2 | *renderTOC { 3 | ^nil; 4 | } 5 | 6 | *renderHeader {|stream, doc, body| 7 | var x, cats, m, z; 8 | var thisIsTheMainHelpFile; 9 | var folder = doc.path.dirname; 10 | var undocumented = false; 11 | var displayedTitle; 12 | if(folder==".",{folder=""}); 13 | 14 | // FIXME: use SCDoc.helpTargetDir relative to baseDir 15 | baseDir = "."; 16 | doc.path.occurrencesOf($/).do { 17 | baseDir = baseDir ++ "/.."; 18 | }; 19 | 20 | thisIsTheMainHelpFile = (doc.title == "Help") and: { 21 | (folder == "") or: 22 | { (thisProcess.platform.name === \windows) and: { folder == "Help" } } 23 | }; 24 | 25 | stream 26 | << "" 27 | << "" 28 | << ""; 29 | 30 | stream << ""; 31 | stream << "
"; 32 | stream << "*%* ".format(doc.title); 33 | if(thisIsTheMainHelpFile) { 34 | stream << "SuperCollider " << Main.version << " Help"; 35 | } { 36 | stream << "| SuperCollider " << Main.version << " | Help"; 37 | }; 38 | stream << "
"; 39 | 40 | doc.related !? { 41 | stream << "
See also: " 42 | << (doc.related.collect {|r| this.htmlForLink(r)}.join(" ")) 43 | << "
"; 44 | }; 45 | } 46 | 47 | *renderFooter {|stream, doc| 48 | doc.fullPath !? { 49 | stream << "
helpfile source: " << doc.fullPath << "
"; 50 | }; 51 | stream << "
vim:tw=78:et:ft=help.supercollider:norl:<\div>"; 52 | stream << ""; 53 | } 54 | 55 | *renderMethod {|stream, node, methodType, cls, icls| 56 | var methodTypeIndicator; 57 | var methodCodePrefix; 58 | var args = node.text ?? ""; // only outside class/instance methods 59 | var names = node.children[0].children.collect(_.text); 60 | var mstat, sym, m, m2, mname2; 61 | var lastargs, args2; 62 | var x, maxargs = -1; 63 | var methArgsMismatch = false; 64 | 65 | methodTypeIndicator = switch( 66 | methodType, 67 | \classMethod, { "*" }, 68 | \instanceMethod, { "-" }, 69 | \genericMethod, { "" } 70 | ); 71 | 72 | minArgs = inf; 73 | currentMethod = nil; 74 | names.do {|mname| 75 | methodCodePrefix = switch( 76 | methodType, 77 | \classMethod, { if(cls.notNil) { cls.name.asString[5..] } { "" } ++ "." }, 78 | \instanceMethod, { 79 | // If the method name contains any valid binary operator character, remove the 80 | // "." to reduce confusion. 81 | if(mname.asString.any(this.binaryOperatorCharacters.contains(_)), { "" }, { "." }) 82 | }, 83 | \genericMethod, { "" } 84 | ); 85 | 86 | mname2 = this.escapeSpecialChars(mname); 87 | if(cls.notNil) { 88 | mstat = 0; 89 | sym = mname.asSymbol; 90 | //check for normal method or getter 91 | m = icls !? {icls.findRespondingMethodFor(sym.asGetter)}; 92 | m = m ?? {cls.findRespondingMethodFor(sym.asGetter)}; 93 | m !? { 94 | mstat = mstat | 1; 95 | args = this.makeArgString(m); 96 | args2 = m.argNames !? {m.argNames[1..]}; 97 | }; 98 | //check for setter 99 | m2 = icls !? {icls.findRespondingMethodFor(sym.asSetter)}; 100 | m2 = m2 ?? {cls.findRespondingMethodFor(sym.asSetter)}; 101 | m2 !? { 102 | mstat = mstat | 2; 103 | args = m2.argNames !? {this.makeArgString(m2,false)} ?? {"value"}; 104 | args2 = m2.argNames !? {m2.argNames[1..]}; 105 | }; 106 | maxargs.do {|i| 107 | var a = args2 !? args2[i]; 108 | var b = lastargs[i]; 109 | if(a!=b and: {a!=nil} and: {b!=nil}) { 110 | methArgsMismatch = true; 111 | } 112 | }; 113 | lastargs = args2; 114 | case 115 | {args2.size>maxargs} { 116 | maxargs = args2.size; 117 | currentMethod = m2 ?? m; 118 | } 119 | {args2.size" 130 | << "" << methodCodePrefix << "" 131 | << "" << mname2 << "" 134 | }; 135 | 136 | switch (mstat, 137 | // getter only 138 | 1, { x.value; stream << args; }, 139 | // getter and setter 140 | 3, { x.value; }, 141 | // method not found 142 | 0, { 143 | "SCDoc: In %\n" 144 | " Method %% not found.".format(currDoc.fullPath, methodTypeIndicator, mname2).warn; 145 | x.value; 146 | stream << ": METHOD NOT FOUND!"; 147 | } 148 | ); 149 | 150 | stream << " ~\n"; 151 | 152 | // has setter 153 | if(mstat & 2 > 0) { 154 | x.value; 155 | if(args2.size<2) { 156 | stream << " = " << args << "\n"; 157 | } { 158 | stream << "_(" << args << ")\n"; 159 | } 160 | }; 161 | 162 | m = m ?? m2; 163 | m !? { 164 | if(m.isExtensionOf(cls) and: {icls.isNil or: {m.isExtensionOf(icls)}}) { 165 | stream << "\n"; 168 | } { 169 | if(m.ownerClass == icls) { 170 | stream << "
From implementing class
\n"; 171 | } { 172 | if(m.ownerClass != cls) { 173 | m = m.ownerClass.name; 174 | m = if(m.isMetaClassName) {m.asString.drop(5)} {m}; 175 | stream << "
From superclass: " << m << "
\n"; 177 | } 178 | } 179 | }; 180 | }; 181 | }; 182 | 183 | if(methArgsMismatch) { 184 | "SCDoc: In %\n" 185 | " Grouped methods % do not have the same argument signature." 186 | .format(currDoc.fullPath, names).warn; 187 | }; 188 | 189 | // ignore trailing mul add arguments 190 | if(currentMethod.notNil) { 191 | currentNArgs = currentMethod.argNames.size; 192 | if(currentNArgs > 2 193 | and: {currentMethod.argNames[currentNArgs-1] == \add} 194 | and: {currentMethod.argNames[currentNArgs-2] == \mul}) { 195 | currentNArgs = currentNArgs - 2; 196 | } 197 | } { 198 | currentNArgs = 0; 199 | }; 200 | 201 | if(node.children.size > 1) { 202 | stream << "
"; 203 | this.renderChildren(stream, node.children[1]); 204 | stream << "
"; 205 | }; 206 | currentMethod = nil; 207 | } 208 | 209 | *renderSubTree {|stream, node| 210 | var f, z, img; 211 | switch(node.id, 212 | \PROSE, { 213 | if(noParBreak) { 214 | noParBreak = false; 215 | } { 216 | stream << "\n

"; 217 | }; 218 | this.renderChildren(stream, node); 219 | }, 220 | \NL, { }, // these shouldn't be here.. 221 | // Plain text and modal tags 222 | \TEXT, { 223 | stream << this.escapeSpecialChars(node.text); 224 | }, 225 | \LINK, { 226 | stream << this.htmlForLink(node.text); 227 | }, 228 | \CODEBLOCK, { 229 | stream << "

"
230 |                 << ">\n"
231 | 				<< this.escapeSpecialChars(node.text)
232 |                 << "
"; 233 | }, 234 | \CODE, { 235 | stream << "" 236 | << this.escapeSpecialChars(node.text) 237 | << ""; 238 | }, 239 | \EMPHASIS, { 240 | stream << "" << this.escapeSpecialChars(node.text) << ""; 241 | }, 242 | \TELETYPEBLOCK, { 243 | stream << "
" << this.escapeSpecialChars(node.text) << "
"; 244 | }, 245 | \TELETYPE, { 246 | stream << "" << this.escapeSpecialChars(node.text) << ""; 247 | }, 248 | \STRONG, { 249 | stream << "" << this.escapeSpecialChars(node.text) << ""; 250 | }, 251 | \SOFT, { 252 | stream << "" << this.escapeSpecialChars(node.text) << ""; 253 | }, 254 | \ANCHOR, { 255 | stream << " "; 256 | }, 257 | \KEYWORD, { 258 | node.children.do {|child| 259 | stream << " "; 260 | } 261 | }, 262 | \IMAGE, { 263 | f = node.text.split($#); 264 | stream << "
"; 265 | img = ""; 266 | if(f[2].isNil) { 267 | stream << img; 268 | } { 269 | stream << this.htmlForLink(f[2]++"#"++(f[3]?"")++"#"++img,false); 270 | }; 271 | f[1] !? { stream << "
" << f[1] << "" }; // ugly.. 272 | stream << "
\n"; 273 | }, 274 | // Other stuff 275 | \NOTE, { 276 | stream << "
NOTE: "; 277 | noParBreak = true; 278 | this.renderChildren(stream, node); 279 | stream << "
"; 280 | }, 281 | \WARNING, { 282 | stream << "
WARNING: "; 283 | noParBreak = true; 284 | this.renderChildren(stream, node); 285 | stream << "
"; 286 | }, 287 | \FOOTNOTE, { 288 | footNotes = footNotes.add(node); 289 | stream << "" 294 | << footNotes.size 295 | << " "; 296 | }, 297 | \CLASSTREE, { 298 | stream << "
    "; 299 | this.renderClassTree(stream, node.text.asSymbol.asClass); 300 | stream << "
"; 301 | }, 302 | // Lists and tree 303 | \LIST, { 304 | stream << "
    \n"; 305 | this.renderChildren(stream, node); 306 | stream << "
\n"; 307 | }, 308 | \TREE, { 309 | stream << "
    \n"; 310 | this.renderChildren(stream, node); 311 | stream << "
\n"; 312 | }, 313 | \NUMBEREDLIST, { 314 | stream << "
    \n"; 315 | this.renderChildren(stream, node); 316 | stream << "
\n"; 317 | }, 318 | \ITEM, { // for LIST, TREE and NUMBEREDLIST 319 | stream << "
  • "; 320 | noParBreak = true; 321 | this.renderChildren(stream, node); 322 | }, 323 | // Definitionlist 324 | \DEFINITIONLIST, { 325 | stream << "
    \n"; 326 | this.renderChildren(stream, node); 327 | stream << "
    \n"; 328 | }, 329 | \DEFLISTITEM, { 330 | this.renderChildren(stream, node); 331 | }, 332 | \TERM, { 333 | stream << "
    "; 334 | noParBreak = true; 335 | this.renderChildren(stream, node); 336 | }, 337 | \DEFINITION, { 338 | stream << "
    "; 339 | noParBreak = true; 340 | this.renderChildren(stream, node); 341 | }, 342 | // Tables 343 | \TABLE, { 344 | stream << "\n"; 345 | this.renderChildren(stream, node); 346 | stream << "
    \n"; 347 | }, 348 | \TABROW, { 349 | stream << ""; 350 | this.renderChildren(stream, node); 351 | }, 352 | \TABCOL, { 353 | stream << ""; 354 | noParBreak = true; 355 | this.renderChildren(stream, node); 356 | }, 357 | // Methods 358 | \CMETHOD, { 359 | this.renderMethod( 360 | stream, node, 361 | \classMethod, 362 | currentClass !? {currentClass.class}, 363 | currentImplClass !? {currentImplClass.class} 364 | ); 365 | }, 366 | \IMETHOD, { 367 | this.renderMethod( 368 | stream, node, 369 | \instanceMethod, 370 | currentClass, 371 | currentImplClass 372 | ); 373 | }, 374 | \METHOD, { 375 | this.renderMethod( 376 | stream, node, 377 | \genericMethod, 378 | nil, nil 379 | ); 380 | }, 381 | \CPRIVATE, {}, 382 | \IPRIVATE, {}, 383 | \COPYMETHOD, {}, 384 | \CCOPYMETHOD, {}, 385 | \ICOPYMETHOD, {}, 386 | \ARGUMENTS, { 387 | stream << "

    Arguments:

    \n\n"; 388 | currArg = 0; 389 | if(currentMethod.notNil and: {node.children.size < (currentNArgs-1)}) { 390 | "SCDoc: In %\n" 391 | " Method %% has % args, but doc has % argument:: tags.".format( 392 | currDoc.fullPath, 393 | if(currentMethod.ownerClass.isMetaClass) {"*"} {"-"}, 394 | currentMethod.name, 395 | currentNArgs-1, 396 | node.children.size, 397 | ).warn; 398 | }; 399 | this.renderChildren(stream, node); 400 | stream << "
    "; 401 | }, 402 | \ARGUMENT, { 403 | currArg = currArg + 1; 404 | stream << ""; 405 | if(node.text.isNil) { 406 | currentMethod !? { 407 | if(currentMethod.varArgs and: {currArg==(currentMethod.argNames.size-1)}) { 408 | stream << "... "; 409 | }; 410 | stream << if(currArg < currentMethod.argNames.size) { 411 | if(currArg > minArgs) { 412 | "("++currentMethod.argNames[currArg]++")"; 413 | } { 414 | currentMethod.argNames[currArg]; 415 | } 416 | } { 417 | "(arg"++currArg++")" // excessive arg 418 | }; 419 | }; 420 | } { 421 | stream << if(currentMethod.isNil or: {currArg < currentMethod.argNames.size}) { 422 | currentMethod !? { 423 | f = currentMethod.argNames[currArg].asString; 424 | if( 425 | (z = if(currentMethod.varArgs and: {currArg==(currentMethod.argNames.size-1)}) 426 | {"... "++f} {f} 427 | ) != node.text; 428 | ) { 429 | "SCDoc: In %\n" 430 | " Method %% has arg named '%', but doc has 'argument:: %'.".format( 431 | currDoc.fullPath, 432 | if(currentMethod.ownerClass.isMetaClass) {"*"} {"-"}, 433 | currentMethod.name, 434 | z, 435 | node.text, 436 | ).warn; 437 | }; 438 | }; 439 | if(currArg > minArgs) { 440 | "("++node.text++")"; 441 | } { 442 | node.text; 443 | }; 444 | } { 445 | "("++node.text++")" // excessive arg 446 | }; 447 | }; 448 | stream << ""; 449 | this.renderChildren(stream, node); 450 | }, 451 | \RETURNS, { 452 | stream << "

    Returns:

    \n
    "; 453 | this.renderChildren(stream, node); 454 | stream << "
    "; 455 | 456 | }, 457 | \DISCUSSION, { 458 | stream << "

    Discussion:

    \n"; 459 | this.renderChildren(stream, node); 460 | }, 461 | // Sections 462 | \CLASSMETHODS, { 463 | if(node.notPrivOnly) { 464 | stream << "

    Class Methods ~

    \n"; 465 | }; 466 | this.renderChildren(stream, node); 467 | }, 468 | \INSTANCEMETHODS, { 469 | if(node.notPrivOnly) { 470 | stream << "

    Instance Methods ~

    \n"; 471 | }; 472 | this.renderChildren(stream, node); 473 | }, 474 | \DESCRIPTION, { 475 | stream << "

    Description ~

    \n"; 476 | this.renderChildren(stream, node); 477 | }, 478 | \EXAMPLES, { 479 | stream << "

    Examples ~

    \n"; 480 | this.renderChildren(stream, node); 481 | }, 482 | \SECTION, { 483 | stream << "

    " << this.escapeSpecialChars(node.text) << "

    \n"; 485 | if(node.makeDiv.isNil) { 486 | this.renderChildren(stream, node); 487 | } { 488 | stream << "
    "; 489 | this.renderChildren(stream, node); 490 | stream << "
    "; 491 | }; 492 | }, 493 | \SUBSECTION, { 494 | stream << "

    " << this.escapeSpecialChars(node.text) << "

    \n"; 496 | if(node.makeDiv.isNil) { 497 | this.renderChildren(stream, node); 498 | } { 499 | stream << "
    "; 500 | this.renderChildren(stream, node); 501 | stream << "
    "; 502 | }; 503 | }, 504 | { 505 | "SCDoc: In %\n" 506 | " Unknown SCDocNode id: %".format(currDoc.fullPath, node.id).warn; 507 | this.renderChildren(stream, node); 508 | } 509 | ); 510 | } 511 | } 512 | -------------------------------------------------------------------------------- /scide_scnvim/Classes/SCNvimDoc/extSCNvim.sc: -------------------------------------------------------------------------------- 1 | + SCNvim { 2 | // copy of `Help.methodArgs` with an additional bugfix 3 | // remove this later if merged upstream 4 | *getMethodArgs {|string| 5 | var class, meth, f, m; 6 | f = string.findRegexp("(\\w*)\\.(\\w+)").flop[1]; 7 | if(f.notNil) {#class, meth = f[1..]} { 8 | if(string[0].isUpper) { 9 | class = string; 10 | meth = \new; 11 | } { 12 | meth = string; 13 | } 14 | }; 15 | f = {|m,c| 16 | class = (c ?? {m.ownerClass}).name; 17 | class = if(class.isMetaClassName) {class.asString[5..]++" *"} {class.asString++" -"}; 18 | if (m.argNames.notNil) { // argNames can be nil in rare cases such as `Done.freeSelf` 19 | class++m.name++" ("++m.argNames[1..].collect {|n,i| 20 | n.asString++":"+m.prototypeFrame[i+1]; 21 | }.join(", ")++")"; 22 | } { "" } 23 | }; 24 | class = class.asSymbol.asClass; 25 | if(class.notNil) { 26 | m = class.class.findRespondingMethodFor(meth.asSymbol); 27 | ^if(m.notNil) {f.value(m,class.class)} {""}; 28 | } { 29 | ^Class.allClasses.reject{|c|c.isMetaClass}.collect {|c| 30 | c.findMethod(meth.asSymbol); 31 | }.reject{|m|m.isNil}.collect {|m| 32 | f.value(m); 33 | }.join($\n); 34 | } 35 | } 36 | 37 | // This function will be replaced by LSP in the future 38 | *methodArgs {|method| 39 | var args = this.getMethodArgs(method); 40 | args = args.split(Char.nl); 41 | if (args.size == 1) { 42 | ^args[0] 43 | } 44 | ^""; 45 | } 46 | 47 | *prepareHelpFor {|text| 48 | var urlString, url, brokenAction; 49 | var result, tmp; 50 | 51 | tmp = SCDoc.renderer.asClass; 52 | SCDoc.renderer = SCNvimDocRenderer; 53 | 54 | urlString = SCNvimDoc.findHelpFile(text); 55 | url = URI(urlString); 56 | brokenAction = { 57 | "Sorry no help for %".format(text).postln; 58 | ^nil; 59 | }; 60 | 61 | result = SCNvimDoc.prepareHelpForURL(url) ?? brokenAction; 62 | SCDoc.renderer = tmp; 63 | ^result; 64 | } 65 | 66 | *getHelpUri {arg subject; 67 | var uri = SCNvim.prepareHelpFor(subject); 68 | if (uri.notNil) { 69 | ^uri.asLocalPath; 70 | }; 71 | ^nil; 72 | } 73 | 74 | *getFileNameFromUri {arg uri; 75 | ^PathName(uri).fileNameWithoutExtension; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /scide_scnvim/Classes/SCNvimJSON.sc: -------------------------------------------------------------------------------- 1 | /* 2 | * Helper class to convert between Dictionary types and JSON. 3 | * Note: Array types are not implemented yet. 4 | */ 5 | SCNvimJSON { 6 | classvar . 15 | " 16 | " Vim syntax file 17 | " Language: scdoc (SuperCollider help file markup) 18 | 19 | " For version 5.x: Clear all syntax items 20 | " For version 6.x: Quit when a syntax file was already loaded 21 | if v:version < 600 22 | syntax clear 23 | elseif exists('b:current_syntax') 24 | finish 25 | endif 26 | 27 | setlocal iskeyword=a-z,A-Z,48-57,_ 28 | 29 | syn case ignore 30 | 31 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 32 | " header tags 33 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 34 | 35 | " TODO: highlighting of remainder of lines? 36 | syn match scdocTitle /\/ end=/\n/ 28 | 29 | " Using Log.quark 30 | syn match logger /^\[\w*\]/ 31 | 32 | """"""""""""""""""" 33 | " Error and warning messages 34 | """"""""""""""""""" 35 | syn match errorCursor "\^\^" 36 | syn match errors /ERROR:.*$/ 37 | syn match receiverError /RECEIVER:.*$/ 38 | syn match fails /FAIL:.*$/ 39 | syn match warns /WARNING:.*$/ 40 | syn match exceptions /EXCEPTION:.*$/ 41 | 42 | syn match errorblock /^ERROR:.*$/ 43 | syn match receiverBlock /^RECEIVER:.*$/ 44 | syn match protectedcallstack /^PROTECTED CALL STACK:.*$/ 45 | syn match callstack /^CALL STACK:.*$/ 46 | 47 | " unittests 48 | syn match unittestPass /^PASS:.*$/ 49 | syn match unittestRunning /^RUNNING UNIT TEST.*$/ 50 | 51 | """"""""""""""""""" 52 | " Linking 53 | """"""""""""""""""" 54 | 55 | " Special scnvim links 56 | hi def link errors ErrorMsg 57 | hi def link errorBlock ErrorMsg 58 | hi def link receiverError ErrorMsg 59 | hi def link exceptions ErrorMsg 60 | hi def link errorCursor Bold 61 | hi def link fails ErrorMsg 62 | hi def link syntaxErrorContent Underlined 63 | hi def link warns WarningMsg 64 | 65 | hi def link receiverBlock WarningMsg 66 | hi def link callstack WarningMsg 67 | hi def link protectedcallstack WarningMsg 68 | 69 | hi def link logger Bold 70 | hi def link unittestPass String 71 | 72 | hi def link result String 73 | -------------------------------------------------------------------------------- /syntax/supercollider.vim: -------------------------------------------------------------------------------- 1 | " Copyright 2007 Alex Norman 2 | " This file is part of SCVIM. 3 | " 4 | " SCVIM is free software: you can redistribute it and/or modify 5 | " it under the terms of the GNU General Public License as published by 6 | " the Free Software Foundation, either version 3 of the License, or 7 | " (at your option) any later version. 8 | " 9 | " SCVIM is distributed in the hope that it will be useful, 10 | " but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | " GNU General Public License for more details. 13 | " 14 | " You should have received a copy of the GNU General Public License 15 | " along with SCVIM. If not, see . 16 | " 17 | " Vim syntax file 18 | " Language: supercollider 19 | " Maintainer: Stephen Lumenta 20 | " Version: 0.2 21 | " Last change: 2012-03-31 22 | " 23 | " Maintainer: David Granström 24 | " Version: 0.3 25 | " Modified: 2018-01-06 26 | 27 | scriptencoding utf-8 28 | 29 | if exists('b:current_syntax') 30 | finish 31 | endif 32 | let b:current_syntax = 'supercollider' 33 | 34 | syn clear 35 | 36 | " source generated syntax file 37 | let classes = luaeval('require("scnvim.path").get_asset "syntax"') 38 | if filereadable(classes) 39 | execute "source " . classes 40 | endif 41 | 42 | syn match scAoperator "{" 43 | syn match scAoperator "}" 44 | 45 | "syn match scVariable "\%(var.*\)\@<=\(\l\w*\)" "lowercase followed by wordchar 46 | syn match scGlobvariable "\~\l\w*" "~ followed by lowercase followed by wordchar 47 | syn match scVar "\s*var\s" 48 | syn match scVar "\s*classvar\s" 49 | syn match scVar "\s*const\s" 50 | syn match scArg "\s*arg\s" 51 | 52 | " symbols, strings, numbers 53 | syn match scSymbol "\v(\w|\\)@" display 63 | syn match scInteger "\%(\%(\w\|[]})\"']\s*\)\@" display 64 | syn match scInteger "\%(\%(\w\|[]})\"']\s*\)\@" display 65 | syn match scInteger "\%(\%(\w\|[]})\"']\s*\)\@" display 66 | syn match scFloat "\%(\%(\w\|[]})\"']\s*\)\@" display 67 | syn match scFloat "\%(\%(\w\|[]})\"']\s*\)\@" display 68 | syn match scInfinity "\W\@<=inf\W\@=" 69 | 70 | " keywords 71 | syn match scControl "\<\%(break\|rescue\|return\)\>[?!]\@!" 72 | syn match scKeyword "\<\%(super\|this\|new\|yield\)\>[?!]\@!" 73 | syn match scBoolean "\<\%(true\|false\)\>[?!]\@!" 74 | syn match scControl "\<\%(case\|begin\|do\|forBy\|loop\|if\|while\|else\)\>[?!]\@!" 75 | 76 | " scsynth 77 | syn match scArate "\v\.@<=ar(\w)@!" 78 | syn match scKrate "\v\.@<=kr(\w)@!" 79 | 80 | " operators 81 | syn keyword scUnaryoperator neg reciprocal abs floor ceil frac sign squared cubed sqrt exp midicps cpsmidi midiratio ratiomidi dbamp ampdb octcps cpsoct log log2 log10 sin cos tan asin acos atan sinh cosh tanh distort softclip isPositive isNegative isStrictlyPositive 82 | syn keyword scBinaryoperator min max round trunc atan2 hypot hypotApx ring1 ring2 ring3 ring4 sumsqr difsqr sqrsum sqrdif absdif thresh amclip scaleneg clip2 wrap2 fold2 excess + - * _ 83 | 84 | syn match scBinaryoperator "+" 85 | syn match scBinaryoperator "-" 86 | syn match scBinaryoperator "*" 87 | syn match scBinaryoperator "/" 88 | syn match scBinaryoperator "%" 89 | syn match scBinaryoperator "\*\*" 90 | syn match scBinaryoperator "<" 91 | syn match scBinaryoperator "<=" 92 | syn match scBinaryoperator ">" 93 | syn match scBinaryoperator "<>" 94 | syn match scBinaryoperator ">=" 95 | syn match scBinaryoperator "=" 96 | syn match scBinaryoperator "==" 97 | syn match scBinaryoperator "===" 98 | syn match scBinaryoperator "!=" 99 | syn match scBinaryoperator "!==" 100 | syn match scBinaryoperator "&" 101 | syn match scBinaryoperator "|" 102 | syn match scBinaryoperator " this is some output' 37 | postwin.open() 38 | postwin.post(expected) 39 | local lines = vim.api.nvim_buf_get_lines(postwin.buf, -2, -1, true) 40 | assert.equals(1, #lines) 41 | assert.are.equal(lines[1], expected) 42 | end) 43 | 44 | it('can focus the window', function() 45 | local win_id = vim.api.nvim_tabpage_get_win(0) 46 | postwin.focus() 47 | local postwin_id = vim.api.nvim_tabpage_get_win(0) 48 | assert.are_not.equal(win_id, postwin_id) 49 | postwin.focus() 50 | win_id = vim.api.nvim_tabpage_get_win(0) 51 | assert.are.equal(win_id, postwin_id) 52 | end) 53 | 54 | it('can use configuration for the floating window', function() 55 | config.postwin.float.enabled = true 56 | config.postwin.float.row = 3 57 | config.postwin.float.col = function() 58 | return 3 59 | end 60 | config.postwin.float.width = 12 61 | config.postwin.float.height = 12 62 | config.postwin.float.config = { 63 | anchor = 'NW', 64 | } 65 | local id = postwin.open() 66 | local cfg = vim.api.nvim_win_get_config(id) 67 | assert.are.equal(3, cfg.row) 68 | assert.are.equal(3, cfg.col) 69 | assert.are.equal(12, cfg.width) 70 | assert.are.equal(12, cfg.height) 71 | assert.are.equal('NW', cfg.anchor) 72 | postwin.close() 73 | config.postwin.float.enabled = false 74 | end) 75 | 76 | it('truncates width and height to integer values', function() 77 | local id, width, height 78 | config.postwin.size = 20.5 79 | id = postwin.open() 80 | width = vim.api.nvim_win_get_width(id) 81 | assert.are.equal(20, width) 82 | postwin.destroy() 83 | config.postwin.float.enabled = true 84 | config.postwin.float.width = 20.5 85 | config.postwin.float.height = 20.5 86 | id = postwin.open() 87 | width = vim.api.nvim_win_get_width(id) 88 | height = vim.api.nvim_win_get_height(id) 89 | assert.are.equal(20, width) 90 | assert.are.equal(20, height) 91 | end) 92 | 93 | it('limits scrollback', function() 94 | postwin.open() 95 | local tmp = config.postwin.scrollback 96 | for i = 1, 15 do 97 | local line = string.format('line %d', i) 98 | postwin.post(line) 99 | end 100 | local count = vim.api.nvim_buf_line_count(postwin.buf) 101 | assert.are.equal(16, count) -- + one empty line 102 | postwin.destroy() 103 | postwin.open() 104 | config.postwin.scrollback = 10 105 | for i = 1, 15 do 106 | local line = string.format('line %d', i) 107 | postwin.post(line) 108 | end 109 | count = vim.api.nvim_buf_line_count(postwin.buf) 110 | assert.are.equal(10, count) 111 | config.postwin.scrollback = tmp 112 | end) 113 | 114 | it('does not leave empty buffers when toggled', function() 115 | local num_bufs 116 | postwin.open() 117 | num_bufs = #vim.api.nvim_list_bufs() 118 | postwin.close() 119 | assert.are.equal(num_bufs, #vim.api.nvim_list_bufs()) 120 | postwin.open() 121 | postwin.close() 122 | assert.are.equal(num_bufs, #vim.api.nvim_list_bufs()) 123 | end) 124 | 125 | describe('actions', function() 126 | before_each(function() 127 | config.postwin.float.enabled = false 128 | postwin.destroy() 129 | end) 130 | 131 | local x = 0 132 | postwin.on_open:append(function() 133 | x = x + 1 134 | end) 135 | 136 | it('applies on_open() action', function() 137 | assert.are.equal(0, x) 138 | postwin.open() 139 | assert.are.equal(1, x) 140 | end) 141 | end) 142 | end) 143 | -------------------------------------------------------------------------------- /test/spec/fixtures/file.lua: -------------------------------------------------------------------------------- 1 | return { 2 | a = 1, 3 | b = 2, 4 | c = 3, 5 | } 6 | -------------------------------------------------------------------------------- /test/spec/fixtures/lua/scnvim/_extensions/unit-test.lua: -------------------------------------------------------------------------------- 1 | -- luacheck: globals assert 2 | 3 | local x = nil 4 | 5 | return require('scnvim').register_extension { 6 | setup = function(ext, user) 7 | assert.is_true(type(ext) == 'table') 8 | assert.is_true(type(user) == 'table') 9 | x = ext.some_var 10 | end, 11 | exports = { 12 | test_args = function(a, b, c) 13 | assert.are.equal(777, tonumber(a)) 14 | assert.are.equal('foo', b) 15 | assert.are.equal(666, tonumber(c)) 16 | end, 17 | test_setup = function() 18 | assert.are.equal(123, x) 19 | end, 20 | }, 21 | health = function() end, 22 | } 23 | -------------------------------------------------------------------------------- /test/spec/integration/sclang_spec.lua: -------------------------------------------------------------------------------- 1 | local sclang = require 'scnvim.sclang' 2 | local config = require 'scnvim.config' 3 | local timeout = 5000 4 | local sclang_path = vim.loop.os_getenv 'SCNVIM_SCLANG_PATH' 5 | local stdout 6 | 7 | config.sclang.cmd = sclang_path 8 | 9 | sclang.on_init:append(function() 10 | stdout = { '' } 11 | end) 12 | 13 | sclang.on_output:append(function(line) 14 | table.insert(stdout, line) 15 | end) 16 | 17 | sclang.on_exit:append(function() 18 | stdout = nil 19 | end) 20 | 21 | describe('sclang', function() 22 | it('can start the interpreter', function() 23 | sclang.start() 24 | local result = false 25 | vim.wait(timeout, function() 26 | if type(stdout) == 'table' then 27 | for _, line in ipairs(stdout) do 28 | if line:match '^*** Welcome to SuperCollider' then 29 | result = true 30 | return result 31 | end 32 | end 33 | end 34 | end) 35 | assert.is_true(result) 36 | assert.is_true(sclang.is_running()) 37 | end) 38 | 39 | it('can recompile the interpreter', function() 40 | sclang.recompile() 41 | local result = 0 42 | vim.wait(timeout, function() 43 | for _, line in ipairs(stdout) do 44 | -- this line should now appear twice in the output 45 | if line:match '^*** Welcome to SuperCollider' then 46 | result = result + 1 47 | end 48 | if result == 2 then 49 | return true 50 | end 51 | end 52 | end) 53 | assert.are.equal(2, result) 54 | end) 55 | 56 | it('can evaluate an expression', function() 57 | local result 58 | sclang.send('1 + 1', false) 59 | vim.wait(timeout, function() 60 | result = stdout[#stdout] 61 | return result == '-> 2' 62 | end) 63 | assert.are.equal('-> 2', result) 64 | end) 65 | 66 | it('can pass results to a lua callback', function() 67 | local result 68 | sclang.eval('7 * 7', function(res) 69 | result = res 70 | end) 71 | vim.wait(timeout, function() 72 | return result == 49 73 | end) 74 | assert.are.equal(49, result) 75 | end) 76 | 77 | it('eval correctly escapes strings', function() 78 | local result 79 | sclang.eval('"hello"', function(res) 80 | result = res 81 | end) 82 | vim.wait(timeout, function() 83 | return result == 'hello' 84 | end) 85 | assert.are.equal('hello', result) 86 | end) 87 | 88 | it('can stop the interpreter', function() 89 | sclang.stop() 90 | vim.wait(timeout, function() 91 | return not stdout 92 | end) 93 | assert.is_nil(stdout) 94 | assert.is_false(sclang.is_running()) 95 | end) 96 | end) 97 | -------------------------------------------------------------------------------- /test/test_init.vim: -------------------------------------------------------------------------------- 1 | set rtp+=../ 2 | set rtp+=./spec/fixtures 3 | set packpath+=.deps 4 | packadd plenary.nvim 5 | set noswapfile 6 | --------------------------------------------------------------------------------