Tree-sitter support for SuperCollider

Ranjith Hegde has added support for SuperCollider in the textobjects plugin for Neovim, allowing really cool syntax specific manipulation !!!

2 Likes

Hey :slightly_smiling_face:

Thanks for working on this! I am using nvim and I have treesitter and cmp working. It is all working for Rust for example but in SC I donā€™t get autocompletion options like you do here (I only get a popup with type ā€˜textā€™). Do you mind sharing your config?

Thanks!

Sure, here is mine:

-- Setup nvim-cmp.
local cmp = require'cmp'

-- local lspkind = require "lspkind"
-- lspkind.init()

vim.opt.completeopt = { "menu", "menuone", "noselect" }

-- Don't show the dumb matching stuff.
vim.opt.shortmess:append "c"

cmp.setup({
	snippet = {
		expand = function(args)
			require('luasnip').lsp_expand(args.body) -- For `luasnip` users.
			-- vim.fn["UltiSnips#Anon"](args.body) -- For `ultisnips` users.
		end,
	},
	mapping = {
		['<C-d>'] = cmp.mapping.scroll_docs(-4),
		['<C-f>'] = cmp.mapping.scroll_docs(4),
		['<C-Space>'] = cmp.mapping.complete(),
		['<C-e>'] = cmp.mapping.close(),
		['<CR>'] = cmp.mapping.confirm({ select = true }),
	},
	sources = {
		{ name = 'path' },
		{ name = 'luasnip' }, -- For luasnip users.
		{ name = 'nvim_lsp' },
		{ name = 'tags' },
		-- { name = 'nvim_lua' },
		{ name = 'treesitter' },
		-- { name = 'spell' },
		{ name = 'buffer' , keyword_length=5}, -- dont complete until at 5 chars
	},
	formatting = {
		-- set up nice formatting for your sources.
		-- format = lspkind.cmp_format {
		-- 	with_text = true,
		-- 	menu = {
		-- 		nvim_lsp = "[LSP]",
		-- 		nvim_lua = "[api]",
		-- 		path = "[path]",
		-- 		luasnip = "[snip]",
		-- 		-- gh_issues = "[issues]",
		-- 		rg = "[ripgrep]",
		-- 		tags = "[tags]",
		-- 		buffer = "[buf]",
		-- 	},
		-- },
	},
	view = {
		entries = "native",
	},
	experimental = {
		-- native_menu = true,
		ghost_text = true
	}
})

And then the sources I have installed as plugins:

use {'hrsh7th/nvim-cmp',
			disable = false,
			requires = {
			'hrsh7th/cmp-nvim-lsp',
			'hrsh7th/cmp-buffer',
			'quangnguyen30192/cmp-nvim-tags',
			'saadparwaiz1/cmp_luasnip',
			'hrsh7th/cmp-nvim-lua',
			-- 'f3fora/cmp-spell',
			'ray-x/cmp-treesitter',
			'hrsh7th/cmp-path',
			-- 'onsails/lspkind-nvim',
			-- 'lukas-reineke/cmp-rg'
			}, config = function()
	require"plugins/cmp"
end}
2 Likes

I seem to have a similar problem to Dionysis even with your config Mads! I see Snippet completions and Text but no luck with Tags or Treesitter. Just been lamely copypasting so apologies if Iā€™ve missed something - any tips appreciated!

Did you install these plugins ?

		'hrsh7th/cmp-nvim-lsp',
		'hrsh7th/cmp-buffer',
		'quangnguyen30192/cmp-nvim-tags',
		'saadparwaiz1/cmp_luasnip',
		'hrsh7th/cmp-nvim-lua',
		-- 'f3fora/cmp-spell',
		'ray-x/cmp-treesitter',
		'hrsh7th/cmp-path',

You also need to generate snippets and tags in SCNvim to see those. See the SCnVim wiki for more info as well as luasnip setup

Thank you! I finally got it working. That was not easy :sweat_smile: But I am very disapointed that I am not getting the help file in the popup :face_with_monocle::stuck_out_tongue: Just kidding, so happy to have argument completion! I had tried to implement this in the past during the times when vimscript was the only option and had failedā€¦ This is luxury!

