├── .gitignore ├── README.md ├── after └── plugin │ └── cmp_tidal.lua ├── lua └── cmp_tidal │ ├── init.lua │ ├── source-completions.lua │ ├── source-samples.lua │ └── utils.lua └── showcase.gif /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cmp-tidal 2 | 3 | Autocompletion for [Tidal Cycles](https://tidalcycles.org/) powered by [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) and [hoogle](http://hackage.haskell.org/cgi-bin/hackage-scripts/package/hoogle). 4 | 5 | ![Showcase](showcase.gif) 6 | 7 | ## Requirements 8 | 9 | - [neovim](https://github.com/neovim/neovim) 10 | - [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) 11 | - [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) 12 | - [hoogle](http://hackage.haskell.org/cgi-bin/hackage-scripts/package/hoogle) 13 | 14 | ## Installation 15 | 16 | ### Nvim-cmp 17 | 18 | Please have a look at the [official repo](https://github.com/hrsh7th/nvim-cmp). 19 | 20 | ### Cmp-tidal 21 | 22 | For [vim-plug](https://github.com/junegunn/vim-plug): 23 | 24 | ```vim 25 | Plug 'nvim-lua/plenary.nvim' 26 | Plug 'unqool/cmp-tidal' 27 | ``` 28 | 29 | For [packer.nvim](https://github.com/wbthomason/packer.nvim): 30 | 31 | ```lua 32 | use({ "unqool/cmp-tidal", requires = "nvim-lua/plenary.nvim" }) 33 | ``` 34 | 35 | ### Hoogle 36 | 37 | ```sh 38 | cabal install hoogle 39 | hoogle generate 40 | ``` 41 | 42 | ## Configuration 43 | 44 | ### Tidal 45 | 46 | ```lua 47 | require("cmp").setup({ 48 | sources = { 49 | { name = "tidal" }, 50 | -- ...more sources 51 | }, 52 | }) 53 | ``` 54 | 55 | ### Samples 56 | 57 | ```lua 58 | require("cmp").setup({ 59 | sources = { 60 | { name = "tidal" }, 61 | { name = "tidal_samples" }, 62 | -- ...more sources 63 | }, 64 | }) 65 | 66 | ``` 67 | 68 | #### Options 69 | 70 | By default `tidal_samples` will use the standard installation paths for the 'Dirt Samples'. 71 | You can change this by passing the absolute path to your 'Dirt Samples' folder to the `dirt_samples` option. 72 | 73 | E.g.: 74 | 75 | ```lua 76 | require("cmp").setup({ 77 | sources = { 78 | { name = "tidal" }, 79 | { 80 | name = "tidal_samples", 81 | option = { 82 | dirt_samples = "~/Library/Application Support/SuperCollider/downloaded-quarks/Dirt-Samples", 83 | }, 84 | }, 85 | -- ...more sources 86 | }, 87 | }) 88 | ``` 89 | 90 | ## Roadmap 91 | 92 | - [ ] New option: `custom_samples` to pass additional custom sample folders 93 | - [ ] Autocompletion for sample number (bd -> bd:1) 94 | - [ ] Caching 95 | 96 | ## Ideas 97 | 98 | - New option `custom_commands` for `tidal` source to pass a list of commands that are not part of the Tidal library (e.g. {name: ..., abbr(optional): ..., description(optional): ...}) 99 | - Autocompletion for mini-notation (https://tidalcycles.org/docs/reference/mini_notation#mini-notation-table) 100 | -------------------------------------------------------------------------------- /after/plugin/cmp_tidal.lua: -------------------------------------------------------------------------------- 1 | require("cmp").register_source("tidal", require("cmp_tidal.source-completions")) 2 | require("cmp").register_source("tidal_samples", require("cmp_tidal.source-samples")) 3 | -------------------------------------------------------------------------------- /lua/cmp_tidal/init.lua: -------------------------------------------------------------------------------- 1 | local sources = {} 2 | 3 | sources.completions = require("cmp_tidal.source-completions").new() 4 | sources.samples = require("cmp_tidal.source-samples").new() 5 | 6 | return sources 7 | -------------------------------------------------------------------------------- /lua/cmp_tidal/source-completions.lua: -------------------------------------------------------------------------------- 1 | local utils = require("cmp_tidal.utils") 2 | local cmp = require("cmp") 3 | local Job = require("plenary.job") 4 | 5 | local source = {} 6 | 7 | source.is_available = function() 8 | return vim.bo.filetype == "tidal" 9 | end 10 | 11 | source.new = function() 12 | return setmetatable({}, { __index = source }) 13 | end 14 | 15 | source.complete = function(_, params, callback) 16 | local input = string.sub(params.context.cursor_before_line, params.offset) 17 | 18 | Job 19 | :new({ 20 | command = "hoogle", 21 | args = { "+tidal", input }, 22 | 23 | on_exit = function(job) 24 | local job_output = job:result() 25 | 26 | local completion_items = {} 27 | for _, element in ipairs(job_output) do 28 | local completion_table = utils.split_string(element) 29 | local label = completion_table[2] 30 | 31 | local item = { label = label, kind = cmp.lsp.CompletionItemKind.Function } 32 | table.insert(completion_items, item) 33 | 34 | callback({ items = completion_items, isIncomplete = true }) 35 | end 36 | end, 37 | }) 38 | :start() 39 | end 40 | 41 | -- Search for documentation when item is selected 42 | source.resolve = function(_, completion_item, callback) 43 | Job 44 | :new({ 45 | command = "hoogle", 46 | args = { "-i", "+tidal", completion_item.label }, 47 | 48 | on_exit = function(job) 49 | local documenation_table = job:result() 50 | local description_table = {} 51 | 52 | local type = documenation_table[1] 53 | local module = documenation_table[2] 54 | 55 | -- Get description from documentation_table 56 | for i = 3, table.maxn(documenation_table), 1 do 57 | table.insert(description_table, documenation_table[i]) 58 | end 59 | 60 | -- Convert description_table to string 61 | local description_string = table.concat(description_table, "\n") 62 | 63 | if documenation_table ~= nil then 64 | completion_item.documentation = { 65 | kind = "markdown", 66 | value = string.format("**Type:** %s\n**Module:** %s\n\n%s", type, module, description_string), 67 | } 68 | callback(completion_item) 69 | end 70 | end, 71 | }) 72 | :start() 73 | end 74 | 75 | return source 76 | -------------------------------------------------------------------------------- /lua/cmp_tidal/source-samples.lua: -------------------------------------------------------------------------------- 1 | local utils = require("cmp_tidal.utils") 2 | local cmp = require("cmp") 3 | local scan = require("plenary.scandir") 4 | 5 | local source = {} 6 | 7 | local default_option = { dirt_samples = utils.get_dirt_samples_path() } 8 | 9 | source.is_available = function() 10 | return vim.bo.filetype == "tidal" 11 | end 12 | 13 | source.new = function() 14 | return setmetatable({}, { __index = source }) 15 | end 16 | 17 | source._validate_options = function(_, params) 18 | local opts = vim.tbl_deep_extend("keep", params.option, default_option) 19 | vim.validate({ dirt_samples = { opts.dirt_samples, "string" } }) 20 | return opts 21 | end 22 | 23 | source.complete = function(self, params, callback) 24 | local opts = self:_validate_options(params) 25 | local dirt_samples = opts.dirt_samples 26 | 27 | scan.scan_dir_async(dirt_samples, { 28 | depth = 1, 29 | only_dirs = true, 30 | on_exit = function(folders) 31 | -- Folders 32 | local folder_table = {} 33 | for _, folder in ipairs(folders) do 34 | local folder_name = folder:match("^.+/(.+)$") 35 | local folder_item = { label = folder_name, kind = cmp.lsp.CompletionItemKind.Folder, path = folder } 36 | table.insert(folder_table, folder_item) 37 | end 38 | 39 | callback({ items = folder_table, isIncomplete = true }) 40 | end, 41 | }) 42 | end 43 | 44 | -- List files of selected folder in documentation 45 | source.resolve = function(_, completion_item, callback) 46 | scan.scan_dir_async(completion_item.path, { 47 | depth = 1, 48 | search_pattern = { "%.wav$", "%.WAV$", "%.flac$", "%.FLAC$", "%.aiff$", "%.AIFF$" }, 49 | on_exit = function(files) 50 | local files_table = {} 51 | for index, file in ipairs(files) do 52 | local file_name = file:match("^.+/(.+)$") 53 | table.insert(files_table, string.format("**:%s ::** %s", index, file_name)) 54 | end 55 | 56 | -- Add documentation 57 | local file_count = table.maxn(files_table) 58 | local documentation_string = table.concat(files_table, "\n") 59 | completion_item.documentation = { 60 | kind = "markdown", 61 | value = string.format("**Samples**: %s\n\n%s", file_count, documentation_string), 62 | } 63 | 64 | callback(completion_item) 65 | end, 66 | }) 67 | end 68 | 69 | return source 70 | -------------------------------------------------------------------------------- /lua/cmp_tidal/utils.lua: -------------------------------------------------------------------------------- 1 | local utils = {} 2 | 3 | utils.split_string = function(string) 4 | local t = {} 5 | for str in string.gmatch(string, "([^" .. "%s" .. "]+)") do 6 | table.insert(t, str) 7 | end 8 | return t 9 | end 10 | 11 | utils.get_os = function() 12 | if vim.fn.has("win64") == 1 or vim.fn.has("win32") == 1 or vim.fn.has("win16") == 1 then 13 | return "Windows" 14 | else 15 | return vim.fn.substitute(vim.fn.system("uname"), "\n", "", "") 16 | end 17 | end 18 | 19 | utils.get_dirt_samples_path = function() 20 | local os = utils.get_os() 21 | local os_paths = { 22 | ["Darwin"] = "/Library/Application Support/SuperCollider", 23 | ["Linux"] = "/.local/share/SuperCollider", 24 | ["Windows"] = "/AppData/Local/SuperCollider", 25 | } 26 | return vim.env.HOME .. os_paths[os] .. "/downloaded-quarks/Dirt-Samples" 27 | end 28 | 29 | return utils 30 | -------------------------------------------------------------------------------- /showcase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unqool/cmp-tidal/f6221d5bd222ba1c275586ae72bd784e31c14018/showcase.gif --------------------------------------------------------------------------------