├── test
├── .gitignore
├── spec
│ ├── fixtures
│ │ ├── file.lua
│ │ └── lua
│ │ │ └── scnvim
│ │ │ └── _extensions
│ │ │ └── unit-test.lua
│ ├── automated
│ │ ├── install_spec.lua
│ │ ├── extensions_spec.lua
│ │ ├── action_spec.lua
│ │ ├── commands_spec.lua
│ │ ├── map_spec.lua
│ │ ├── path_spec.lua
│ │ ├── editor_spec.lua
│ │ └── postwin_spec.lua
│ └── integration
│ │ └── sclang_spec.lua
├── test_init.vim
└── Makefile
├── .luacheckrc
├── .gitignore
├── .ldoc
├── ftdetect
├── supercollider.vim
└── filetype.lua
├── .stylua.toml
├── autoload
├── scnvim.vim
└── scnvim
│ ├── statusline.vim
│ └── editor.vim
├── Makefile
├── lua
├── scnvim
│ ├── statusline.lua
│ ├── settings.lua
│ ├── install.lua
│ ├── utils.lua
│ ├── commands.lua
│ ├── udp.lua
│ ├── map.lua
│ ├── action.lua
│ ├── extensions.lua
│ ├── health.lua
│ ├── signature.lua
│ ├── path.lua
│ ├── postwin.lua
│ ├── config.lua
│ ├── sclang.lua
│ ├── editor.lua
│ └── help.lua
└── scnvim.lua
├── .github
├── workflows
│ ├── docs.yml
│ ├── lint.yml
│ └── ci.yml
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── scide_scnvim
├── Classes
│ ├── Document.sc
│ ├── SCNvimJSON.sc
│ ├── SCNvimDoc
│ │ ├── extSCNvim.sc
│ │ ├── SCNvimDoc.sc
│ │ └── SCNvimDocRenderer.sc
│ └── SCNvim.sc
└── HelpSource
│ └── Classes
│ └── SCNvim.schelp
├── indent
└── supercollider.vim
├── syntax
├── scnvim.vim
├── supercollider.vim
└── scdoc.vim
├── README.md
└── doc
└── SCNvim.txt
/test/.gitignore:
--------------------------------------------------------------------------------
1 | .deps
2 |
--------------------------------------------------------------------------------
/.luacheckrc:
--------------------------------------------------------------------------------
1 | globals = { 'vim' }
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | private/
2 | doc/tags
3 | docs
4 |
--------------------------------------------------------------------------------
/test/spec/fixtures/file.lua:
--------------------------------------------------------------------------------
1 | return {
2 | a = 1,
3 | b = 2,
4 | c = 3,
5 | }
6 |
--------------------------------------------------------------------------------
/.ldoc:
--------------------------------------------------------------------------------
1 | project = 'scnvim'
2 | title = 'scnvim reference'
3 | file = 'lua'
4 | dir = 'docs'
5 | format = 'markdown'
6 |
--------------------------------------------------------------------------------
/test/test_init.vim:
--------------------------------------------------------------------------------
1 | set rtp+=../
2 | set rtp+=./spec/fixtures
3 | set packpath+=.deps
4 | packadd plenary.nvim
5 | set noswapfile
6 |
--------------------------------------------------------------------------------
/ftdetect/supercollider.vim:
--------------------------------------------------------------------------------
1 | if !exists('g:do_filetype_lua')
2 | autocmd BufEnter,BufWinEnter,BufNewFile,BufRead *.schelp set filetype=scdoc
3 | endif
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test/spec/automated/install_spec.lua:
--------------------------------------------------------------------------------
1 | local install = require 'scnvim.install'
2 |
3 | -- override for unit test runner
4 | require('scnvim.path').get_plugin_root_dir = function()
5 | return vim.fn.expand '%:p:h:h'
6 | end
7 |
8 | describe('install', function()
9 | it('can link scnvim classes', function()
10 | install.install()
11 | assert.not_nil(install.check())
12 | end)
13 |
14 | it('can unlink scnvim classes', function()
15 | install.uninstall()
16 | assert.is_nil(install.check())
17 | install.install() -- reset for next test
18 | end)
19 | end)
20 |
--------------------------------------------------------------------------------
/test/Makefile:
--------------------------------------------------------------------------------
1 | all: automated integration
2 |
3 | automated: deps
4 | nvim --headless --noplugin -u test_init.vim -c "PlenaryBustedDirectory spec/automated { minimal_init = './test_init.vim' }"
5 |
6 | integration: deps
7 | nvim --headless --noplugin -u test_init.vim -c "PlenaryBustedDirectory spec/integration { minimal_init = './test_init.vim' }"
8 |
9 | deps:
10 | mkdir -p .deps/pack/ci/opt
11 | [ ! -d .deps/pack/ci/opt/plenary.nvim ] && git clone --depth 1 https://github.com/nvim-lua/plenary.nvim.git .deps/pack/ci/opt/plenary.nvim ; true
12 |
13 | .PHONY: deps automated integration
14 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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/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/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 |
--------------------------------------------------------------------------------
/test/spec/automated/extensions_spec.lua:
--------------------------------------------------------------------------------
1 | local scnvim = require 'scnvim'
2 |
3 | require 'scnvim.commands'()
4 |
5 | scnvim.setup {
6 | ensure_installed = false,
7 | extensions = {
8 | ['unit-test'] = {
9 | some_var = 123,
10 | },
11 | },
12 | }
13 |
14 | describe('extensions', function()
15 | it('can be loaded', function()
16 | local ext = scnvim.load_extension 'unit-test'
17 | assert.is_true(type(ext) == 'table')
18 | ext.test_setup()
19 | end)
20 |
21 | it('can run user commands', function()
22 | assert.is_true((pcall(vim.cmd, 'SCNvimExt unit-test.test_setup')))
23 | assert.is_false((pcall(vim.cmd, 'SCNvimExt unit-test.foobar')))
24 | end)
25 |
26 | it('can run user commands with arguments', function()
27 | assert.is_true((pcall(vim.cmd, 'SCNvimExt unit-test.test_args 777 foo 666')))
28 | end)
29 | end)
30 |
--------------------------------------------------------------------------------
/test/spec/automated/action_spec.lua:
--------------------------------------------------------------------------------
1 | local action = require 'scnvim.action'
2 |
3 | describe('actions', function()
4 | it('can replace and restore default function', function()
5 | local x = 0
6 | local a = action.new(function()
7 | x = x + 1
8 | end)
9 | a()
10 | assert.are_equal(1, x)
11 | a:replace(function()
12 | x = x - 1
13 | end)
14 | a()
15 | assert.are_equal(0, x)
16 | a:restore()
17 | a()
18 | assert.are_equal(1, x)
19 | end)
20 |
21 | it('can append and remove functions', function()
22 | local x = 0
23 | local a = action.new(function()
24 | x = x + 1
25 | end)
26 | local id = a:append(function()
27 | x = x - 1
28 | end)
29 | assert.not_nil(id)
30 | a()
31 | assert.are.equal(0, x)
32 | a:remove(id)
33 | a()
34 | assert.are.equal(1, x)
35 | end)
36 | end)
37 |
--------------------------------------------------------------------------------
/test/spec/automated/commands_spec.lua:
--------------------------------------------------------------------------------
1 | local create_commands = require 'scnvim.commands'
2 |
3 | describe('commands', function()
4 | it('creates user commands for the current buffer', function()
5 | vim.cmd [[ edit commands.scd ]]
6 | create_commands()
7 | local expected = {
8 | 'SCNvimGenerateAssets',
9 | 'SCNvimHelp',
10 | 'SCNvimRecompile',
11 | 'SCNvimReboot',
12 | 'SCNvimStart',
13 | 'SCNvimStatusLine',
14 | 'SCNvimStop',
15 | 'SCNvimExt',
16 | 'SCNvimTags', -- deprecated
17 | }
18 | local cmds = vim.api.nvim_buf_get_commands(0, {})
19 | assert.are.equal(#expected, vim.tbl_count(cmds))
20 | local count = 0
21 | for _, key in ipairs(expected) do
22 | local c = cmds[key]
23 | if c ~= nil then
24 | count = count + 1
25 | end
26 | end
27 | assert.are.equal(#expected, count)
28 | vim.cmd [[ bd! ]]
29 | end)
30 | end)
31 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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
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 |
--------------------------------------------------------------------------------
/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 =~# '\(\/\/.*\)\@\/ 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 |
--------------------------------------------------------------------------------
/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 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/spec/automated/path_spec.lua:
--------------------------------------------------------------------------------
1 | local path = require 'scnvim.path'
2 | local eq = assert.are.same
3 |
4 | -- override for unit test runner
5 | path.get_plugin_root_dir = function()
6 | return vim.fn.expand '%:p:h:h'
7 | end
8 |
9 | describe('path', function()
10 | it('tests that a directory exists', function()
11 | local cur_dir = vim.fn.expand '%:p:h'
12 | local dir = path.concat(cur_dir, 'spec', 'fixtures')
13 | assert.is_true(path.exists(dir))
14 | dir = path.concat(cur_dir, 'spec', 'nop')
15 | assert.is_false(path.exists(dir))
16 | end)
17 |
18 | it('tests that a file exists', function()
19 | local cur_dir = vim.fn.expand '%:p:h'
20 | local file = path.concat(cur_dir, 'spec', 'fixtures', 'file.lua')
21 | assert.is_true(path.exists(file))
22 | file = path.concat(cur_dir, 'spec', 'fixtures', 'nop.lua')
23 | assert.is_false(path.exists(file))
24 | end)
25 |
26 | it('concatenates items into a path', function()
27 | local value = path.concat('this', 'is', 'a', 'file.txt')
28 | local expected = 'this/is/a/file.txt'
29 | eq(value, expected)
30 | end)
31 |
32 | it('returns asset paths', function()
33 | local asset = path.get_asset 'snippets'
34 | asset = string.match(asset, 'scnvim_snippets.lua')
35 | eq(asset, 'scnvim_snippets.lua')
36 | asset = path.get_asset 'syntax'
37 | asset = string.match(asset, 'classes.vim')
38 | eq(asset, 'classes.vim')
39 | asset = path.get_asset 'tags'
40 | asset = string.match(asset, 'tags')
41 | eq(asset, 'tags')
42 | assert.has_errors(function()
43 | path.get_asset 'foo'
44 | end)
45 | end)
46 |
47 | it('returns the cache directory', function()
48 | local cache_dir = path.get_cache_dir()
49 | assert.not_nil(cache_dir)
50 | cache_dir = string.match(cache_dir, 'nvim/scnvim')
51 | local expected = path.concat('nvim', 'scnvim')
52 | eq(cache_dir, expected)
53 | end)
54 |
55 | it('converts windows paths to unix style', function()
56 | local s = [[C:\Users\test\AppData\Local]]
57 | eq('C:/Users/test/AppData/Local', path.normalize(s))
58 | end)
59 |
60 | it('can create symbolic links', function()
61 | local dir = vim.fn.expand '%:p:h'
62 | local source = path.concat(dir, 'spec', 'fixtures', 'file.lua')
63 | local destination = path.get_cache_dir() .. '/linktest.lua'
64 | path.link(source, destination)
65 | assert.is_true(path.is_symlink(destination))
66 | end)
67 |
68 | it('can delete symbolic links', function()
69 | local destination = path.get_cache_dir() .. '/linktest.lua'
70 | path.unlink(destination)
71 | assert.is_false(path.exists(destination))
72 | end)
73 |
74 | -- TODO: Find another way to test this function
75 | -- it('returns plugin root dir', function()
76 | -- end)
77 | end)
78 |
--------------------------------------------------------------------------------
/lua/scnvim/action.lua:
--------------------------------------------------------------------------------
1 | --- Define actions.
2 | --- An action defines an object that can be overriden by the user or by an extension.
3 | ---
4 | --- **Actions overview**
5 | ---
6 | --- See the corresponding module for detailed documentation.
7 | ---
8 | --- `scnvim.sclang`
9 | ---
10 | --- * on_init
11 | --- * on_exit
12 | --- * on_output
13 | ---
14 | --- `scnvim.help`
15 | ---
16 | --- * on_open
17 | --- * on_select
18 | ---
19 | --- `scnvim.editor`
20 | ---
21 | --- * on_highlight
22 | --- * on_send
23 | ---
24 | --- `scnvim.postwin`
25 | ---
26 | --- * on_open
27 | ---
28 | ---@module scnvim.action
29 | local action = {}
30 |
31 | local _id = 1000
32 | local function get_next_id()
33 | local next = _id
34 | _id = _id + 1
35 | return next
36 | end
37 |
38 | --- Create a new action.
39 | ---@param fn The default function to call.
40 | ---@return The action.
41 | function action.new(fn)
42 | local self = setmetatable({}, {
43 | __index = action,
44 | __call = function(tbl, ...)
45 | tbl.default_fn(...)
46 | for _, obj in ipairs(tbl.appended) do
47 | obj.fn(...)
48 | end
49 | end,
50 | })
51 | self._default = fn
52 | self.default_fn = fn
53 | self.appended = {}
54 | return self
55 | end
56 |
57 | --- Methods
58 | ---@type action
59 |
60 | --- Replace the default function.
61 | --- If several extension replace the same function then "last one wins".
62 | --- Consider using `action:append` if your extensions doesn't need to replace the
63 | --- default behaviour.
64 | ---@param fn The replacement function. The signature and return values *must*
65 | --- match the function to be replaced.
66 | function action:replace(fn)
67 | self.default_fn = fn
68 | end
69 |
70 | --- Restore the default function.
71 | function action:restore()
72 | self.default_fn = self._default
73 | end
74 |
75 | --- Append a function.
76 | --- The appended function will run after the default function and will receive
77 | --- the same input arguments. There can be several appended functions and they
78 | --- will be executed in the order they were appended.
79 | ---@param fn The function to append.
80 | ---@return An integer ID. Use this ID if you need to remove the appended action.
81 | function action:append(fn)
82 | local id = get_next_id()
83 | self.appended[#self.appended + 1] = {
84 | id = id,
85 | fn = fn,
86 | }
87 | return id
88 | end
89 |
90 | --- Remove an appended action.
91 | ---@param id ID of the action to remove.
92 | function action:remove(id)
93 | for i, obj in ipairs(self.appended) do
94 | if obj.id == id then
95 | table.remove(self.appended, i)
96 | return
97 | end
98 | end
99 | error('Could not find action with id: ' .. id)
100 | end
101 |
102 | return action
103 |
--------------------------------------------------------------------------------
/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.lua:
--------------------------------------------------------------------------------
1 | --- Main module.
2 | ---@module scnvim
3 | ---@author David Granström
4 | ---@license GPLv3
5 |
6 | local sclang = require 'scnvim.sclang'
7 | local editor = require 'scnvim.editor'
8 | local config = require 'scnvim.config'
9 | local extensions = require 'scnvim.extensions'
10 |
11 | local scnvim = {}
12 |
13 | --- Map helper.
14 | ---
15 | --- Can be used in two ways:
16 | ---
17 | --- 1) As a table to map functions from scnvim.editor
18 | ---
19 | --- 2) As a function to set up an arbitrary mapping.
20 | ---
21 | --- When indexed, it returns a function with the signature `(modes, callback, flash)`
22 | ---
23 | --- * modes: Table of vim modes ('i', 'n', 'x' etc.). A string can be used for
24 | --- a single mode. Default mode is 'n' (normal mode).
25 | ---
26 | --- * callback: A callback that receives a table of lines as its only
27 | --- argument. The callback should always return a table. (Only used
28 | --- by functions that manipulates text).
29 | ---
30 | --- * flash: Apply the editor flash effect for the selected text (default is
31 | --- true) (Only used by functions that manipulates text).
32 | ---
33 | ---@see scnvim.editor
34 | ---@usage scnvim.map.send_line('n'),
35 | ---@usage scnvim.map.send_line({'i', 'n'}, function(data)
36 | --- local line = data[1]
37 | --- line = line:gsub('goodbye', 'hello')
38 | --- return {line}
39 | --- end)
40 | ---@usage scnvim.map(function()
41 | --- vim.cmd [[ SCNvimGenerateAssets ]]
42 | --- end, { 'n' })
43 | ---@usage scnvim.map(scnvim.recompile)
44 | local map = require 'scnvim.map'
45 | scnvim.map = map.map
46 | scnvim.map_expr = map.map_expr
47 |
48 | --- Setup function.
49 | ---
50 | --- This function is called from the user's config to initialize scnvim.
51 | ---@param user_config A user config or an empty table.
52 | function scnvim.setup(user_config)
53 | user_config = user_config or {}
54 | config.resolve(user_config)
55 | editor.setup()
56 | if config.ensure_installed then
57 | local installer = require 'scnvim.install'
58 | local ok, msg = pcall(installer.install)
59 | if not ok then
60 | error(msg)
61 | end
62 | end
63 | end
64 |
65 | --- Evaluate an expression.
66 | ---@param expr Any valid SuperCollider expression.
67 | function scnvim.send(expr)
68 | sclang.send(expr, false)
69 | end
70 |
71 | --- Evaluate an expression without feedback from the post window.
72 | ---@param expr Any valid SuperCollider expression.
73 | function scnvim.send_silent(expr)
74 | sclang.send(expr, true)
75 | end
76 |
77 | --- Evaluate an expression and get the return value in lua.
78 | ---@param expr Any valid SuperCollider expression.
79 | ---@param cb A callback that will receive the return value as its first argument.
80 | ---@usage scnvim.eval('1 + 1', function(res)
81 | --- print(res)
82 | --- end)
83 | function scnvim.eval(expr, cb)
84 | sclang.eval(expr, cb)
85 | end
86 |
87 | --- Start sclang.
88 | function scnvim.start()
89 | sclang.start()
90 | end
91 |
92 | --- Stop sclang.
93 | function scnvim.stop()
94 | sclang.stop()
95 | end
96 |
97 | --- Recompile class library.
98 | function scnvim.recompile()
99 | sclang.recompile()
100 | end
101 |
102 | --- Reboot sclang.
103 | function scnvim.reboot()
104 | sclang.reboot()
105 | end
106 |
107 | --- Determine if a sclang process is active.
108 | ---@return True if sclang is running otherwise false.
109 | function scnvim.is_running()
110 | return sclang.is_running()
111 | end
112 |
113 | --- Register an extension.
114 | ---@param ext The extension to register.
115 | ---@return The extension.
116 | function scnvim.register_extension(ext)
117 | return extensions.register(ext)
118 | end
119 |
120 | --- Load an extension.
121 | --- Should only be called after `scnvim.setup`.
122 | ---@param name The extension to load.
123 | ---@return The exported functions from the extension.
124 | ---@usage scnvim.load_extension('logger')
125 | function scnvim.load_extension(name)
126 | return extensions.load(name)
127 | end
128 |
129 | return scnvim
130 |
--------------------------------------------------------------------------------
/test/spec/automated/editor_spec.lua:
--------------------------------------------------------------------------------
1 | local editor = require 'scnvim.editor'
2 |
3 | -- NOTE: only append to this data in order to not break existing tests
4 | local content = [[
5 | x = 123;
6 | (
7 | var x = 123;
8 | x * 2;
9 | )
10 | x = 7;
11 | x = x * 2;
12 | (
13 | var x = 7;
14 | // )
15 | x * 2;
16 | )
17 | (
18 | var y = 3;
19 | y * 2;
20 | )foo
21 | (degree: 0).play;
22 | ]]
23 |
24 | local buf = vim.api.nvim_create_buf(false, true)
25 | vim.api.nvim_win_set_buf(0, buf)
26 | vim.api.nvim_buf_set_option(buf, 'filetype', 'supercollider')
27 | content = vim.split(content, '\n', { plain = true, trimempty = true })
28 | vim.api.nvim_buf_set_lines(buf, -2, -1, false, content)
29 |
30 | describe('editor', function()
31 | describe('eval', function()
32 | before_each(function()
33 | vim.api.nvim_win_set_cursor(0, { 1, 0 })
34 | end)
35 |
36 | it('can send a single line', function()
37 | editor.send_line(function(data)
38 | assert.are.equal('x = 123;', data[1])
39 | return data
40 | end)
41 | end)
42 |
43 | it('can send a code block', function()
44 | vim.api.nvim_win_set_cursor(0, { 2, 0 })
45 | editor.send_block(function(data)
46 | local block = table.concat(data, '\n')
47 | local expected = [[
48 | (
49 | var x = 123;
50 | x * 2;
51 | )]]
52 | assert.are.equal(expected, block)
53 | return data
54 | end)
55 | end)
56 |
57 | it('can send linewise visual selection', function()
58 | vim.api.nvim_win_set_cursor(0, { 6, 0 })
59 | vim.cmd [[normal! V]]
60 | vim.api.nvim_win_set_cursor(0, { 7, 0 })
61 | editor.send_selection(function(data)
62 | local selection = table.concat(data, '\n')
63 | local expected = [[
64 | x = 7;
65 | x = x * 2;]]
66 | assert.are.equal(expected, selection)
67 | return data
68 | end)
69 | end)
70 |
71 | it('can send a visual selection', function()
72 | vim.api.nvim_win_set_cursor(0, { 6, 0 })
73 | vim.cmd [[normal! v]]
74 | vim.api.nvim_win_set_cursor(0, { 6, 4 })
75 | editor.send_selection(function(data)
76 | local selection = table.concat(data, '\n')
77 | local expected = 'x = 7'
78 | assert.are.equal(expected, selection)
79 | return data
80 | end)
81 | end)
82 |
83 | it('ignores parenthesis in comments', function()
84 | vim.api.nvim_win_set_cursor(0, { 8, 0 })
85 | editor.send_block(function(data)
86 | local block = table.concat(data, '\n')
87 | local expected = [[
88 | (
89 | var x = 7;
90 | // )
91 | x * 2;
92 | )]]
93 | assert.are.equal(expected, block)
94 | return data
95 | end)
96 | end)
97 |
98 | it('ignores everything after block end', function()
99 | vim.api.nvim_win_set_cursor(0, { 13, 0 })
100 | editor.send_block(function(data)
101 | local block = table.concat(data, '\n')
102 | local expected = [[
103 | (
104 | var y = 3;
105 | y * 2;
106 | )]]
107 | assert.are.equal(expected, block)
108 | return data
109 | end)
110 | end)
111 |
112 | it('can send a single line containing parenthesis', function()
113 | vim.api.nvim_win_set_cursor(0, { 17, 0 })
114 | editor.send_block(function(data)
115 | local block = table.concat(data, '\n')
116 | local expected = [[(degree: 0).play;]]
117 | assert.are.equal(expected, block)
118 | return data
119 | end)
120 | end)
121 | end)
122 |
123 | describe('autocmds', function()
124 | local config = require 'scnvim.config'
125 | it('treats .sc files as supercollider', function()
126 | editor.setup()
127 | vim.cmd [[edit Test.sc]]
128 | assert.are.equal('supercollider', vim.bo.filetype)
129 | config.editor.force_ft_supercollider = false
130 | editor.setup()
131 | vim.cmd [[edit Test.sc]]
132 | assert.are.equal('scala', vim.bo.filetype)
133 | config.editor.force_ft_supercollider = true
134 | end)
135 |
136 | it('can use user keymaps in post window', function()
137 | local postwin = require 'scnvim.postwin'
138 | local map = require('scnvim.map').map
139 | config.postwin.keymaps = {
140 | q = map(function()
141 | postwin.close()
142 | end),
143 | }
144 | editor.setup() -- add autocmds
145 | vim.cmd [[edit maptest.scd]]
146 | postwin.focus()
147 | assert.is_true(postwin.is_open())
148 | assert.are.equal('scnvim', vim.bo.filetype)
149 | vim.cmd [[normal q]]
150 | assert.is_false(postwin.is_open())
151 | end)
152 | end)
153 | end)
154 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test/spec/automated/postwin_spec.lua:
--------------------------------------------------------------------------------
1 | local postwin = require 'scnvim.postwin'
2 | local config = require 'scnvim.config'
3 |
4 | describe('post window', function()
5 | before_each(function()
6 | config.postwin.float.enabled = false
7 | postwin.destroy()
8 | end)
9 |
10 | it('can be displayed in a split window', function()
11 | local id = postwin.open()
12 | local cfg = vim.api.nvim_win_get_config(id)
13 | assert.is_number(id)
14 | assert.is_true(postwin.is_open())
15 | assert.is_nil(cfg.zindex)
16 | postwin.close()
17 | local new_id = postwin.open()
18 | assert.is_number(new_id)
19 | assert.are_not.equal(id, new_id)
20 | end)
21 |
22 | it('can be displayed in a floating window', function()
23 | config.postwin.float.enabled = true
24 | local id = postwin.open()
25 | local cfg = vim.api.nvim_win_get_config(id)
26 | assert.is_number(id)
27 | assert.is_true(postwin.is_open())
28 | assert.not_nil(cfg.zindex)
29 | postwin.close()
30 | local new_id = postwin.open()
31 | assert.is_number(new_id)
32 | assert.are_not.equal(id, new_id)
33 | end)
34 |
35 | it('can add lines to its buffer', function()
36 | local expected = '-> 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 " 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/config.lua:
--------------------------------------------------------------------------------
1 | --- Default configuration.
2 | --- Provides fallback values not specified in the user config.
3 | ---@module scnvim.config
4 |
5 | --- table
6 | ---@table default
7 | ---@field ensure_installed (default: true) If installed once, this can be set to false to improve startup time.
8 | local default = {
9 | ensure_installed = true,
10 |
11 | --- table
12 | ---@table default.sclang
13 | ---@field cmd Path to the sclang executable. Not needed if sclang is already in your $PATH.
14 | ---@field args Comma separated arguments passed to the sclang executable.
15 | sclang = {
16 | cmd = nil,
17 | args = {},
18 | },
19 |
20 | --- table (empty by default)
21 | ---@table default.keymaps
22 | ---@field keymap scnvim.map
23 | ---@usage keymaps = {
24 | --- [''] = 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/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # scnvim
2 |
3 | [Neovim][neovim] frontend for [SuperCollider][supercollider].
4 |
5 | [](https://github.com/davidgranstrom/scnvim/actions/workflows/ci.yml)
6 | [](https://github.com/davidgranstrom/scnvim/actions/workflows/lint.yml)
7 | [](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 [lazy.nvim](https://github.com/folke/lazy.nvim)
47 |
48 | ```lua
49 | return {
50 | 'davidgranstrom/scnvim',
51 | ft = 'supercollider',
52 | config = function()
53 | local scnvim = require 'scnvim'
54 | local map = scnvim.map
55 | local map_expr = scnvim.map_expr
56 | scnvim.setup {
57 | -- your config here
58 | }
59 | end
60 | }
61 | ```
62 |
63 | * Using [packer.nvim](https://github.com/wbthomason/packer.nvim)
64 |
65 | ```lua
66 | use { 'davidgranstrom/scnvim' }
67 | ```
68 |
69 | * Using [vim-plug](https://github.com/junegunn/vim-plug)
70 |
71 | ```vim
72 | Plug 'davidgranstrom/scnvim'
73 | ```
74 |
75 | ### Verify
76 |
77 | Run `:checkhealth scnvim` to verify that the installation was successful.
78 |
79 | ## Usage
80 |
81 | ### Configuration
82 |
83 | `scnvim` uses `lua` for configuration. Below is an example that you can copy
84 | and paste to your `init.lua`.
85 |
86 | If you are using `init.vim` for configuration you will need to surround the
87 | call to `scnvim.setup` in a `lua-heredoc`:
88 |
89 | ```vim
90 | " file: init.vim
91 | lua << EOF
92 | require('scnvim').setup({})
93 | EOF
94 | ```
95 |
96 | ### Example
97 |
98 | ```lua
99 | local scnvim = require 'scnvim'
100 | local map = scnvim.map
101 | local map_expr = scnvim.map_expr
102 |
103 | scnvim.setup({
104 | keymaps = {
105 | [''] = map('editor.send_line', {'i', 'n'}),
106 | [''] = {
107 | map('editor.send_block', {'i', 'n'}),
108 | map('editor.send_selection', 'x'),
109 | },
110 | [''] = map('postwin.toggle'),
111 | [''] = map('postwin.toggle', 'i'),
112 | [''] = map('postwin.clear', {'n', 'i'}),
113 | [''] = map('signature.show', {'n', 'i'}),
114 | [''] = map('sclang.hard_stop', {'n', 'x', 'i'}),
115 | ['st'] = map('sclang.start'),
116 | ['sk'] = map('sclang.recompile'),
117 | [''] = map_expr('s.boot'),
118 | [''] = map_expr('s.meter'),
119 | },
120 | editor = {
121 | highlight = {
122 | color = 'IncSearch',
123 | },
124 | },
125 | postwin = {
126 | float = {
127 | enabled = true,
128 | },
129 | },
130 | })
131 | ```
132 |
133 | ### Start
134 |
135 | Open a new file in `nvim` with a `.scd` or `.sc` extension and type `:SCNvimStart` to start SuperCollider.
136 |
137 | ### Commands
138 |
139 | | Command | Description |
140 | |:-----------------------|:---------------------------------------------------------------|
141 | | `SCNvimStart` | Start SuperCollider |
142 | | `SCNvimStop` | Stop SuperCollider |
143 | | `SCNvimRecompile` | Recompile SCClassLibrary |
144 | | `SCNvimGenerateAssets` | Generate tags, syntax, snippets etc. |
145 | | `SCNvimHelp ` | Open help for \ (By default mapped to `K`) |
146 | | `SCNvimStatusLine` | Start to poll server status to be displayed in the status line |
147 |
148 | ### Additional setup
149 |
150 | Run `:SCNvimGenerateAssets` after starting SuperCollider to generate syntax highlighting and tags.
151 |
152 | The plugin should work "out of the box", but if you want even more fine-grained
153 | control please have a look at the [configuration
154 | section](https://github.com/davidgranstrom/scnvim/wiki/Configuration) in the
155 | wiki.
156 |
157 | ## Documentation
158 |
159 | * `:help scnvim` for detailed documentation.
160 | * [API documentation](https://davidgranstrom.github.io/scnvim/)
161 |
162 | ## Extensions
163 |
164 | The extension system provides additional functionalities and integrations. If
165 | you have made a scnvim extension, please open a PR and add it to this list!
166 |
167 | * [fzf-sc](https://github.com/madskjeldgaard/fzf-sc)
168 | - Combine the magic of fuzzy searching with the magic of SuperCollider in Neovim
169 | * [nvim-supercollider-piano](https://github.com/madskjeldgaard/nvim-supercollider-piano)
170 | - Play SuperCollider synths using your (computer) keyboard in neovim!
171 | * [scnvim-tmux](https://github.com/davidgranstrom/scnvim-tmux)
172 | - Redirect post window ouput to a tmux pane.
173 | * [scnvim-logger](https://github.com/davidgranstrom/scnvim-logger)
174 | - Log post window output to a file (example scnvim extension)
175 | * [telescope-scdoc](https://github.com/davidgranstrom/telescope-scdoc.nvim)
176 | - Use Telescope to fuzzy find documentation
177 |
178 | ## Supported platforms
179 |
180 | * Linux
181 | * macOS
182 | * Windows (tested with `nvim-qt` and `nvim.exe` in Windows PowerShell)
183 |
184 | ### Note to Windows users
185 |
186 | The path to `sclang.exe` needs to be specified in the config:
187 |
188 | ```lua
189 | local scnvim = require('scnvim')
190 | scnvim.setup({
191 | sclang = {
192 | cmd = 'C:/Program Files/SuperCollider-3.12.2/sclang.exe'
193 | },
194 | })
195 | ```
196 |
197 | Modify the `sclang.cmd` to point to where SuperCollider is installed on your system.
198 |
199 | Additionally, to be able to boot the server you will need to add the following to `startup.scd`:
200 |
201 | ```supercollider
202 | if (\SCNvim.asClass.notNil) {
203 | Server.program = (Platform.resourceDir +/+ "scsynth.exe").quote;
204 | }
205 | ```
206 |
207 | ## License
208 |
209 | ```plain
210 | scnvim - Neovim frontend for SuperCollider
211 | Copyright © 2018 David Granström
212 |
213 | This program is free software: you can redistribute it and/or modify
214 | it under the terms of the GNU General Public License as published by
215 | the Free Software Foundation, either version 3 of the License, or
216 | (at your option) any later version.
217 |
218 | This program is distributed in the hope that it will be useful,
219 | but WITHOUT ANY WARRANTY; without even the implied warranty of
220 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
221 | GNU General Public License for more details.
222 |
223 | You should have received a copy of the GNU General Public License
224 | along with this program. If not, see .
225 | ```
226 |
227 | [neovim]: https://github.com/neovim/neovim
228 | [supercollider]: https://github.com/supercollider/supercollider
229 |
--------------------------------------------------------------------------------
/syntax/scdoc.vim:
--------------------------------------------------------------------------------
1 | " This file is part of SCVIM.
2 | "
3 | " SCVIM is free software: you can redistribute it and/or modify
4 | " it under the terms of the GNU General Public License as published by
5 | " the Free Software Foundation, either version 3 of the License, or
6 | " (at your option) any later version.
7 | "
8 | " SCVIM is distributed in the hope that it will be useful,
9 | " but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | " GNU General Public License for more details.
12 | "
13 | " You should have received a copy of the GNU General Public License
14 | " along with SCVIM. If not, see .
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 /\
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/scide_scnvim/Classes/SCNvim.sc:
--------------------------------------------------------------------------------
1 | SCNvim {
2 | 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/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 << "\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 << " ";
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 << "\n";
465 | };
466 | this.renderChildren(stream, node);
467 | },
468 | \INSTANCEMETHODS, {
469 | if(node.notPrivOnly) {
470 | stream << "\n";
471 | };
472 | this.renderChildren(stream, node);
473 | },
474 | \DESCRIPTION, {
475 | stream << "\n";
476 | this.renderChildren(stream, node);
477 | },
478 | \EXAMPLES, {
479 | stream << "\n";
480 | this.renderChildren(stream, node);
481 | },
482 | \SECTION, {
483 | stream << "\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 << "\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 |
--------------------------------------------------------------------------------
|