@semiquaver Donā€™t give up. There is kind of crazy amount of plugins involved to get the full experience! Here is my configuration in case it helps (Make sure you commit your current setup before experimenting with any cut and paste of my mess thoughā€¦).

Plug 'nvim-treesitter/nvim-treesitter', {'do': ':TSUpdate'}
Plug 'nvim-treesitter/nvim-treesitter-refactor'
Plug 'nvim-treesitter/playground'
Plug 'haorenW1025/completion-nvim'
Plug 'nvim-treesitter/completion-treesitter'
Plug 'nvim-treesitter/nvim-treesitter-textobjects'

Plug 'neovim/nvim-lspconfig'
Plug 'williamboman/nvim-lsp-installer'

Plug 'hrsh7th/cmp-nvim-lsp'
Plug 'hrsh7th/cmp-buffer'
Plug 'hrsh7th/cmp-path'
Plug 'hrsh7th/cmp-cmdline'
Plug 'hrsh7th/nvim-cmp'
Plug 'ray-x/cmp-treesitter'
Plug 'quangnguyen30192/cmp-nvim-tags'
Plug 'hrsh7th/cmp-nvim-lua'

" For vsnip users.
Plug 'hrsh7th/cmp-vsnip'

" Snippets {{{
Plug 'rafamadriz/friendly-snippets'
Plug 'hrsh7th/vim-vsnip'
Plug 'L3MON4D3/LuaSnip'
Plug 'saadparwaiz1/cmp_luasnip'

let g:scnvim_snippet_format = "luasnip"

" Jump forward or backward in vsnip
imap <expr> <Tab>   vsnip#jumpable(1)   ? '<Plug>(vsnip-jump-next)'      : '<Tab>'
smap <expr> <Tab>   vsnip#jumpable(1)   ? '<Plug>(vsnip-jump-next)'      : '<Tab>'
imap <expr> <S-Tab> vsnip#jumpable(-1)  ? '<Plug>(vsnip-jump-prev)'      : '<S-Tab>'
smap <expr> <S-Tab> vsnip#jumpable(-1)  ? '<Plug>(vsnip-jump-prev)'      : '<S-Tab>'

" and here for luasnip
imap <silent><expr> <Tab> luasnip#expand_or_jumpable() ? '<Plug>luasnip-expand-or-jump' : '<Tab>' 
" -1 for jumping backwards.
inoremap <silent> <S-Tab> <cmd>lua require'luasnip'.jump(-1)<Cr>

snoremap <silent> <Tab> <cmd>lua require('luasnip').jump(1)<Cr>
snoremap <silent> <S-Tab> <cmd>lua require('luasnip').jump(-1)<Cr>

"}}}

set completeopt=menu,menuone,noselect

and for lua:

-- SuperCollider

require("luasnip").add_snippets("supercollider", require("scnvim/utils").get_snippets())

-- Rust
require('rust-tools').setup({})

require("nvim-treesitter.configs").setup {
  -- ensure_installed = {"supercollider", "rust", "html", "javascript"},
  ensure_installed = "maintained",
  highlight = {
    enable = true, additional_vim_regex_highlighting = true,
    -- disable = { "supercollider"},
  },
  incremental_selection = {
    enable = true,
    keymaps = {
      init_selection = "<CR>",
      scope_incremental = "<CR>",
      node_incremental = "<TAB>",
      node_decremental = "<S-TAB>",
    },
  },
  indent = { enable = true },
  matchup = { enable = true },
  autopairs = { enable = true },
  playground = {
    enable = true,
    disable = {},
    updatetime = 25,
    persist_queries = false,
    keybindings = {
      toggle_query_editor = "o",
      toggle_hl_groups = "i",
      toggle_injected_languages = "t",
      toggle_anonymous_nodes = "a",
      toggle_language_display = "I",
      focus_language = "f",
      unfocus_language = "F",
      update = "R",
      goto_node = "<cr>",
      show_help = "?",
    },
  },
  rainbow = {
    enable = true,
    extended_mode = true, -- Highlight also non-parentheses delimiters
    max_file_lines = 1000,
  },
  refactor = {
    smart_rename = { enable = true, keymaps = { smart_rename = "grr" } },
    highlight_definitions = { enable = true },
    navigation = {
      enable = true,
      keymaps = {
        goto_definition_lsp_fallback = "gnd",
        -- use telescope for these lists
        -- list_definitions = "gnD",
        -- list_definitions_toc = "gO",
        -- @TODOUA: figure out if I need both below
        goto_next_usage = "<a-*>", -- is this redundant?
        goto_previous_usage = "<a-#>", -- also this one?
      },
      disable = { "supercollider"},
    },
    -- highlight_current_scope = {enable = true}
  },
  textobjects = {
    lsp_interop = {
      enable = true,
      border = "none",
      peek_definition_code = {
        ["df"] = "@function.outer",
        ["dF"] = "@class.outer",
      },
    },
    move = {
      enable = true,
      set_jumps = true, -- whether to set jumps in the jumplist
      goto_next_start = {
        ["]m"] = "@function.outer",
        ["]]"] = "@call.outer",
      },
      goto_next_end = {
        ["]M"] = "@function.outer",
        ["]["] = "@call.outer",
      },
      goto_previous_start = {
        ["[m"] = "@function.outer",
        ["[["] = "@call.outer",
      },
      goto_previous_end = {
        ["[M"] = "@function.outer",
        ["[]"] = "@call.outer",
      },
    },
    select = {
      enable = true,
      lookahead = true,
      keymaps = {
        ["af"] = "@function.outer",
        ["if"] = "@function.inner",
        ["ac"] = "@call.outer",
        ["ic"] = "@call.inner",
      },
    },
    swap = {
      enable = true,
      swap_next = {
        [",a"] = "@parameter.inner",
      },
      swap_previous = {
        [",A"] = "@parameter.inner",
      },
    },
  },
}

vim.opt.foldmethod = "expr"
vim.opt.foldexpr = "nvim_treesitter#foldexpr()"

local cmp = require'cmp'

vim.opt.completeopt = { "menu", "menuone", "noselect" }
vim.opt.shortmess:append "c"

cmp.setup {
  snippet = {
      -- REQUIRED - you must specify a snippet engine
      expand = function(args)
        vim.fn["vsnip#anonymous"](args.body) -- For `vsnip` users.
        require('luasnip').lsp_expand(args.body) -- For `luasnip` users.
        -- require('snippy').expand_snippet(args.body) -- For `snippy` users.
        -- vim.fn["UltiSnips#Anon"](args.body) -- For `ultisnips` users.
      end,
    },
	mapping = {
		['<C-d>'] = cmp.mapping.scroll_docs(-4),
		['<C-f>'] = cmp.mapping.scroll_docs(4),
		['<C-Space>'] = cmp.mapping.complete(),
		['<C-e>'] = cmp.mapping.close(),
		['<CR>'] = cmp.mapping.confirm({ select = true }),
	},
	sources = {
		{ name = 'path' },
		{ name = 'vsnip' },
		{ name = 'luasnip' },
		{ name = 'nvim_lsp' },
		{ name = 'tags' },
		-- { name = 'nvim_lua' },
		{ name = 'treesitter' },
		-- { name = 'spell' },
		{ name = 'buffer' , keyword_length=5}, -- dont complete until at 5 chars
	},
	view = {
		entries = "native",
	},
	experimental = {
		-- native_menu = true,
		ghost_text = true
	}
}

-- The nvim-cmp almost supports LSP's capabilities so You should advertise it to LSP servers..
local capabilities = vim.lsp.protocol.make_client_capabilities()
capabilities = require('cmp_nvim_lsp').update_capabilities(capabilities)

-- The following example advertise capabilities to `clangd`.
require'lspconfig'.clangd.setup {
  capabilities = capabilities,
}

Good luck! :slightly_smiling_face:

EDIT: Also check here Additional configuration Ā· davidgranstrom/scnvim Wiki Ā· GitHub

1 Like

whoop whoop! Congratulations!

Ps all of this dependency handling of plugins is a bit easier with Packer as packagemanager. It allows you to set these as dependencies of the main cmp plugin, which in turn allows you to disable the cmp plugin and then automatically all of the dependendents. Just a tip.

1 Like

Ah yes, I can see everyone using Packer. It is just that I have a 1000 lines configuration in vimscript and havenā€™t decided to transition to lua yet. I literally just added a require for a config.lua a few days ago to manage treesitter and cmp configurations! I do like the idea of moving to lua though so it is just a matter of time :slightly_smiling_face: Again thanks for your work!! Great stuff :upside_down_face:

1 Like

Bouncing this, since it seems like the right topic!

Got this working in my vim, and now I want to figure out how to do two things:

  1. Add indentation/auto formatting on save, similar to clang-format. From what I see, this should be possible ? But Iā€™m having a hard time finding any documention on how to begin creating these rules.
  2. If 1. succeeds, limiting the max number of columns to 80.

Basically, start working on creating a ā€œBlackā€ for Supercollider code. Iā€™m happy to tinker on it because itā€™s the next piece in my larger puzzle of creating a proper, self-contained projects environment for SC.

@madskjeldgaard

I found this:

You can use the existing .so generated for the scnvim tree-parser and this lets you write a python script to handle the code formatting. External formatters (i.e. clang-format and gofmt) are industry standard, so having a python script that parses tree-sitter code and formats it according to code conventions (Code style guidelines Ā· supercollider/supercollider Wiki Ā· GitHub) should be reasonably straightforward.

Iā€™ve written a parser already that traverses an arbitrary supercollider code tree, so now I think itā€™s a matter of hammering out the rules. Itā€™d likely be better to make it as opinionated as possible and follow the existing community guidelines. gofmt is only 500 lines or so (go/gofmt.go at master Ā· golang/go Ā· GitHub) so with the tree parser in hand (EXCELLENT work, btw) Iā€™m hoping this wonā€™t be so bad ?

1 Like

Example output of the python call and the tree walk:

$ python ./sclang-format.py -f ../supercollider/sinosc.sc -l ./sclang.so
String: { SinOsc.ar(200, 0, 0.5) }.play;


<Node kind=source_file, start_point=(0, 0), end_point=(2, 0)> b'{ SinOsc.ar(200, 0, 0.5) }.play;\n\n'
<Node kind=function_call, start_point=(0, 0), end_point=(0, 31)> b'{ SinOsc.ar(200, 0, 0.5) }.play'
<Node kind=receiver, start_point=(0, 0), end_point=(0, 26)> b'{ SinOsc.ar(200, 0, 0.5) }'
<Node kind=function_block, start_point=(0, 0), end_point=(0, 26)> b'{ SinOsc.ar(200, 0, 0.5) }'
<Node kind="{", start_point=(0, 0), end_point=(0, 1)> b'{'
<Node kind=function_call, start_point=(0, 2), end_point=(0, 24)> b'SinOsc.ar(200, 0, 0.5)'
<Node kind=receiver, start_point=(0, 2), end_point=(0, 8)> b'SinOsc'
<Node kind=class, start_point=(0, 2), end_point=(0, 8)> b'SinOsc'
<Node kind=method_call, start_point=(0, 8), end_point=(0, 24)> b'.ar(200, 0, 0.5)'
<Node kind=".", start_point=(0, 8), end_point=(0, 9)> b'.'
<Node kind=method_name, start_point=(0, 9), end_point=(0, 11)> b'ar'
<Node kind="(", start_point=(0, 11), end_point=(0, 12)> b'('
<Node kind=parameter_call_list, start_point=(0, 12), end_point=(0, 23)> b'200, 0, 0.5'
<Node kind=argument_calls, start_point=(0, 12), end_point=(0, 15)> b'200'
<Node kind=unnamed_argument, start_point=(0, 12), end_point=(0, 15)> b'200'
<Node kind=literal, start_point=(0, 12), end_point=(0, 15)> b'200'
<Node kind=number, start_point=(0, 12), end_point=(0, 15)> b'200'
<Node kind=integer, start_point=(0, 12), end_point=(0, 15)> b'200'
<Node kind=",", start_point=(0, 15), end_point=(0, 16)> b','
<Node kind=argument_calls, start_point=(0, 17), end_point=(0, 18)> b'0'
<Node kind=unnamed_argument, start_point=(0, 17), end_point=(0, 18)> b'0'
<Node kind=literal, start_point=(0, 17), end_point=(0, 18)> b'0'
<Node kind=number, start_point=(0, 17), end_point=(0, 18)> b'0'
<Node kind=integer, start_point=(0, 17), end_point=(0, 18)> b'0'
<Node kind=",", start_point=(0, 18), end_point=(0, 19)> b','
<Node kind=argument_calls, start_point=(0, 20), end_point=(0, 23)> b'0.5'
<Node kind=unnamed_argument, start_point=(0, 20), end_point=(0, 23)> b'0.5'
<Node kind=literal, start_point=(0, 20), end_point=(0, 23)> b'0.5'
<Node kind=number, start_point=(0, 20), end_point=(0, 23)> b'0.5'
<Node kind=float, start_point=(0, 20), end_point=(0, 23)> b'0.5'
<Node kind=")", start_point=(0, 23), end_point=(0, 24)> b')'
<Node kind="}", start_point=(0, 25), end_point=(0, 26)> b'}'
<Node kind=method_call, start_point=(0, 26), end_point=(0, 31)> b'.play'
<Node kind=".", start_point=(0, 26), end_point=(0, 27)> b'.'
<Node kind=method_name, start_point=(0, 27), end_point=(0, 31)> b'play'
<Node kind=";", start_point=(0, 31), end_point=(0, 32)> b';'

I think thereā€™s a lot here to work with - is there a mailing list for chatter about the sc treesitter code so I donā€™t clog things up here ?

This is probably the best place for it - maybe start a new thread in the Development category? Good code formatting is an exciting prospect - as soon as you have a proof of concept working, Iā€™ll integrate it into the VSCode / language server quark.

Exciting stuff ! Looking forward to seeing the progress of this.

Note of warning: the indentation queries in the Supercollider TS grammar may not be super fulfilling because I simply donā€™t understand how to use those queries but if they need to be changed as part of this let me know. But maybe it isnā€™t even used in this ?

Thanks! I did some investigating on the tree sitter indentation and it seemed neither fully featured nor well documented ; I think for now, at least, having a separate script that manages formatting will return results that more closely match the coding guidelines.

Weā€™ll see how it goes as I get this moving.

This plugin now has tree-sitter-supercollider support:

awesome. Iā€™ve written a proof of concept for the low-hanging fruit. Now Iā€™m going to need to do some treesitter queries.

Gist of where I am is below. Basic idea is that there are a number of formatter classes that parse the tree and the raw data, rebuilding the tree after each formatter does its work.

There are three types of formatters - pre-processing (i.e. stripping duplicate newlines, etc.), inline processing (the bulk), post-processing (i.e. adding the newling at the end of the file.)

Lots to be done, but as soon as I get my head around how to do one query, this should move pretty quickly.

We should also probably have a spirited argument about what the best general case format should be (think things like - are one-liners ever appropriate ?). It wonā€™t please everyone, but if weā€™re thinking about it, I think covering the edge cases that arenā€™t covered in the best-case document would potentially be a good use of time.

I think this is the best inspiration of what the end product should be - GitHub - psf/black: The uncompromising Python code formatter. If youā€™ve never read the manifesto, itā€™d be worth doing. I use this for work and while I donā€™t agree with everything it does, it does its job and gets out of the way.

Iā€™ll create a repo once I get a query working and a few more of the format classes written out.

Once the formatter is boiled down into rules, we can easily find a best-fit rule set for the existing class library by trying brute-force combinations and finding formatting options that produce the minimum diff with the current classlib. There are comparable tools for clang-format and Iā€™ve had good luck with them.

Brilliant. Iā€™ll figure out how to do a query and ts update, and then Iā€™ll post a repo.

Also, if thereā€™s a good set of transforms that cover the bases, we can create a set of unit tests to verify and do some test driven development.

2 Likes