├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .themisrc ├── LICENSE ├── README.md ├── autoload ├── db_ui.vim └── db_ui │ ├── connections.vim │ ├── dbout.vim │ ├── drawer.vim │ ├── notifications.vim │ ├── query.vim │ ├── schemas.vim │ ├── table_helpers.vim │ └── utils.vim ├── doc └── dadbod-ui.txt ├── ftplugin ├── dbout.vim ├── dbui.vim ├── javascript.vim ├── mysql.vim ├── plsql.vim └── sql.vim ├── plugin └── db_ui.vim ├── run.sh ├── syntax └── dbui.vim └── test ├── dadbod_ui_test.db ├── test-add-edit-delete-connection.vim ├── test-auto-execute-table-helpers.vim ├── test-bind-parameters-custom-pattern.vim ├── test-bind-parameters.vim ├── test-corrupted-connections-file.vim ├── test-custom-buffer-name-generator.vim ├── test-custom-icons.vim ├── test-db-navigation.vim ├── test-delete-buffer.vim ├── test-disable-mappings.vim ├── test-drawer-sections.vim ├── test-duplicate-connections-from-same-source.vim ├── test-find-buffer-in-drawer.vim ├── test-goto-sibling-and-node.vim ├── test-initialization-no-connections.vim ├── test-initialization-with-custom-env-variable.vim ├── test-initialization-with-dbs-array-function-variable.vim ├── test-initialization-with-dbs-dictionary-function-variable.vim ├── test-initialization-with-dbs-variable.vim ├── test-initialization-with-dotenv-variable.vim ├── test-initialization-with-env-and-dbs-variable.vim ├── test-initialization-with-env-variable.vim ├── test-mods.vim ├── test-open-query.vim ├── test-rename-buffer-and-file.vim ├── test-save-query-to-file.vim ├── test-show-help.vim ├── test-table-helpers.vim ├── test-tmp-query-location.vim ├── test-toggle-details.vim └── test-toggle-drawer-and-quit.vim /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: 17 | - ubuntu-latest 18 | - windows-latest 19 | - macos-latest 20 | versions: 21 | - version: nightly 22 | neovim: false 23 | - version: stable 24 | neovim: false 25 | - version: nightly 26 | neovim: true 27 | - version: stable 28 | neovim: true 29 | runs-on: ${{ matrix.os }} 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Setup Ubuntu System 33 | if: matrix.os == 'ubuntu-latest' 34 | run: | 35 | sudo apt-get install sqlite3 libsqlite3-dev 36 | - name: Setup Windows System 37 | if: matrix.os == 'windows-latest' 38 | run: | 39 | choco install sqlite 40 | - uses: rhysd/action-setup-vim@v1 41 | id: vim 42 | with: 43 | neovim: ${{ matrix.versions.neovim }} 44 | version: ${{ matrix.versions.version }} 45 | - name: Run tests 46 | env: 47 | THEMIS_VIM: ${{ steps.vim.outputs.executable }} 48 | # XXX: 49 | # Overwrite %TMP% to point a correct temp directory. 50 | # Note that %TMP% only affects value of 'tempname()' in Windows. 51 | # https://github.community/t5/GitHub-Actions/TEMP-is-broken-on-Windows/m-p/30432#M427 52 | TMP: 'C:\Users\runneradmin\AppData\Local\Temp' 53 | run: ./run.sh 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vim-themis 2 | vim-dotenv 3 | vim-dadbod 4 | -------------------------------------------------------------------------------- /.themisrc: -------------------------------------------------------------------------------- 1 | call themis#option('runtimepath', './vim-dadbod') 2 | call themis#option('runtimepath', './vim-dotenv') 3 | call themis#option('reporter', 'spec') 4 | filetype plugin indent on 5 | syntax enable 6 | let g:mapleader = ',' 7 | set shiftwidth=2 8 | set softtabstop=2 9 | set tabstop=2 10 | set expandtab 11 | set breakindent 12 | set smartindent 13 | let s:location_path = expand('%:p:h') 14 | let g:db_ui_save_location = s:location_path 15 | let g:db_ui_show_help = 0 16 | 17 | function! SetupTestDbs() abort 18 | let g:dbs = [ 19 | \ {'name': 'dadbod_ui_test', 'url': 'sqlite:'.fnamemodify('test/dadbod_ui_test.db', ':p') }, 20 | \ {'name': 'dadbod_ui_testing', 'url': 'sqlite:'.fnamemodify('test/dadbod_ui_test.db', ':p') }, 21 | \ ] 22 | endfunction 23 | 24 | function! Cleanup() abort 25 | call db_ui#reset_state() 26 | unlet! g:dbs 27 | let w = 2 28 | while w <= winnr('$') 29 | silent! execute w . 'close!' 30 | let w += 1 31 | endwhile 32 | silent! execute 'bwipeout!' join(range(1, bufnr('$')), ' ') 33 | endfunction 34 | 35 | function! SetOptionVariable(name, value) abort 36 | unlet! g:loaded_dbui 37 | let g:{a:name} = a:value 38 | let g:db_ui_save_location = s:location_path 39 | runtime plugin/db_ui.vim 40 | endfunction 41 | 42 | function! UnsetOptionVariable(name) abort 43 | unlet! g:loaded_dbui 44 | unlet! let g:{a:name} 45 | let g:db_ui_save_location = s:location_path 46 | runtime plugin/db_ui.vim 47 | endfunction 48 | 49 | " vim:ft=vim 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kristijan Husak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vim-dadbod-ui 2 | 3 | Simple UI for [vim-dadbod](https://github.com/tpope/vim-dadbod). 4 | It allows simple navigation through databases and allows saving queries for later use. 5 | 6 | ![screenshot](https://i.imgur.com/fhGqC9U.png) 7 | 8 | 9 | With nerd fonts: 10 | ![with-nerd-fonts](https://i.imgur.com/aXI5BTG.png) 11 | 12 | 13 | Video presentation by TJ: 14 | 15 | [![Video presentation by TJ](https://i.ytimg.com/vi/ALGBuFLzDSA/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDmOFtUnDmQx5U_PKBqV819YujOBw)](https://www.youtube.com/watch?v=ALGBuFLzDSA) 16 | 17 | Tested on Linux, Mac and Windows, Vim 8.1+ and Neovim. 18 | 19 | Features: 20 | * Navigate through multiple databases and it's tables and schemas 21 | * Several ways to define your connections 22 | * Save queries on single location for later use 23 | * Define custom table helpers 24 | * Bind parameters (see `:help vim-dadbod-ui-bind-parameters`) 25 | * Autocompletion with [vim-dadbod-completion](https://github.com/kristijanhusak/vim-dadbod-completion) 26 | * Jump to foreign keys from the dadbod output (see `:help (DBUI_JumpToForeignKey)`) 27 | * Support for nerd fonts (see `:help g:db_ui_use_nerd_fonts`) 28 | * Async query execution 29 | 30 | ## Installation 31 | 32 | Configuration with [lazy.nvim](https://github.com/folke/lazy.nvim) 33 | ```lua 34 | return { 35 | 'kristijanhusak/vim-dadbod-ui', 36 | dependencies = { 37 | { 'tpope/vim-dadbod', lazy = true }, 38 | { 'kristijanhusak/vim-dadbod-completion', ft = { 'sql', 'mysql', 'plsql' }, lazy = true }, -- Optional 39 | }, 40 | cmd = { 41 | 'DBUI', 42 | 'DBUIToggle', 43 | 'DBUIAddConnection', 44 | 'DBUIFindBuffer', 45 | }, 46 | init = function() 47 | -- Your DBUI configuration 48 | vim.g.db_ui_use_nerd_fonts = 1 49 | end, 50 | } 51 | ``` 52 | 53 | Or [vim-plug](https://github.com/junegunn/vim-plug) 54 | ```vim 55 | Plug 'tpope/vim-dadbod' 56 | Plug 'kristijanhusak/vim-dadbod-ui' 57 | Plug 'kristijanhusak/vim-dadbod-completion' "Optional 58 | ``` 59 | 60 | After installation, run `:DBUI`, which should open up a drawer with all databases provided. 61 | When you finish writing your query, just write the file (`:w`) and it will automatically execute the query for that database. 62 | 63 | ## Databases 64 | There are 3 ways to provide database connections to UI: 65 | 66 | 1. [Through environment variables](#through-environment-variables) 67 | 2. [Via g:dbs global variable](#via-gdbs-global-variable) 68 | 3. [Via :DBUIAddConnection command](#via-dbuiaddconnection-command) 69 | 70 | #### Through environment variables 71 | If `$DBUI_URL` env variable exists, it will be added as a connection. Name for the connection will be parsed from the url. 72 | If you want to use a custom name, pass `$DBUI_NAME` alongside the url. 73 | Env variables that will be read can be customized like this: 74 | 75 | ```vimL 76 | let g:db_ui_env_variable_url = 'DATABASE_URL' 77 | let g:db_ui_env_variable_name = 'DATABASE_NAME' 78 | ``` 79 | 80 | Optionally you can leverage [dotenv.vim](https://github.com/tpope/vim-dotenv) 81 | to specific any number of connections in an `.env` file by using a specific 82 | prefix (defaults to `DB_UI_`). The latter part of the env variable becomes the 83 | name of the connection (lowercased) 84 | 85 | ```bash 86 | # .env 87 | DB_UI_DEV=... # becomes the `dev` connection 88 | DB_UI_PRODUCTION=... # becomes the `production` connection 89 | ``` 90 | 91 | The prefix can be customized like this: 92 | 93 | ```vimL 94 | let g:db_ui_dotenv_variable_prefix = 'MYPREFIX_' 95 | ``` 96 | 97 | #### Via g:dbs global variable 98 | Provide list with all databases that you want to use through `g:dbs` variable as an array of objects or an object: 99 | 100 | ```vimL 101 | function s:resolve_production_url() 102 | let url = system('get-prod-url') 103 | return url 104 | end 105 | 106 | let g:dbs = { 107 | \ 'dev': 'postgres://postgres:mypassword@localhost:5432/my-dev-db', 108 | \ 'staging': 'postgres://postgres:mypassword@localhost:5432/my-staging-db', 109 | \ 'wp': 'mysql://root@localhost/wp_awesome', 110 | \ 'production': function('s:resolve_production_url') 111 | \ } 112 | ``` 113 | 114 | Or if you want them to be sorted in the order you define them, this way is also available: 115 | 116 | ```vimL 117 | function s:resolve_production_url() 118 | let url = system('get-prod-url') 119 | return url 120 | end 121 | 122 | let g:dbs = [ 123 | \ { 'name': 'dev', 'url': 'postgres://postgres:mypassword@localhost:5432/my-dev-db' } 124 | \ { 'name': 'staging', 'url': 'postgres://postgres:mypassword@localhost:5432/my-staging-db' }, 125 | \ { 'name': 'wp', 'url': 'mysql://root@localhost/wp_awesome' }, 126 | \ { 'name': 'production', 'url': function('s:resolve_production_url') }, 127 | \ ] 128 | ``` 129 | 130 | In case you use Neovim, here's an example with Lua: 131 | 132 | ```lua 133 | vim.g.dbs = { 134 | { name = 'dev', url = 'postgres://postgres:mypassword@localhost:5432/my-dev-db' }, 135 | { name = 'staging', url = 'postgres://postgres:mypassword@localhost:5432/my-staging-db' }, 136 | { name = 'wp', url = 'mysql://root@localhost/wp_awesome' }, 137 | { 138 | name = 'production', 139 | url = function() 140 | return vim.fn.system('get-prod-url') 141 | end 142 | }, 143 | } 144 | ``` 145 | 146 | 147 | Just make sure to **NOT COMMIT** these. I suggest using project local vim config (`:help exrc`) 148 | 149 | #### Via :DBUIAddConnection command 150 | 151 | Using `:DBUIAddConnection` command or pressing `A` in dbui drawer opens up a prompt to enter database url and name, 152 | that will be saved in `g:db_ui_save_location` connections file. These connections are available from everywhere. 153 | 154 | #### Connection related notes 155 | It is possible to have two connections with same name, but from different source. 156 | for example, you can have `my-db` in env variable, in `g:dbs` and in saved connections. 157 | To view from which source the database is, press `H` in drawer. 158 | If there are duplicate connection names from same source, warning will be shown and first one added will be preserved. 159 | 160 | ## Settings 161 | 162 | An overview of all settings and their default values can be found at `:help vim-dadbod-ui`. 163 | 164 | ### Table helpers 165 | Table helper is a predefined query that is available for each table in the list. 166 | Currently, default helper that each scheme has for it's tables is `List`, which for most schemes defaults to `g:db_ui_default_query`. 167 | Postgres, Mysql and Sqlite has some additional helpers defined, like "Indexes", "Foreign Keys", "Primary Keys". 168 | 169 | Predefined query can inject current db name and table name via `{table}` and `{dbname}`. 170 | 171 | To add your own for a specific scheme, provide it through .`g:db_ui_table_helpers`. 172 | 173 | For example, to add a "count rows" helper for postgres, you would add this as a config: 174 | 175 | ```vimL 176 | let g:db_ui_table_helpers = { 177 | \ 'postgresql': { 178 | \ 'Count': 'select count(*) from "{table}"' 179 | \ } 180 | \ } 181 | ``` 182 | 183 | Or if you want to override any of the defaults, provide the same name as part of config: 184 | ```vimL 185 | let g:db_ui_table_helpers = { 186 | \ 'postgresql': { 187 | \ 'List': 'select * from "{table}" order by id asc' 188 | \ } 189 | \ } 190 | ``` 191 | 192 | ### Auto execute query 193 | If this is set to `1`, opening any of the table helpers will also automatically execute the query. 194 | 195 | Default value is: `0` 196 | 197 | To enable it, add this to vimrc: 198 | 199 | ```vimL 200 | let g:db_ui_auto_execute_table_helpers = 1 201 | ``` 202 | 203 | ### Icons 204 | These are the default icons used: 205 | 206 | ```vimL 207 | let g:db_ui_icons = { 208 | \ 'expanded': '▾', 209 | \ 'collapsed': '▸', 210 | \ 'saved_query': '*', 211 | \ 'new_query': '+', 212 | \ 'tables': '~', 213 | \ 'buffers': '»', 214 | \ 'connection_ok': '✓', 215 | \ 'connection_error': '✕', 216 | \ } 217 | ``` 218 | 219 | You can override any of these: 220 | ```vimL 221 | let g:db_ui_icons = { 222 | \ 'expanded': '+', 223 | \ 'collapsed': '-', 224 | \ } 225 | ``` 226 | 227 | ### Help text 228 | To hide `Press ? for help` add this to vimrc: 229 | 230 | ``` 231 | let g:db_ui_show_help = 0 232 | ``` 233 | 234 | Pressing `?` will show/hide help no matter if this option is set or not. 235 | 236 | ### Drawer width 237 | 238 | What should be the drawer width when opened. Default is `40`. 239 | 240 | ```vimL 241 | let g:db_ui_winwidth = 30 242 | ``` 243 | 244 | ### Default query 245 | 246 | **DEPRECATED**: Use [Table helpers](#table-helpers) instead. 247 | 248 | When opening up a table, buffer will be prepopulated with some basic select, which defaults to: 249 | ```sql 250 | select * from table LIMIT 200; 251 | ``` 252 | To change the default value, use `g:db_ui_default_query`, where `{table}` is placeholder for table name. 253 | 254 | ```vimL 255 | let g:db_ui_default_query = 'select * from "{table}" limit 10' 256 | ``` 257 | 258 | ### Save location 259 | All queries are by default written to tmp folder. 260 | There's a mapping to save them permanently for later to the specific location. 261 | 262 | That location is by default `~/.local/share/db_ui`. To change it, addd `g:db_ui_save_location` to your vimrc. 263 | ```vimL 264 | let g:db_ui_save_location = '~/Dropbox/db_ui_queries' 265 | ``` 266 | 267 | ## Mappings 268 | These are the default mappings for `dbui` drawer: 269 | 270 | * o / \ - Open/Toggle Drawer options (`(DBUI_SelectLine)`) 271 | * S - Open in vertical split (`(DBUI_SelectLineVsplit)`) 272 | * d - Delete buffer or saved sql (`(DBUI_DeleteLine)`) 273 | * R - Redraw (`(DBUI_Redraw)`) 274 | * A - Add connection (`(DBUI_AddConnection)`) 275 | * H - Toggle database details (`(DBUI_ToggleDetails)`) 276 | 277 | For queries, filetype is automatically set to `sql`. Also, two mappings is added for the `sql` filetype: 278 | 279 | * \W - Permanently save query for later use (`(DBUI_SaveQuery)`) 280 | * \E - Edit bind parameters (`(DBUI_EditBindParameters)`) 281 | 282 | Any of these mappings can be overridden: 283 | ```vimL 284 | autocmd FileType dbui nmap v (DBUI_SelectLineVsplit) 285 | ``` 286 | 287 | If you don't want mappings to be added, add this to vimrc: 288 | ```vimL 289 | let g:db_ui_disable_mappings = 1 " Disable all mappings 290 | let g:db_ui_disable_mappings_dbui = 1 " Disable mappings in DBUI drawer 291 | let g:db_ui_disable_mappings_dbout = 1 " Disable mappings in DB output 292 | let g:db_ui_disable_mappings_sql = 1 " Disable mappings in SQL buffers 293 | let g:db_ui_disable_mappings_javascript = 1 " Disable mappings in Javascript buffers (for Mongodb) 294 | ``` 295 | 296 | ## Toggle showing postgres views in the drawer 297 | If you don't want to see any views in the drawer, add this to vimrc: 298 | This option must be disabled (set to 0) for Redshift. 299 | 300 | ```vimL 301 | let g:db_ui_use_postgres_views = 0 302 | ``` 303 | 304 | ## Disable builtin progress bar 305 | If you want to utilize *DBExecutePre or *DBExecutePost to make your own progress bar 306 | or if you want to disable the progress entirely set to 1. 307 | 308 | ```vimL 309 | let g:db_ui_disable_progress_bar = 1 310 | ``` 311 | 312 | ## TODO 313 | 314 | * [ ] Test with more db types 315 | -------------------------------------------------------------------------------- /autoload/db_ui.vim: -------------------------------------------------------------------------------- 1 | let s:dbui_instance = {} 2 | let s:dbui = {} 3 | 4 | function! db_ui#open(mods) abort 5 | call s:init() 6 | return s:dbui_instance.drawer.open(a:mods) 7 | endfunction 8 | 9 | function! db_ui#toggle() abort 10 | call s:init() 11 | return s:dbui_instance.drawer.toggle() 12 | endfunction 13 | 14 | function! db_ui#close() abort 15 | call s:init() 16 | return s:dbui_instance.drawer.quit() 17 | endfunction 18 | 19 | function! db_ui#save_dbout(file) abort 20 | call s:init() 21 | return s:dbui_instance.save_dbout(a:file) 22 | endfunction 23 | 24 | function! db_ui#connections_list() abort 25 | call s:init() 26 | return map(copy(s:dbui_instance.dbs_list), {_,v-> { 27 | \ 'name': v.name, 28 | \ 'url': v.url, 29 | \ 'is_connected': !empty(s:dbui_instance.dbs[v.key_name].conn), 30 | \ 'source': v.source, 31 | \ }}) 32 | endfunction 33 | 34 | function! db_ui#find_buffer() abort 35 | call s:init() 36 | if !len(s:dbui_instance.dbs_list) 37 | return db_ui#notifications#error('No database entries found in DBUI.') 38 | endif 39 | 40 | if !exists('b:dbui_db_key_name') 41 | let saved_query_db = s:dbui_instance.drawer.get_query().get_saved_query_db_name() 42 | let db = s:get_db(saved_query_db) 43 | if empty(db) 44 | return db_ui#notifications#error('No database entries selected or found.') 45 | endif 46 | call s:dbui_instance.connect(db) 47 | call db_ui#notifications#info('Assigned buffer to db '.db.name, {'delay': 10000 }) 48 | let b:dbui_db_key_name = db.key_name 49 | let b:db = db.conn 50 | endif 51 | 52 | if !exists('b:dbui_db_key_name') 53 | return db_ui#notifications#error('Unable to find in DBUI. Not a valid dbui query buffer.') 54 | endif 55 | 56 | let db = b:dbui_db_key_name 57 | let bufname = bufname('%') 58 | 59 | call s:dbui_instance.drawer.get_query().setup_buffer(s:dbui_instance.dbs[db], { 'existing_buffer': 1 }, bufname, 0) 60 | if exists('*vim_dadbod_completion#fetch') 61 | call vim_dadbod_completion#fetch(bufnr('')) 62 | endif 63 | let s:dbui_instance.dbs[db].expanded = 1 64 | let s:dbui_instance.dbs[db].buffers.expanded = 1 65 | call s:dbui_instance.drawer.open() 66 | let row = 1 67 | for line in s:dbui_instance.drawer.content 68 | if line.dbui_db_key_name ==? db && line.type ==? 'buffer' && line.file_path ==? bufname 69 | break 70 | endif 71 | let row += 1 72 | endfor 73 | call cursor(row, 0) 74 | call s:dbui_instance.drawer.render({ 'db_key_name': db, 'queries': 1 }) 75 | wincmd p 76 | endfunction 77 | 78 | function! db_ui#rename_buffer() abort 79 | call s:init() 80 | return s:dbui_instance.drawer.rename_buffer(bufname('%'), get(b:, 'dbui_db_key_name'), 0) 81 | endfunction 82 | 83 | function! db_ui#get_conn_info(db_key_name) abort 84 | if empty(s:dbui_instance) 85 | return {} 86 | endif 87 | if !has_key(s:dbui_instance.dbs, a:db_key_name) 88 | return {} 89 | endif 90 | let db = s:dbui_instance.dbs[a:db_key_name] 91 | call s:dbui_instance.connect(db) 92 | return { 93 | \ 'url': db.url, 94 | \ 'conn': db.conn, 95 | \ 'tables': db.tables.list, 96 | \ 'schemas': db.schemas.list, 97 | \ 'scheme': db.scheme, 98 | \ 'connected': !empty(db.conn), 99 | \ } 100 | endfunction 101 | 102 | function! db_ui#query(query) abort 103 | if empty(b:db) 104 | throw 'Cannot find valid connection for a buffer.' 105 | endif 106 | 107 | let parsed = db#url#parse(b:db) 108 | let scheme = db_ui#schemas#get(parsed.scheme) 109 | if empty(scheme) 110 | throw 'Unsupported scheme '.parsed.scheme 111 | endif 112 | 113 | let result = db_ui#schemas#query(b:db, scheme, a:query) 114 | 115 | return scheme.parse_results(result, 0) 116 | endfunction 117 | 118 | function! db_ui#print_last_query_info() abort 119 | call s:init() 120 | let info = s:dbui_instance.drawer.get_query().get_last_query_info() 121 | if empty(info.last_query) 122 | return db_ui#notifications#info('No queries ran.') 123 | endif 124 | 125 | let content = ['Last query:'] + info.last_query 126 | let content += ['' + 'Time: '.info.last_query_time.' sec.'] 127 | 128 | return db_ui#notifications#info(content, {'echo': 1}) 129 | endfunction 130 | 131 | function! db_ui#statusline(...) 132 | let db_key_name = get(b:, 'dbui_db_key_name', '') 133 | let dbout = get(b:, 'db', '') 134 | if empty(s:dbui_instance) || (&filetype !=? 'dbout' && empty(db_key_name)) 135 | return '' 136 | end 137 | if &filetype ==? 'dbout' 138 | let last_query_info = s:dbui_instance.drawer.get_query().get_last_query_info() 139 | let last_query_time = last_query_info.last_query_time 140 | if !empty(last_query_time) 141 | return 'Last query time: '.last_query_time.' sec.' 142 | endif 143 | return '' 144 | endif 145 | let opts = get(a:, 1, {}) 146 | let prefix = get(opts, 'prefix', 'DBUI: ') 147 | let separator = get(opts, 'separator', ' -> ') 148 | let show = get(opts, 'show', ['db_name', 'schema', 'table']) 149 | let db_table = get(b:, 'dbui_table_name', '') 150 | let db_schema = get(b:, 'dbui_schema_name', '') 151 | let db = s:dbui_instance.dbs[db_key_name] 152 | let data = { 'db_name': db.name, 'schema': db_schema, 'table': db_table } 153 | let content = [] 154 | for item in show 155 | let entry = get(data, item, '') 156 | if !empty(entry) 157 | call add(content, entry) 158 | endif 159 | endfor 160 | return prefix.join(content, separator) 161 | endfunction 162 | 163 | function! s:dbui.new() abort 164 | let instance = copy(self) 165 | let instance.dbs = {} 166 | let instance.dbs_list = [] 167 | let instance.save_path = '' 168 | let instance.connections_path = '' 169 | let instance.tmp_location = '' 170 | let instance.drawer = {} 171 | let instance.old_buffers = [] 172 | let instance.dbout_list = {} 173 | 174 | if !empty(g:db_ui_save_location) 175 | let instance.save_path = substitute(fnamemodify(g:db_ui_save_location, ':p'), '\/$', '', '') 176 | let instance.connections_path = printf('%s/%s', instance.save_path, 'connections.json') 177 | endif 178 | 179 | if !empty(g:db_ui_tmp_query_location) 180 | let tmp_loc = substitute(fnamemodify(g:db_ui_tmp_query_location, ':p'), '\/$', '', '') 181 | if !isdirectory(tmp_loc) 182 | call mkdir(tmp_loc, 'p') 183 | endif 184 | let instance.tmp_location = tmp_loc 185 | let instance.old_buffers = glob(tmp_loc.'/*', 1, 1) 186 | endif 187 | 188 | call instance.populate_dbs() 189 | let instance.drawer = db_ui#drawer#new(instance) 190 | return instance 191 | endfunction 192 | 193 | function! s:dbui.save_dbout(file) abort 194 | let db_input = '' 195 | let content = '' 196 | if has_key(self.dbout_list, a:file) && !empty(self.dbout_list[a:file]) 197 | return 198 | endif 199 | let db_input = get(getbufvar(a:file, 'db', {}), 'input') 200 | if !empty(db_input) && filereadable(db_input) 201 | let content = get(readfile(db_input, 1), 0) 202 | if len(content) > 30 203 | let content = printf('%s...', content[0:30]) 204 | endif 205 | endif 206 | let self.dbout_list[a:file] = content 207 | call self.drawer.render() 208 | endfunction 209 | 210 | function! s:dbui.populate_dbs() abort 211 | let self.dbs_list = [] 212 | call self.populate_from_dotenv() 213 | call self.populate_from_env() 214 | call self.populate_from_global_variable() 215 | call self.populate_from_connections_file() 216 | 217 | for db in self.dbs_list 218 | let key_name = printf('%s_%s', db.name, db.source) 219 | if !has_key(self.dbs, key_name) || db.url !=? self.dbs[key_name].url 220 | let new_entry = self.generate_new_db_entry(db) 221 | if !empty(new_entry) 222 | let self.dbs[key_name] = new_entry 223 | endif 224 | else 225 | let self.dbs[key_name] = self.drawer.populate(self.dbs[key_name]) 226 | endif 227 | endfor 228 | endfunction 229 | 230 | function! s:dbui.generate_new_db_entry(db) abort 231 | let parsed_url = self.parse_url(a:db.url) 232 | if empty(parsed_url) 233 | return parsed_url 234 | endif 235 | let db_name = substitute(get(parsed_url, 'path', ''), '^\/', '', '') 236 | let save_path = '' 237 | if !empty(self.save_path) 238 | let save_path = printf('%s/%s', self.save_path, a:db.name) 239 | endif 240 | let buffers = filter(copy(self.old_buffers), 'fnamemodify(v:val, ":e") =~? "^".a:db.name."-" || fnamemodify(v:val, ":t") =~? "^".a:db.name."-"') 241 | 242 | let db = { 243 | \ 'url': a:db.url, 244 | \ 'conn': '', 245 | \ 'conn_error': '', 246 | \ 'conn_tried': 0, 247 | \ 'source': a:db.source, 248 | \ 'scheme': '', 249 | \ 'table_helpers': {}, 250 | \ 'expanded': 0, 251 | \ 'tables': {'expanded': 0 , 'items': {}, 'list': [] }, 252 | \ 'schemas': {'expanded': 0, 'items': {}, 'list': [] }, 253 | \ 'saved_queries': { 'expanded': 0, 'list': [] }, 254 | \ 'buffers': { 'expanded': 0, 'list': buffers, 'tmp': [] }, 255 | \ 'save_path': save_path, 256 | \ 'db_name': !empty(db_name) ? db_name : a:db.name, 257 | \ 'name': a:db.name, 258 | \ 'key_name': printf('%s_%s', a:db.name, a:db.source), 259 | \ 'schema_support': 0, 260 | \ 'quote': 0, 261 | \ 'default_scheme': '', 262 | \ 'filetype': '' 263 | \ } 264 | 265 | call self.populate_schema_info(db) 266 | return db 267 | endfunction 268 | 269 | function! s:dbui.resolve_url_global_variable(Value) abort 270 | if type(a:Value) ==? type('') 271 | return a:Value 272 | endif 273 | 274 | if type(a:Value) ==? type(function('tr')) 275 | return call(a:Value, []) 276 | endif 277 | 278 | " if type(a:Value) ==? type(v:t_func) 279 | " endif 280 | " 281 | " echom string(type(a:Value)) 282 | " echom string(a:Value) 283 | " 284 | throw 'Invalid type global variable database url:'..type(a:Value) 285 | endfunction 286 | 287 | function! s:dbui.populate_from_global_variable() abort 288 | if exists('g:db') && !empty(g:db) 289 | let url = self.resolve_url_global_variable(g:db) 290 | let gdb_name = split(url, '/')[-1] 291 | call self.add_if_not_exists(gdb_name, url, 'g:dbs') 292 | endif 293 | 294 | if !exists('g:dbs') || empty(g:dbs) 295 | return self 296 | endif 297 | 298 | if type(g:dbs) ==? type({}) 299 | for [db_name, Db_url] in items(g:dbs) 300 | call self.add_if_not_exists(db_name, self.resolve_url_global_variable(Db_url), 'g:dbs') 301 | endfor 302 | return self 303 | endif 304 | 305 | for db in g:dbs 306 | call self.add_if_not_exists(db.name, self.resolve_url_global_variable(db.url), 'g:dbs') 307 | endfor 308 | 309 | return self 310 | endfunction 311 | 312 | function! s:dbui.populate_from_dotenv() abort 313 | let prefix = g:db_ui_dotenv_variable_prefix 314 | let all_envs = {} 315 | if exists('*environ') 316 | let all_envs = environ() 317 | else 318 | for item in systemlist('env') 319 | let env = split(item, '=') 320 | if len(env) > 1 321 | let all_envs[env[0]] = join(env[1:], '') 322 | endif 323 | endfor 324 | endif 325 | let all_envs = extend(all_envs, exists('*DotenvGet') ? DotenvGet() : {}) 326 | for [name, url] in items(all_envs) 327 | if stridx(name, prefix) != -1 328 | let db_name = tolower(join(split(name, prefix))) 329 | call self.add_if_not_exists(db_name, url, 'dotenv') 330 | endif 331 | endfor 332 | endfunction 333 | 334 | function! s:dbui.env(var) abort 335 | return exists('*DotenvGet') ? DotenvGet(a:var) : eval('$'.a:var) 336 | endfunction 337 | 338 | function! s:dbui.populate_from_env() abort 339 | let env_url = self.env(g:db_ui_env_variable_url) 340 | if empty(env_url) 341 | return self 342 | endif 343 | let env_name = self.env(g:db_ui_env_variable_name) 344 | if empty(env_name) 345 | let env_name = get(split(env_url, '/'), -1, '') 346 | endif 347 | 348 | if empty(env_name) 349 | return db_ui#notifications#error( 350 | \ printf('Found %s variable for db url, but unable to parse the name. Please provide name via %s', g:db_ui_env_variable_url, g:db_ui_env_variable_name)) 351 | endif 352 | 353 | call self.add_if_not_exists(env_name, env_url, 'env') 354 | return self 355 | endfunction 356 | 357 | function! s:dbui.parse_url(url) abort 358 | try 359 | return db#url#parse(a:url) 360 | catch /.*/ 361 | call db_ui#notifications#error(v:exception) 362 | return {} 363 | endtry 364 | endfunction 365 | 366 | function! s:dbui.populate_from_connections_file() abort 367 | if empty(self.connections_path) || !filereadable(self.connections_path) 368 | return 369 | endif 370 | 371 | let file = db_ui#utils#readfile(self.connections_path) 372 | 373 | for conn in file 374 | call self.add_if_not_exists(conn.name, conn.url, 'file') 375 | endfor 376 | 377 | return self 378 | endfunction 379 | 380 | function! s:dbui.add_if_not_exists(name, url, source) abort 381 | let existing = get(filter(copy(self.dbs_list), 'v:val.name ==? a:name && v:val.source ==? a:source'), 0, {}) 382 | if !empty(existing) 383 | return db_ui#notifications#warning(printf('Warning: Duplicate connection name "%s" in "%s" source. First one added has precedence.', a:name, a:source)) 384 | endif 385 | return add(self.dbs_list, { 386 | \ 'name': a:name, 'url': db_ui#resolve(a:url), 'source': a:source, 'key_name': printf('%s_%s', a:name, a:source) 387 | \ }) 388 | endfunction 389 | 390 | function! s:dbui.is_tmp_location_buffer(db, buf) abort 391 | if index(a:db.buffers.tmp, a:buf) > -1 392 | return 1 393 | endif 394 | return !empty(self.tmp_location) && a:buf =~? '^'.self.tmp_location 395 | endfunction 396 | 397 | function! s:dbui.connect(db) abort 398 | if !empty(a:db.conn) 399 | return a:db 400 | endif 401 | 402 | try 403 | let query_time = reltime() 404 | call db_ui#notifications#info('Connecting to db '.a:db.name.'...') 405 | let a:db.conn = db#connect(a:db.url) 406 | let a:db.conn_error = '' 407 | call self.populate_schema_info(a:db) 408 | call db_ui#notifications#info('Connected to db '.a:db.name.' after '.split(reltimestr(reltime(query_time)))[0].' sec.') 409 | catch /.*/ 410 | let a:db.conn_error = v:exception 411 | let a:db.conn = '' 412 | call db_ui#notifications#error('Error connecting to db '.a:db.name.': '.v:exception, {'width': 80 }) 413 | endtry 414 | 415 | redraw! 416 | let a:db.conn_tried = 1 417 | return a:db 418 | endfunction 419 | 420 | function! s:dbui.populate_schema_info(db) abort 421 | let url = !empty(a:db.conn) ? a:db.conn : a:db.url 422 | let parsed_url = self.parse_url(url) 423 | let scheme = get(parsed_url, 'scheme', '') 424 | let scheme_info = db_ui#schemas#get(scheme) 425 | let a:db.scheme = scheme 426 | let a:db.table_helpers = db_ui#table_helpers#get(scheme) 427 | let a:db.schema_support = db_ui#schemas#supports_schemes(scheme_info, parsed_url) 428 | let a:db.quote = get(scheme_info, 'quote', 0) 429 | let a:db.default_scheme = get(scheme_info, 'default_scheme', '') 430 | let a:db.filetype = get(scheme_info, 'filetype', db#adapter#call(url, 'input_extension', [], 'sql')) 431 | " Properly map mongodb js to javascript 432 | if a:db.filetype ==? 'js' 433 | let a:db.filetype = 'javascript' 434 | endif 435 | endfunction 436 | 437 | " Resolve only urls for DBs that are files 438 | function db_ui#resolve(url) abort 439 | let parsed_url = db#url#parse(a:url) 440 | let resolve_schemes = ['sqlite', 'jq', 'duckdb', 'osquery'] 441 | 442 | if index(resolve_schemes, get(parsed_url, 'scheme', '')) > -1 443 | return db#resolve(a:url) 444 | endif 445 | 446 | return a:url 447 | endfunction 448 | 449 | function! db_ui#reset_state() abort 450 | let s:dbui_instance = {} 451 | endfunction 452 | 453 | function! s:init() abort 454 | if empty(s:dbui_instance) 455 | let s:dbui_instance = s:dbui.new() 456 | endif 457 | 458 | return s:dbui_instance 459 | endfunction 460 | 461 | function! s:get_db(saved_query_db) abort 462 | if !len(s:dbui_instance.dbs_list) 463 | return {} 464 | endif 465 | 466 | if !empty(a:saved_query_db) 467 | let saved_db = get(filter(copy(s:dbui_instance.dbs_list), 'v:val.name ==? a:saved_query_db'), 0, {}) 468 | if empty(saved_db) 469 | return {} 470 | endif 471 | return s:dbui_instance.dbs[saved_db.key_name] 472 | endif 473 | 474 | if len(s:dbui_instance.dbs_list) ==? 1 475 | return values(s:dbui_instance.dbs)[0] 476 | endif 477 | 478 | let options = map(copy(s:dbui_instance.dbs_list), '(v:key + 1).") ".v:val.name') 479 | let selection = db_ui#utils#inputlist(['Select db to assign this buffer to:'] + options) 480 | if selection < 1 || selection > len(options) 481 | call db_ui#notifications#error('Wrong selection.') 482 | return {} 483 | endif 484 | let selected_db = s:dbui_instance.dbs_list[selection - 1] 485 | let selected_db = s:dbui_instance.dbs[selected_db.key_name] 486 | return selected_db 487 | endfunction 488 | -------------------------------------------------------------------------------- /autoload/db_ui/connections.vim: -------------------------------------------------------------------------------- 1 | let s:connections_instance = {} 2 | let s:connections = {} 3 | 4 | function! db_ui#connections#new(drawer) 5 | let s:connections_instance = s:connections.new(a:drawer) 6 | return s:connections_instance 7 | endfunction 8 | 9 | function! db_ui#connections#add() abort 10 | if empty(s:connections_instance) 11 | let s:connections_instance = s:connections.new({}) 12 | endif 13 | 14 | return s:connections_instance.add() 15 | endfunction 16 | 17 | function! s:connections.new(drawer) abort 18 | let instance = copy(self) 19 | let instance.drawer = a:drawer 20 | let s:connections_instance = instance 21 | 22 | return s:connections_instance 23 | endfunction 24 | 25 | function! s:connections.add() abort 26 | if empty(g:db_ui_save_location) 27 | return db_ui#notifications#error('Please set up valid save location via g:db_ui_save_location') 28 | endif 29 | 30 | return self.add_full_url() 31 | endfunction 32 | 33 | function! s:connections.delete(db) abort 34 | let confirm_delete = confirm(printf('Are you sure you want to delete connection %s?', a:db.name), "&Yes\n&No\n&Cancel") 35 | if confirm_delete !=? 1 36 | return 37 | endif 38 | 39 | let file = self.read() 40 | call filter(file, {i, conn -> !(conn.name ==? a:db.name && db_ui#resolve(conn.url) ==? db_ui#resolve(a:db.url) )}) 41 | return self.write(file) 42 | endfunction 43 | 44 | function! s:connections.add_full_url() abort 45 | let url = '' 46 | 47 | try 48 | let url = db_ui#resolve(db_ui#utils#input('Enter connection url: ', url)) 49 | call db#url#parse(url) 50 | " Attempt to resolve to check if it's valid url 51 | call db#resolve(url) 52 | catch /.*/ 53 | return db_ui#notifications#error(v:exception) 54 | endtry 55 | 56 | try 57 | let name = self.enter_db_name(url) 58 | catch /.*/ 59 | return db_ui#notifications#error(v:exception) 60 | endtry 61 | 62 | return self.save(name, url) 63 | endfunction 64 | 65 | function! s:connections.rename(db) abort 66 | if a:db.source !=? 'file' 67 | return db_ui#notifications#error('Cannot edit connections added via variables.') 68 | endif 69 | 70 | let connections = copy(self.read()) 71 | let idx = 0 72 | let entry = {} 73 | for conn in connections 74 | if conn.name ==? a:db.name && db_ui#resolve(conn.url) ==? a:db.url 75 | let entry = conn 76 | break 77 | endif 78 | let idx += 1 79 | endfor 80 | 81 | let url = entry.url 82 | try 83 | let url = db_ui#resolve(db_ui#utils#input('Edit connection url for "'.entry.name.'": ', url)) 84 | call db#url#parse(url) 85 | " Attempt to resolve to check if it's valid url 86 | call db#resolve(url) 87 | catch /.*/ 88 | return db_ui#notifications#error(v:exception) 89 | endtry 90 | 91 | let name = '' 92 | 93 | try 94 | let name = db_ui#utils#input('Edit connection name: ', entry.name) 95 | if empty(trim(name)) 96 | throw 'Please enter valid name.' 97 | endif 98 | catch /.*/ 99 | return db_ui#notifications#error(v:exception) 100 | endtry 101 | 102 | call remove(connections, idx) 103 | let connections = insert(connections, {'name': name, 'url': url }, idx) 104 | return self.write(connections) 105 | endfunction 106 | 107 | function! s:connections.enter_db_name(url) abort 108 | let name = db_ui#utils#input('Enter name: ', split(a:url, '/')[-1]) 109 | 110 | if empty(trim(name)) 111 | throw 'Please enter valid name.' 112 | endif 113 | 114 | return name 115 | endfunction 116 | 117 | function! s:connections.get_file() abort 118 | let save_folder = substitute(fnamemodify(g:db_ui_save_location, ':p'), '\/$', '', '') 119 | return printf('%s/%s', save_folder, 'connections.json') 120 | endfunction 121 | 122 | function s:connections.save(name, url) abort 123 | let file = self.get_file() 124 | let dir = fnamemodify(file, ':p:h') 125 | 126 | if !isdirectory(dir) 127 | call mkdir(dir, 'p') 128 | endif 129 | 130 | if !filereadable(file) 131 | call writefile(['[]'], file) 132 | endif 133 | 134 | let file = self.read() 135 | let existing_connection = filter(copy(file), 'v:val.name ==? a:name') 136 | if !empty(existing_connection) 137 | call db_ui#notifications#error('Connection with that name already exists. Please enter different name.') 138 | return 0 139 | endif 140 | call add(file, {'name': a:name, 'url': a:url}) 141 | return self.write(file) 142 | endfunction 143 | 144 | function! s:connections.read() abort 145 | return db_ui#utils#readfile(self.get_file()) 146 | endfunction 147 | 148 | function! s:connections.write(file) abort 149 | call writefile([json_encode(a:file)], self.get_file()) 150 | if !empty(self.drawer) 151 | call self.drawer.render({ 'dbs': 1 }) 152 | endif 153 | return 1 154 | endfunction 155 | -------------------------------------------------------------------------------- /autoload/db_ui/dbout.vim: -------------------------------------------------------------------------------- 1 | function! db_ui#dbout#jump_to_foreign_table() abort 2 | let db_url = b:db.db_url 3 | let parsed = db#url#parse(db_url) 4 | let scheme = db_ui#schemas#get(parsed.scheme) 5 | if empty(scheme) 6 | return db_ui#notifications#error(parsed.scheme.' scheme not supported for foreign key jump.') 7 | endif 8 | 9 | let cell_line_number = s:get_cell_line_number(scheme) 10 | let cell_range = s:get_cell_range(cell_line_number, getcurpos(), scheme) 11 | 12 | let virtual_cell_range = get(cell_range, 'virtual', cell_range) 13 | let field_name = trim(getline(cell_line_number - 1)[virtual_cell_range.from : virtual_cell_range.to]) 14 | 15 | let field_value = trim(getline('.')[cell_range.from : cell_range.to]) 16 | 17 | let foreign_key_query = substitute(scheme.foreign_key_query, '{col_name}', field_name, '') 18 | let Parser = get(scheme, 'parse_virtual_results', scheme.parse_results) 19 | let result = Parser(db_ui#schemas#query(db_url, scheme, foreign_key_query), 3) 20 | 21 | if empty(result) 22 | return db_ui#notifications#error('No valid foreign key found.') 23 | endif 24 | 25 | let [foreign_table_name, foreign_column_name,foreign_table_schema] = result[0] 26 | let query = printf(scheme.select_foreign_key_query, foreign_table_schema, foreign_table_name, foreign_column_name, db_ui#utils#quote_query_value(field_value)) 27 | 28 | exe 'DB '.query 29 | endfunction 30 | 31 | function! db_ui#dbout#foldexpr(lnum) abort 32 | if getline(a:lnum) !~? '^[[:blank:]]*$' 33 | " Mysql 34 | if getline(a:lnum) =~? '^+---' && getline(a:lnum + 2) =~? '^+---' 35 | return '>1' 36 | endif 37 | " Postgres & Sqlserver 38 | if getline(a:lnum + 1) =~? '^----' 39 | return '>1' 40 | endif 41 | return 1 42 | endif 43 | 44 | "Postgres & Sqlserver 45 | if getline(a:lnum) =~? '^[[:blank:]]*$' 46 | if getline(a:lnum + 2) !~? '^----' 47 | return 1 48 | endif 49 | return 0 50 | endif 51 | 52 | return -1 53 | endfunction 54 | 55 | function! db_ui#dbout#get_cell_value() abort 56 | let parsed = db#url#parse(db_ui#resolve(b:db)) 57 | let scheme = db_ui#schemas#get(parsed.scheme) 58 | if empty(scheme) 59 | return db_ui#notifications#error('Yanking cell value not supported for '.parsed.scheme.' scheme.') 60 | endif 61 | 62 | let cell_line_number = s:get_cell_line_number(scheme) 63 | let cell_range = s:get_cell_range(cell_line_number, getcurpos(), scheme) 64 | let field_value = getline('.')[(cell_range.from):(cell_range.to)] 65 | let start_spaces = len(matchstr(field_value, '^[[:blank:]]*')) 66 | let end_spaces = len(matchstr(field_value, '[[:blank:]]*$')) 67 | let old_selection = &selection 68 | set selection=inclusive 69 | let from = cell_range.from + start_spaces + 1 70 | let to = cell_range.to - end_spaces + 1 71 | call cursor(line('.'), from) 72 | let motion = max([(to - from), 0]) 73 | let cmd = 'normal!v' 74 | if motion > 0 75 | let cmd .= motion.'l' 76 | endif 77 | exe cmd 78 | let &selection = old_selection 79 | endfunction 80 | 81 | function! db_ui#dbout#toggle_layout() abort 82 | let parsed = db#url#parse(db_ui#resolve(b:db)) 83 | let scheme = db_ui#schemas#get(parsed.scheme) 84 | if !has_key(scheme, 'layout_flag') 85 | return db_ui#notifications#error('Toggling layout not supported for '.parsed.scheme.' scheme.') 86 | endif 87 | let content = join(readfile(b:db.input), "\n") 88 | let expanded_layout = get(b:, 'db_ui_expanded_layout', 0) 89 | 90 | if expanded_layout 91 | let b:db_ui_expanded_layout = !expanded_layout 92 | norm R 93 | return 94 | endif 95 | 96 | let content = substitute(content, ';\?$', ' '.scheme.layout_flag, '') 97 | let tmp = tempname() 98 | call writefile(split(content, "\n"), tmp) 99 | let old_db_input = b:db.input 100 | let b:db.input = tmp 101 | norm R 102 | let b:db.input = old_db_input 103 | let b:db_ui_expanded_layout = !expanded_layout 104 | endfunction 105 | 106 | function! db_ui#dbout#yank_header() abort 107 | let parsed = db#url#parse(db_ui#resolve(b:db)) 108 | let scheme = db_ui#schemas#get(parsed.scheme) 109 | if empty(scheme) 110 | return db_ui#notifications#error('Yanking headers not supported for '.parsed.scheme.' scheme.') 111 | endif 112 | 113 | let cell_line_number = s:get_cell_line_number(scheme) 114 | let table_line = '-' 115 | let column_line = getline(cell_line_number-1) 116 | let underline = getline(cell_line_number) 117 | let from = 0 118 | let to = 0 119 | let i = 0 120 | let columns=[] 121 | let lastcol = strlen(underline) 122 | while i <= lastcol 123 | if underline[i] !=? table_line || i == lastcol 124 | let to = i-1 125 | call add(columns, trim(column_line[from:to])) 126 | let from = i+1 127 | endif 128 | let i += 1 129 | endwhile 130 | let csv_columns = join(columns, ', ') 131 | call setreg(v:register, csv_columns) 132 | endfunction 133 | 134 | function! s:get_cell_range(cell_line_number, curpos, scheme) abort 135 | if get(a:scheme, 'has_virtual_results', v:false) 136 | return s:get_virtual_cell_range(a:cell_line_number, a:curpos) 137 | endif 138 | 139 | let line = getline(a:cell_line_number) 140 | let table_line = '-' 141 | 142 | let col = a:curpos[2] - 1 143 | let from = 0 144 | 145 | while col >= 0 && line[col] ==? table_line 146 | let from = col 147 | let col -= 1 148 | endwhile 149 | 150 | let col = a:curpos[2] - 1 151 | let to = 0 152 | 153 | while col <= len(line) && line[col] ==? table_line 154 | let to = col 155 | let col += 1 156 | endwhile 157 | 158 | return {'from': from, 'to': to} 159 | endfunction 160 | 161 | function! s:get_virtual_cell_range(cell_line_number, curpos) abort 162 | let line = getline(a:cell_line_number) 163 | let position = a:curpos[1:] 164 | let table_line = '-' 165 | 166 | let col = position[-1] - 1 167 | let virtual_from = 0 168 | 169 | while col >= 0 && line[col] ==? table_line 170 | let virtual_from = col 171 | let col -= 1 172 | endwhile 173 | 174 | let col = position[-1] - 1 175 | let virtual_to = 0 176 | 177 | while col <= len(line) && line[col] ==? table_line 178 | let virtual_to = col 179 | let col += 1 180 | endwhile 181 | 182 | let position_above = insert(position[1:], position[0] - 1) 183 | 184 | call cursor(add(position_above[:-2], virtual_from)) 185 | norm! j 186 | let from = col('.') 187 | 188 | call cursor(add(position_above[:-2], virtual_to)) 189 | norm! j 190 | let to = col('.') 191 | 192 | call cursor(position) 193 | 194 | " NOTE: 'virtual' refers to a position in reference to virtcol(). 195 | " other fields reference col() 196 | return { 197 | \ 'from': max([from, 0]), 198 | \ 'to': max([to, 0]), 199 | \ 'virtual': {'from': max([virtual_from, 0]), 'to': max([virtual_to, 0])} 200 | \} 201 | endfunction 202 | 203 | function! s:get_cell_line_number(scheme) abort 204 | let line = line('.') 205 | 206 | while (line > a:scheme.cell_line_number) 207 | if getline(line) =~? a:scheme.cell_line_pattern 208 | return line 209 | endif 210 | let line -= 1 211 | endwhile 212 | 213 | return a:scheme.cell_line_number 214 | endfunction 215 | 216 | let s:progress_icons = ['/', '—', '\', '|'] 217 | let s:progress_buffers = {} 218 | let s:progress = { 219 | \ 'win': -1, 220 | \ 'outwin': -1, 221 | \ 'buf': -1, 222 | \ 'timer': -1, 223 | \ 'counter': 0, 224 | \ 'icon_counter': 0, 225 | \ } 226 | 227 | function! s:progress_tick(progress, timer) abort 228 | let a:progress.counter += 100 229 | if a:progress.icon_counter > 3 230 | let a:progress.icon_counter = 0 231 | endif 232 | let secs = string(a:progress.counter * 0.001).'s' 233 | let content = ' '.s:progress_icons[a:progress.icon_counter].' Execute query - '.secs 234 | if has('nvim') 235 | call nvim_buf_set_lines(a:progress.buf, 0, -1, v:false, [content]) 236 | else 237 | call popup_settext(a:progress.win, content) 238 | endif 239 | let a:progress.icon_counter += 1 240 | endfunction 241 | 242 | function! s:progress_winpos(win) 243 | let pos = win_screenpos(a:win) 244 | return [ 245 | \ pos[0] + (winheight(a:win) / 2), 246 | \ pos[1] + (winwidth(a:win) / 2) - 12, 247 | \ ] 248 | endfunction 249 | 250 | function! s:progress_hide(...) abort 251 | let bufname = a:0 > 0 ? a:1 : bufname() 252 | let progress = get(s:progress_buffers, bufname, {}) 253 | if empty(progress) 254 | return 255 | endif 256 | if has('nvim') 257 | silent! call nvim_win_close(progress.win, v:true) 258 | else 259 | silent! call popup_close(progress.win) 260 | endif 261 | silent! call timer_stop(progress.timer) 262 | unlet! s:progress_buffers[bufname] 263 | call s:progress_reset_positions() 264 | endfunction 265 | 266 | function! s:progress_reset_positions() 267 | for bprogress in values(s:progress_buffers) 268 | let win = bprogress.win 269 | let [row, col] = s:progress_winpos(bprogress.outwin) 270 | if has('nvim') 271 | call nvim_win_set_config(win, { 'relative': 'editor', 'row': row - 2, 'col': col }) 272 | else 273 | call popup_move(win, { 'line': row, 'col': col }) 274 | endif 275 | endfor 276 | endfunction 277 | 278 | function! s:progress_show_neovim(path) abort 279 | let bufname = !empty(a:path) ? a:path : bufname() 280 | let outwin = win_getid(bufwinnr(bufname)) 281 | let progress = copy(s:progress) 282 | let progress.outwin = outwin 283 | let progress.buf = nvim_create_buf(v:false, v:true) 284 | call nvim_buf_set_lines(progress.buf, 0, -1, v:false, ['| Execute query - 0.0s']) 285 | let [row, col] = s:progress_winpos(outwin) 286 | let opts = { 287 | \ 'relative': 'editor', 288 | \ 'width': 24, 289 | \ 'height': 1, 290 | \ 'row': row - 2, 291 | \ 'col': col, 292 | \ 'focusable': v:false, 293 | \ 'style': 'minimal' 294 | \ } 295 | 296 | if has('nvim-0.5') 297 | let opts.border = 'rounded' 298 | endif 299 | let progress.win = nvim_open_win(progress.buf, v:false, opts) 300 | let progress.timer = timer_start(100, function('s:progress_tick', [progress]), { 'repeat': -1 }) 301 | let s:progress_buffers[bufname] = progress 302 | endfunction 303 | 304 | function! s:progress_show_vim(path) 305 | let bufname = !empty(a:path) ? a:path : bufname() 306 | let outwin = bufwinnr(bufname) 307 | let pos = win_screenpos(outwin) 308 | let progress = copy(s:progress) 309 | let progress.outwin = outwin 310 | let [row, col] = s:progress_winpos(outwin) 311 | let progress.win = popup_create('| Execute query - 0.0s', { 312 | \ 'line': row, 313 | \ 'col': col, 314 | \ 'minwidth': 24, 315 | \ 'maxwidth': 24, 316 | \ 'minheight': 1, 317 | \ 'maxheight': 1, 318 | \ 'border': [], 319 | \ }) 320 | let progress.timer = timer_start(100, function('s:progress_tick', [progress]), { 'repeat': -1 }) 321 | let s:progress_buffers[bufname] = progress 322 | endfunction 323 | 324 | function! s:progress_show(...) 325 | if has('nvim') 326 | call s:progress_show_neovim(get(a:, 1, '')) 327 | else 328 | call s:progress_show_vim(get(a:, 1, '')) 329 | endif 330 | call s:progress_reset_positions() 331 | endfunction 332 | 333 | 334 | if exists('*nvim_open_win') || exists('*popup_create') 335 | if empty(g:db_ui_disable_progress_bar) 336 | augroup dbui_async_queries_dbout 337 | autocmd! 338 | autocmd User DBQueryPre call s:progress_show() 339 | autocmd User DBQueryPost call s:progress_hide() 340 | autocmd User *DBExecutePre call s:progress_show(expand(':h')) 341 | autocmd User *DBExecutePost call s:progress_hide(expand(':h')) 342 | augroup END 343 | endif 344 | endif 345 | -------------------------------------------------------------------------------- /autoload/db_ui/drawer.vim: -------------------------------------------------------------------------------- 1 | let s:drawer_instance = {} 2 | let s:drawer = {} 3 | 4 | function db_ui#drawer#new(dbui) 5 | let s:drawer_instance = s:drawer.new(a:dbui) 6 | return s:drawer_instance 7 | endfunction 8 | function db_ui#drawer#get() 9 | return s:drawer_instance 10 | endfunction 11 | 12 | function! s:drawer.new(dbui) abort 13 | let instance = copy(self) 14 | let instance.dbui = a:dbui 15 | let instance.show_details = 0 16 | let instance.show_help = 0 17 | let instance.show_dbout_list = 0 18 | let instance.content = [] 19 | let instance.query = {} 20 | let instance.connections = {} 21 | 22 | return instance 23 | endfunction 24 | 25 | function! s:drawer.open(...) abort 26 | if self.is_opened() 27 | silent! exe self.get_winnr().'wincmd w' 28 | return 29 | endif 30 | let mods = get(a:, 1, '') 31 | if !empty(mods) 32 | silent! exe mods.' new dbui' 33 | else 34 | let win_pos = g:db_ui_win_position ==? 'left' ? 'topleft' : 'botright' 35 | silent! exe 'vertical '.win_pos.' new dbui' 36 | silent! exe 'vertical '.win_pos.' resize '.g:db_ui_winwidth 37 | endif 38 | setlocal filetype=dbui buftype=nofile bufhidden=wipe nobuflisted nolist noswapfile nowrap nospell nomodifiable winfixwidth nonumber norelativenumber signcolumn=no 39 | 40 | call self.render() 41 | nnoremap (DBUI_SelectLine) :call method('toggle_line', 'edit') 42 | nnoremap (DBUI_DeleteLine) :call method('delete_line') 43 | let query_win_pos = g:db_ui_win_position ==? 'left' ? 'botright' : 'topleft' 44 | silent! exe "nnoremap (DBUI_SelectLineVsplit) :call method('toggle_line', 'vertical ".query_win_pos." split')" 45 | nnoremap (DBUI_Redraw) :call method('redraw') 46 | nnoremap (DBUI_AddConnection) :call method('add_connection') 47 | nnoremap (DBUI_ToggleDetails) :call method('toggle_details') 48 | nnoremap (DBUI_RenameLine) :call method('rename_line') 49 | nnoremap (DBUI_Quit) :call method('quit') 50 | 51 | nnoremap (DBUI_GotoFirstSibling) :call method('goto_sibling', 'first') 52 | nnoremap (DBUI_GotoNextSibling) :call method('goto_sibling', 'next') 53 | nnoremap (DBUI_GotoPrevSibling) :call method('goto_sibling', 'prev') 54 | nnoremap (DBUI_GotoLastSibling) :call method('goto_sibling', 'last') 55 | nnoremap (DBUI_GotoParentNode) :call method('goto_node', 'parent') 56 | nnoremap (DBUI_GotoChildNode) :call method('goto_node', 'child') 57 | 58 | nnoremap ? :call method('toggle_help') 59 | augroup db_ui 60 | autocmd! * 61 | autocmd BufEnter call s:method('render') 62 | augroup END 63 | silent! doautocmd User DBUIOpened 64 | endfunction 65 | 66 | function! s:drawer.is_opened() abort 67 | return self.get_winnr() > -1 68 | endfunction 69 | 70 | function! s:drawer.get_winnr() abort 71 | for nr in range(1, winnr('$')) 72 | if getwinvar(nr, '&filetype') ==? 'dbui' 73 | return nr 74 | endif 75 | endfor 76 | return -1 77 | endfunction 78 | 79 | function! s:drawer.redraw() abort 80 | let item = self.get_current_item() 81 | if item.level ==? 0 82 | return self.render({ 'dbs': 1, 'queries': 1 }) 83 | endif 84 | return self.render({'db_key_name': item.dbui_db_key_name, 'queries': 1 }) 85 | endfunction 86 | 87 | function! s:drawer.toggle() abort 88 | if self.is_opened() 89 | return self.quit() 90 | endif 91 | return self.open() 92 | endfunction 93 | 94 | function! s:drawer.quit() abort 95 | if self.is_opened() 96 | silent! exe 'bd'.winbufnr(self.get_winnr()) 97 | endif 98 | endfunction 99 | 100 | function! s:method(method_name, ...) abort 101 | if a:0 > 0 102 | return s:drawer_instance[a:method_name](a:1) 103 | endif 104 | 105 | return s:drawer_instance[a:method_name]() 106 | endfunction 107 | 108 | function! s:drawer.goto_sibling(direction) 109 | let index = line('.') - 1 110 | let last_index = len(self.content) - 1 111 | let item = self.content[index] 112 | let current_level = item.level 113 | let is_up = a:direction ==? 'first' || a:direction ==? 'prev' 114 | let is_down = !is_up 115 | let is_edge = a:direction ==? 'first' || a:direction ==? 'last' 116 | let is_prev_or_next = !is_edge 117 | let last_index_same_level = index 118 | 119 | while ((is_up && index >= 0) || (is_down && index < last_index)) 120 | let adjacent_index = is_up ? index - 1 : index + 1 121 | let is_on_edge = (is_up && adjacent_index ==? 0) || (is_down && adjacent_index ==? last_index) 122 | let adjacent_item = self.content[adjacent_index] 123 | if adjacent_item.level ==? 0 && adjacent_item.label ==? '' 124 | return cursor(index + 1, col('.')) 125 | endif 126 | 127 | if is_prev_or_next 128 | if adjacent_item.level ==? current_level 129 | return cursor(adjacent_index + 1, col('.')) 130 | endif 131 | if adjacent_item.level < current_level 132 | return 133 | endif 134 | endif 135 | 136 | if is_edge 137 | if adjacent_item.level ==? current_level 138 | let last_index_same_level = adjacent_index 139 | endif 140 | if adjacent_item.level < current_level || is_on_edge 141 | return cursor(last_index_same_level + 1, col('.')) 142 | endif 143 | endif 144 | let index = adjacent_index 145 | endwhile 146 | endfunction 147 | 148 | function! s:drawer.goto_node(direction) 149 | let index = line('.') - 1 150 | let item = self.content[index] 151 | let last_index = len(self.content) - 1 152 | let is_up = a:direction ==? 'parent' 153 | let is_down = !is_up 154 | let Is_correct_level = {adj-> a:direction ==? 'parent' ? adj.level ==? item.level - 1 : adj.level ==? item.level + 1} 155 | if is_up 156 | while index >= 0 157 | let index = index - 1 158 | let adjacent_item = self.content[index] 159 | if adjacent_item.level < item.level 160 | break 161 | endif 162 | endwhile 163 | return cursor(index + 1, col('.')) 164 | endif 165 | 166 | if item.action !=? 'toggle' 167 | return 168 | endif 169 | 170 | if !item.expanded 171 | call self.toggle_line('') 172 | endif 173 | norm! j 174 | endfunction 175 | 176 | function s:drawer.get_current_item() abort 177 | return self.content[line('.') - 1] 178 | endfunction 179 | 180 | function! s:drawer.rename_buffer(buffer, db_key_name, is_saved_query) abort 181 | let bufnr = bufnr(a:buffer) 182 | let current_win = winnr() 183 | let current_ft = &filetype 184 | 185 | if !filereadable(a:buffer) 186 | return db_ui#notifications#error('Only written queries can be renamed.') 187 | endif 188 | 189 | if empty(a:db_key_name) 190 | return db_ui#notifications#error('Buffer not attached to any database') 191 | endif 192 | 193 | let bufwin = bufwinnr(bufnr) 194 | let db = self.dbui.dbs[a:db_key_name] 195 | let db_slug = db_ui#utils#slug(db.name) 196 | let is_saved = a:is_saved_query || !self.dbui.is_tmp_location_buffer(db, a:buffer) 197 | let old_name = self.get_buffer_name(db, a:buffer) 198 | 199 | try 200 | let new_name = db_ui#utils#input('Enter new name: ', old_name) 201 | catch /.*/ 202 | return db_ui#notifications#error(v:exception) 203 | endtry 204 | 205 | if empty(new_name) 206 | return db_ui#notifications#error('Valid name must be provided.') 207 | endif 208 | 209 | if is_saved 210 | let new = printf('%s/%s', fnamemodify(a:buffer, ':p:h'), new_name) 211 | else 212 | let new = printf('%s/%s', fnamemodify(a:buffer, ':p:h'), db_slug.'-'.new_name) 213 | call add(db.buffers.tmp, new) 214 | endif 215 | 216 | call rename(a:buffer, new) 217 | let new_bufnr = -1 218 | 219 | if bufwin > -1 220 | call self.get_query().open_buffer(db, new, 'edit') 221 | let new_bufnr = bufnr('%') 222 | elseif bufnr > -1 223 | exe 'badd '.new 224 | let new_bufnr = bufnr(new) 225 | call add(db.buffers.list, new) 226 | elseif index(db.buffers.list, a:buffer) > -1 227 | call insert(db.buffers.list, new, index(db.buffers.list, a:buffer)) 228 | endif 229 | 230 | call filter(db.buffers.list, 'v:val !=? a:buffer') 231 | 232 | if new_bufnr > - 1 233 | call setbufvar(new_bufnr, 'dbui_db_key_name', db.key_name) 234 | call setbufvar(new_bufnr, 'db', db.conn) 235 | call setbufvar(new_bufnr, 'dbui_db_table_name', getbufvar(a:buffer, 'dbui_db_table_name')) 236 | call setbufvar(new_bufnr, 'dbui_bind_params', getbufvar(a:buffer, 'dbui_bind_params')) 237 | endif 238 | 239 | silent! exe 'bw! '.a:buffer 240 | if winnr() !=? current_win 241 | wincmd p 242 | endif 243 | 244 | return self.render({ 'queries': 1 }) 245 | endfunction 246 | 247 | function! s:drawer.rename_line() abort 248 | let item = self.get_current_item() 249 | if item.type ==? 'buffer' 250 | return self.rename_buffer(item.file_path, item.dbui_db_key_name, get(item, 'saved', 0)) 251 | endif 252 | 253 | if item.type ==? 'db' 254 | return self.get_connections().rename(self.dbui.dbs[item.dbui_db_key_name]) 255 | endif 256 | 257 | return 258 | endfunction 259 | 260 | function! s:drawer.add_connection() abort 261 | return self.get_connections().add() 262 | endfunction 263 | 264 | function! s:drawer.toggle_dbout_queries() abort 265 | let self.show_dbout_list = !self.show_dbout_list 266 | return self.render() 267 | endfunction 268 | 269 | function! s:drawer.delete_connection(db) abort 270 | return self.get_connections().delete(a:db) 271 | endfunction 272 | 273 | function! s:drawer.get_connections() abort 274 | if empty(self.connections) 275 | let self.connections = db_ui#connections#new(self) 276 | endif 277 | 278 | return self.connections 279 | endfunction 280 | 281 | function! s:drawer.toggle_help() abort 282 | let self.show_help = !self.show_help 283 | return self.render() 284 | endfunction 285 | 286 | function! s:drawer.toggle_details() abort 287 | let self.show_details = !self.show_details 288 | return self.render() 289 | endfunction 290 | 291 | function! s:drawer.focus() abort 292 | if &filetype ==? 'dbui' 293 | return 0 294 | endif 295 | 296 | let winnr = self.get_winnr() 297 | if winnr > -1 298 | exe winnr.'wincmd w' 299 | return 1 300 | endif 301 | return 0 302 | endfunction 303 | 304 | function! s:drawer.render(...) abort 305 | let opts = get(a:, 1, {}) 306 | let restore_win = self.focus() 307 | 308 | if &filetype !=? 'dbui' 309 | return 310 | endif 311 | 312 | if get(opts, 'dbs', 0) 313 | let query_time = reltime() 314 | call db_ui#notifications#info('Refreshing all databases...') 315 | call self.dbui.populate_dbs() 316 | call db_ui#notifications#info('Refreshed all databases after '.split(reltimestr(reltime(query_time)))[0].' sec.') 317 | endif 318 | 319 | if !empty(get(opts, 'db_key_name', '')) 320 | let db = self.dbui.dbs[opts.db_key_name] 321 | call db_ui#notifications#info('Refreshing database '.db.name.'...') 322 | let query_time = reltime() 323 | let self.dbui.dbs[opts.db_key_name] = self.populate(db) 324 | call db_ui#notifications#info('Refreshed database '.db.name.' after '.split(reltimestr(reltime(query_time)))[0].' sec.') 325 | endif 326 | 327 | redraw! 328 | let view = winsaveview() 329 | let self.content = [] 330 | 331 | call self.render_help() 332 | 333 | for db in self.dbui.dbs_list 334 | if get(opts, 'queries', 0) 335 | call self.load_saved_queries(self.dbui.dbs[db.key_name]) 336 | endif 337 | call self.add_db(self.dbui.dbs[db.key_name]) 338 | endfor 339 | 340 | if empty(self.dbui.dbs_list) 341 | call self.add('" No connections', 'noaction', 'help', '', '', 0) 342 | call self.add('Add connection', 'call_method', 'add_connection', g:db_ui_icons.add_connection, '', 0) 343 | endif 344 | 345 | 346 | if !empty(self.dbui.dbout_list) 347 | call self.add('', 'noaction', 'help', '', '', 0) 348 | call self.add('Query results ('.len(self.dbui.dbout_list).')', 'call_method', 'toggle_dbout_queries', self.get_toggle_icon('saved_queries', {'expanded': self.show_dbout_list}), '', 0) 349 | 350 | if self.show_dbout_list 351 | let entries = sort(keys(self.dbui.dbout_list), function('s:sort_dbout')) 352 | for entry in entries 353 | let content = '' 354 | if !empty(self.dbui.dbout_list[entry]) 355 | let content = printf(' (%s)', self.dbui.dbout_list[entry].content) 356 | endif 357 | call self.add(fnamemodify(entry, ':t').content, 'open', 'dbout', g:db_ui_icons.tables, '', 1, { 'file_path': entry }) 358 | endfor 359 | endif 360 | endif 361 | 362 | let content = map(copy(self.content), 'repeat(" ", shiftwidth() * v:val.level).v:val.icon.(!empty(v:val.icon) ? " " : "").v:val.label') 363 | 364 | setlocal modifiable 365 | silent 1,$delete _ 366 | call setline(1, content) 367 | setlocal nomodifiable 368 | call winrestview(view) 369 | 370 | if restore_win 371 | wincmd p 372 | endif 373 | endfunction 374 | 375 | function! s:drawer.render_help() abort 376 | if g:db_ui_show_help 377 | call self.add('" Press ? for help', 'noaction', 'help', '', '', 0) 378 | call self.add('', 'noaction', 'help', '', '', 0) 379 | endif 380 | 381 | if self.show_help 382 | call self.add('" o - Open/Toggle selected item', 'noaction', 'help', '', '', 0) 383 | call self.add('" S - Open/Toggle selected item in vertical split', 'noaction', 'help', '', '', 0) 384 | call self.add('" d - Delete selected item', 'noaction', 'help', '', '', 0) 385 | call self.add('" R - Redraw', 'noaction', 'help', '', '', 0) 386 | call self.add('" A - Add connection', 'noaction', 'help', '', '', 0) 387 | call self.add('" H - Toggle database details', 'noaction', 'help', '', '', 0) 388 | call self.add('" r - Rename/Edit buffer/connection/saved query', 'noaction', 'help', '', '', 0) 389 | call self.add('" q - Close drawer', 'noaction', 'help', '', '', 0) 390 | call self.add('" / - Go to last/first sibling', 'noaction', 'help', '', '', 0) 391 | call self.add('" K/J - Go to prev/next sibling', 'noaction', 'help', '', '', 0) 392 | call self.add('" / - Go to parent/child node', 'noaction', 'help', '', '', 0) 393 | call self.add('" W - (sql) Save currently opened query', 'noaction', 'help', '', '', 0) 394 | call self.add('" E - (sql) Edit bind parameters in opened query', 'noaction', 'help', '', '', 0) 395 | call self.add('" S - (sql) Execute query in visual or normal mode', 'noaction', 'help', '', '', 0) 396 | call self.add('" - (.dbout) Go to entry from foreign key cell', 'noaction', 'help', '', '', 0) 397 | call self.add('" ic - (.dbout) Operator pending mapping for cell value', 'noaction', 'help', '', '', 0) 398 | call self.add('" R - (.dbout) Toggle expanded view', 'noaction', 'help', '', '', 0) 399 | call self.add('', 'noaction', 'help', '', '', 0) 400 | endif 401 | endfunction 402 | 403 | function! s:drawer.add(label, action, type, icon, dbui_db_key_name, level, ...) 404 | let opts = extend({'label': a:label, 'action': a:action, 'type': a:type, 'icon': a:icon, 'dbui_db_key_name': a:dbui_db_key_name, 'level': a:level }, get(a:, '1', {})) 405 | call add(self.content, opts) 406 | endfunction 407 | 408 | function! s:drawer.add_db(db) abort 409 | let db_name = a:db.name 410 | 411 | if !empty(a:db.conn_error) 412 | let db_name .= ' '.g:db_ui_icons.connection_error 413 | elseif !empty(a:db.conn) 414 | let db_name .= ' '.g:db_ui_icons.connection_ok 415 | endif 416 | 417 | if self.show_details 418 | let db_name .= ' ('.a:db.scheme.' - '.a:db.source.')' 419 | endif 420 | 421 | call self.add(db_name, 'toggle', 'db', self.get_toggle_icon('db', a:db), a:db.key_name, 0, { 'expanded': a:db.expanded }) 422 | if !a:db.expanded 423 | return a:db 424 | endif 425 | 426 | " Render sections based on g:db_ui_drawer_sections configuration 427 | for section in g:db_ui_drawer_sections 428 | if section ==# 'new_query' 429 | call self._render_new_query_section(a:db) 430 | elseif section ==# 'buffers' && !empty(a:db.buffers.list) 431 | call self._render_buffers_section(a:db) 432 | elseif section ==# 'saved_queries' 433 | call self._render_saved_queries_section(a:db) 434 | elseif section ==# 'schemas' 435 | call self._render_schemas_section(a:db) 436 | endif 437 | endfor 438 | endfunction 439 | 440 | function! s:drawer.render_tables(tables, db, path, level, schema) abort 441 | if !a:tables.expanded 442 | return 443 | endif 444 | if type(g:Db_ui_table_name_sorter) ==? type(function('tr')) 445 | let tables_list = call(g:Db_ui_table_name_sorter, [a:tables.list]) 446 | else 447 | let tables_list = a:tables.list 448 | endif 449 | for table in tables_list 450 | call self.add(table, 'toggle', a:path.'->'.table, self.get_toggle_icon('table', a:tables.items[table]), a:db.key_name, a:level, { 'expanded': a:tables.items[table].expanded }) 451 | if a:tables.items[table].expanded 452 | for [helper_name, helper] in items(a:db.table_helpers) 453 | call self.add(helper_name, 'open', 'table', g:db_ui_icons.tables, a:db.key_name, a:level + 1, {'table': table, 'content': helper, 'schema': a:schema }) 454 | endfor 455 | endif 456 | endfor 457 | endfunction 458 | 459 | function! s:drawer.toggle_line(edit_action) abort 460 | let item = self.get_current_item() 461 | if item.action ==? 'noaction' 462 | return 463 | endif 464 | 465 | if item.action ==? 'call_method' 466 | return s:method(item.type) 467 | endif 468 | 469 | if item.type ==? 'dbout' 470 | call self.get_query().focus_window() 471 | silent! exe 'pedit' item.file_path 472 | return 473 | endif 474 | 475 | if item.action ==? 'open' 476 | return self.get_query().open(item, a:edit_action) 477 | endif 478 | 479 | let db = self.dbui.dbs[item.dbui_db_key_name] 480 | 481 | let tree = db 482 | if item.type !=? 'db' 483 | let tree = self.get_nested(db, item.type) 484 | endif 485 | 486 | let tree.expanded = !tree.expanded 487 | 488 | if item.type ==? 'db' 489 | call self.toggle_db(db) 490 | endif 491 | 492 | return self.render() 493 | endfunction 494 | 495 | function! s:drawer.get_query() abort 496 | if empty(self.query) 497 | let self.query = db_ui#query#new(self) 498 | endif 499 | return self.query 500 | endfunction 501 | 502 | function! s:drawer.delete_line() abort 503 | let item = self.get_current_item() 504 | 505 | if item.action ==? 'noaction' 506 | return 507 | endif 508 | 509 | if item.action ==? 'toggle' && item.type ==? 'db' 510 | let db = self.dbui.dbs[item.dbui_db_key_name] 511 | if db.source !=? 'file' 512 | return db_ui#notifications#error('Cannot delete this connection.') 513 | endif 514 | return self.delete_connection(db) 515 | endif 516 | 517 | if item.action !=? 'open' || item.type !=? 'buffer' 518 | return 519 | endif 520 | 521 | let db = self.dbui.dbs[item.dbui_db_key_name] 522 | 523 | if has_key(item, 'saved') 524 | let choice = confirm('Are you sure you want to delete this saved query?', "&Yes\n&No") 525 | if choice !=? 1 526 | return 527 | endif 528 | 529 | call delete(item.file_path) 530 | call remove(db.saved_queries.list, index(db.saved_queries.list, item.file_path)) 531 | call filter(db.buffers.list, 'v:val !=? item.file_path') 532 | call db_ui#notifications#info('Deleted.') 533 | endif 534 | 535 | if self.dbui.is_tmp_location_buffer(db, item.file_path) 536 | let choice = confirm('Are you sure you want to delete query?', "&Yes\n&No") 537 | if choice !=? 1 538 | return 539 | endif 540 | 541 | call delete(item.file_path) 542 | call filter(db.buffers.list, 'v:val !=? item.file_path') 543 | call db_ui#notifications#info('Deleted.') 544 | endif 545 | 546 | let win = bufwinnr(item.file_path) 547 | if win > -1 548 | silent! exe win.'wincmd w' 549 | silent! exe 'b#' 550 | endif 551 | 552 | silent! exe 'bw!'.bufnr(item.file_path) 553 | call self.focus() 554 | call self.render() 555 | endfunction 556 | 557 | function! s:drawer.toggle_db(db) abort 558 | if !a:db.expanded 559 | return a:db 560 | endif 561 | 562 | call self.load_saved_queries(a:db) 563 | 564 | call self.dbui.connect(a:db) 565 | 566 | if !empty(a:db.conn) 567 | call self.populate(a:db) 568 | endif 569 | endfunction 570 | 571 | function! s:drawer.populate(db) abort 572 | if empty(a:db.conn) && a:db.conn_tried 573 | call self.dbui.connect(a:db) 574 | endif 575 | if a:db.schema_support 576 | return self.populate_schemas(a:db) 577 | endif 578 | return self.populate_tables(a:db) 579 | endfunction 580 | 581 | function! s:drawer.load_saved_queries(db) abort 582 | if !empty(a:db.save_path) 583 | let a:db.saved_queries.list = split(glob(printf('%s/*', a:db.save_path)), "\n") 584 | endif 585 | endfunction 586 | 587 | function! s:drawer.populate_tables(db) abort 588 | let a:db.tables.list = [] 589 | if empty(a:db.conn) 590 | return a:db 591 | endif 592 | 593 | let tables = db#adapter#call(a:db.conn, 'tables', [a:db.conn], []) 594 | 595 | let a:db.tables.list = tables 596 | " Fix issue with sqlite tables listing as strings with spaces 597 | if a:db.scheme =~? '^sqlite' && len(a:db.tables.list) >=? 0 598 | let temp_table_list = [] 599 | 600 | for table_index in a:db.tables.list 601 | let temp_table_list += map(split(copy(table_index)), 'trim(v:val)') 602 | endfor 603 | 604 | let a:db.tables.list = sort(temp_table_list) 605 | endif 606 | 607 | if a:db.scheme =~? '^mysql' 608 | call filter(a:db.tables.list, 'v:val !~? "mysql: [Warning\\]" && v:val !~? "Tables_in_"') 609 | endif 610 | 611 | call self.populate_table_items(a:db.tables) 612 | return a:db 613 | endfunction 614 | 615 | function! s:drawer.populate_table_items(tables) abort 616 | for table in a:tables.list 617 | if !has_key(a:tables.items, table) 618 | let a:tables.items[table] = {'expanded': 0 } 619 | endif 620 | endfor 621 | endfunction 622 | 623 | function! s:drawer.populate_schemas(db) abort 624 | let a:db.schemas.list = [] 625 | if empty(a:db.conn) 626 | return a:db 627 | endif 628 | let scheme = db_ui#schemas#get(a:db.scheme) 629 | let schemas = scheme.parse_results(db_ui#schemas#query(a:db, scheme, scheme.schemes_query), 1) 630 | let tables = scheme.parse_results(db_ui#schemas#query(a:db, scheme, scheme.schemes_tables_query), 2) 631 | let schemas = filter(schemas, {i, v -> !self._is_schema_ignored(v)}) 632 | let tables_by_schema = {} 633 | for [scheme_name, table] in tables 634 | if self._is_schema_ignored(scheme_name) 635 | continue 636 | endif 637 | if !has_key(tables_by_schema, scheme_name) 638 | let tables_by_schema[scheme_name] = [] 639 | endif 640 | call add(tables_by_schema[scheme_name], table) 641 | call add(a:db.tables.list, table) 642 | endfor 643 | let a:db.schemas.list = schemas 644 | for schema in schemas 645 | if !has_key(a:db.schemas.items, schema) 646 | let a:db.schemas.items[schema] = { 647 | \ 'expanded': 0, 648 | \ 'tables': { 649 | \ 'expanded': 1, 650 | \ 'list': [], 651 | \ 'items': {}, 652 | \ }, 653 | \ } 654 | 655 | endif 656 | let a:db.schemas.items[schema].tables.list = sort(get(tables_by_schema, schema, [])) 657 | call self.populate_table_items(a:db.schemas.items[schema].tables) 658 | endfor 659 | return a:db 660 | endfunction 661 | 662 | function! s:drawer.get_toggle_icon(type, item) abort 663 | if a:item.expanded 664 | return g:db_ui_icons.expanded[a:type] 665 | endif 666 | 667 | return g:db_ui_icons.collapsed[a:type] 668 | endfunction 669 | 670 | function! s:drawer.get_nested(obj, val, ...) abort 671 | let default = get(a:, '1', 0) 672 | let items = split(a:val, '->') 673 | let result = copy(a:obj) 674 | 675 | for item in items 676 | if !has_key(result, item) 677 | let result = default 678 | break 679 | endif 680 | let result = result[item] 681 | endfor 682 | 683 | return result 684 | endfunction 685 | 686 | function! s:drawer.get_buffer_name(db, buffer) 687 | let name = fnamemodify(a:buffer, ':t') 688 | let is_tmp = self.dbui.is_tmp_location_buffer(a:db, a:buffer) 689 | 690 | if !is_tmp 691 | return name 692 | endif 693 | 694 | if fnamemodify(name, ':r') ==? 'db_ui' 695 | let name = fnamemodify(name, ':e') 696 | endif 697 | 698 | return substitute(name, '^'.db_ui#utils#slug(a:db.name).'-', '', '') 699 | endfunction 700 | 701 | function! s:drawer._render_new_query_section(db) abort 702 | call self.add('New query', 'open', 'query', g:db_ui_icons.new_query, a:db.key_name, 1) 703 | endfunction 704 | 705 | function! s:drawer._render_buffers_section(db) abort 706 | call self.add('Buffers ('.len(a:db.buffers.list).')', 'toggle', 'buffers', self.get_toggle_icon('buffers', a:db.buffers), a:db.key_name, 1, { 'expanded': a:db.buffers.expanded }) 707 | if a:db.buffers.expanded 708 | for buf in a:db.buffers.list 709 | let buflabel = self.get_buffer_name(a:db, buf) 710 | if self.dbui.is_tmp_location_buffer(a:db, buf) 711 | let buflabel .= ' *' 712 | endif 713 | call self.add(buflabel, 'open', 'buffer', g:db_ui_icons.buffers, a:db.key_name, 2, { 'file_path': buf }) 714 | endfor 715 | endif 716 | endfunction 717 | 718 | function! s:drawer._render_saved_queries_section(db) abort 719 | call self.add('Saved queries ('.len(a:db.saved_queries.list).')', 'toggle', 'saved_queries', self.get_toggle_icon('saved_queries', a:db.saved_queries), a:db.key_name, 1, { 'expanded': a:db.saved_queries.expanded }) 720 | if a:db.saved_queries.expanded 721 | for saved_query in a:db.saved_queries.list 722 | call self.add(fnamemodify(saved_query, ':t'), 'open', 'buffer', g:db_ui_icons.saved_query, a:db.key_name, 2, { 'file_path': saved_query, 'saved': 1 }) 723 | endfor 724 | endif 725 | endfunction 726 | 727 | function! s:drawer._render_schemas_section(db) abort 728 | if a:db.schema_support 729 | call self.add('Schemas ('.len(a:db.schemas.items).')', 'toggle', 'schemas', self.get_toggle_icon('schemas', a:db.schemas), a:db.key_name, 1, { 'expanded': a:db.schemas.expanded }) 730 | if a:db.schemas.expanded 731 | for schema in a:db.schemas.list 732 | let schema_item = a:db.schemas.items[schema] 733 | let tables = schema_item.tables 734 | call self.add(schema.' ('.len(tables.items).')', 'toggle', 'schemas->items->'.schema, self.get_toggle_icon('schema', schema_item), a:db.key_name, 2, { 'expanded': schema_item.expanded }) 735 | if schema_item.expanded 736 | call self.render_tables(tables, a:db,'schemas->items->'.schema.'->tables->items', 3, schema) 737 | endif 738 | endfor 739 | endif 740 | else 741 | call self.add('Tables ('.len(a:db.tables.items).')', 'toggle', 'tables', self.get_toggle_icon('tables', a:db.tables), a:db.key_name, 1, { 'expanded': a:db.tables.expanded }) 742 | call self.render_tables(a:db.tables, a:db, 'tables->items', 2, '') 743 | endif 744 | endfunction 745 | 746 | function! s:drawer._is_schema_ignored(schema_name) 747 | for ignored_schema in g:db_ui_hide_schemas 748 | if match(a:schema_name, ignored_schema) > -1 749 | return 1 750 | endif 751 | endfor 752 | return 0 753 | endfunction 754 | 755 | function! s:sort_dbout(a1, a2) 756 | return str2nr(fnamemodify(a:a1, ':t:r')) - str2nr(fnamemodify(a:a2, ':t:r')) 757 | endfunction 758 | -------------------------------------------------------------------------------- /autoload/db_ui/notifications.vim: -------------------------------------------------------------------------------- 1 | " Author: Kristijan Husak 2 | " Github: http://github.com/kristijanhusak 3 | " Original: http://github.com/kristijanhusak/vim-simple-notifications 4 | " LICENSE: MIT 5 | 6 | " Default options, overrideable via second argument to functions 7 | let s:delay = 7000 "Hide after this number of milliseconds 8 | let s:width = g:db_ui_notification_width "Default notification width 9 | let s:pos = 'bot'.g:db_ui_win_position "Default position for notification 10 | let s:title = '[DBUI]' "Title of notification 11 | let s:last_msg = '' 12 | let s:use_nvim_notify = g:db_ui_use_nvim_notify 13 | 14 | let s:colors_set = 0 15 | 16 | if s:use_nvim_notify && !has('nvim') 17 | echoerr "Option db_ui_use_nvim_notify is supported only in neovim" 18 | endif 19 | 20 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 21 | " Public API, adapt names to your needs " 22 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 23 | function! db_ui#notifications#info(msg, ...) abort 24 | return s:notification(a:msg, get(a:, 1, {})) 25 | endfunction 26 | 27 | function! db_ui#notifications#error(msg, ...) abort 28 | return s:notification(a:msg, extend({'type': 'error'}, get(a:, 1, {}))) 29 | endfunction 30 | 31 | function! db_ui#notifications#warning(msg, ...) abort 32 | return s:notification(a:msg, extend({'type': 'warning'}, get(a:, 1, {}))) 33 | endfunction 34 | 35 | function! db_ui#notifications#get_last_msg() abort 36 | if type(s:last_msg) ==? type([]) 37 | return join(s:last_msg) 38 | endif 39 | return s:last_msg 40 | endfunction 41 | 42 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 43 | " Implementation " 44 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 45 | let s:win = -1 46 | let s:timer = -1 47 | let s:neovim_float = has('nvim') && exists('*nvim_open_win') 48 | let s:vim_popup = exists('*popup_create') 49 | 50 | function! s:notification(msg, opts) abort 51 | if empty(a:msg) 52 | return 53 | endif 54 | 55 | let type = get(a:opts, 'type', 'info') 56 | 57 | if type ==? 'info' && g:db_ui_disable_info_notifications 58 | return 59 | endif 60 | 61 | let use_echo = get(a:opts, 'echo', 0) 62 | if !use_echo 63 | let use_echo = g:db_ui_force_echo_notifications 64 | endif 65 | 66 | if s:use_nvim_notify && !use_echo 67 | return s:notification_nvim_notify(a:msg, a:opts) 68 | endif 69 | 70 | if !s:colors_set 71 | call s:setup_colors() 72 | let s:colors_set = 1 73 | endif 74 | 75 | if s:neovim_float && !use_echo 76 | return s:notification_nvim(a:msg, a:opts) 77 | endif 78 | 79 | if s:vim_popup && !use_echo 80 | return s:notification_vim(a:msg, a:opts) 81 | endif 82 | 83 | return s:notification_echo(a:msg, a:opts) 84 | endfunction 85 | 86 | let s:hl_by_type = { 87 | \ 'info': 'NotificationInfo', 88 | \ 'error': 'NotificationError', 89 | \ 'warning': 'NotificationWarning', 90 | \ } 91 | 92 | function! s:nvim_close() abort 93 | silent! call nvim_win_close(s:win, v:true) 94 | silent! call timer_stop(s:timer) 95 | endfunction 96 | 97 | function! s:notification_nvim_notify(msg, opts) abort 98 | let type = get(a:opts, 'type', 'info') 99 | let title = get(a:opts, 'title', s:title) 100 | let opts = { 'title': title } 101 | if get(a:opts, 'delay') 102 | let opts.timeout = { 'timeout': a:opts.delay } 103 | endif 104 | if (type ==? 'info') 105 | let opts.id = 'vim-dadbod-ui-info' 106 | endif 107 | 108 | let log_levels = { 109 | \ 'info': luaeval("vim.log.levels.INFO"), 110 | \ 'error': luaeval("vim.log.levels.ERROR"), 111 | \ 'warning': luaeval("vim.log.levels.WARN") 112 | \ } 113 | 114 | return luaeval('vim.notify(_A[1], _A[2], _A[3])', [a:msg, log_levels[type], opts]) 115 | endfunction 116 | 117 | function! s:notification_nvim(msg, opts) abort 118 | let width = get(a:opts, 'width', s:width) 119 | let title = get(a:opts, 'title', s:title) 120 | let msg = type(a:msg) !=? type([]) ? [a:msg] : a:msg 121 | if !empty(title) 122 | let msg = [title] + msg 123 | endif 124 | 125 | let height = 0 126 | for line in msg 127 | let height += len(split(line,'.\{'.width.'}\zs')) 128 | endfor 129 | let delay = get(a:opts, 'delay', s:delay) 130 | let type = get(a:opts, 'type', 'info') 131 | let pos = get(a:opts, 'pos', s:pos) 132 | let pos_map = {'topleft': 'NW', 'topright': 'NE', 'botleft': 'SW', 'botright': 'SE', 'top': 'NW', 'bottom': 'SW'} 133 | 134 | let pos_opts = s:get_pos(pos, width) 135 | let pos_opts.anchor = pos_map[pos] 136 | let opts = extend(pos_opts, { 137 | \ 'relative': 'editor', 138 | \ 'width': width, 139 | \ 'height': height, 140 | \ 'style': 'minimal', 141 | \ }) 142 | 143 | call s:nvim_close() 144 | let buf = nvim_create_buf(v:false, v:true) 145 | call nvim_buf_set_lines(buf, 0, -1, v:false, msg) 146 | 147 | let s:win = nvim_open_win(buf, v:false, opts) 148 | silent! exe 'autocmd BufEnter :bw!' 149 | call nvim_win_set_option(s:win, 'wrap', v:true) 150 | call nvim_win_set_option(s:win, 'signcolumn', 'yes') "simulate left padding 151 | call nvim_win_set_option(s:win, 'winhl', 'Normal:'.s:hl_by_type[type]) 152 | let s:timer = timer_start(delay, {-> s:nvim_close()}) 153 | let s:last_msg = a:msg 154 | endfunction 155 | 156 | function! s:notification_vim(msg, opts) abort 157 | let width = get(a:opts, 'width', s:width) 158 | let delay = get(a:opts, 'delay', s:delay) 159 | let type = get(a:opts, 'type', 'info') 160 | let pos = get(a:opts, 'pos', s:pos) 161 | let title = get(a:opts, 'title', s:title) 162 | let pos_opts = s:get_pos(pos, width) 163 | let pos_opts.line = pos_opts.row 164 | unlet! pos_opts.row 165 | let pos_map = {'top': 'topleft', 'bottom': 'botleft'} 166 | let pos = has_key(pos_map, pos) ? pos_map[pos] : pos 167 | let opts = extend(pos_opts, { 168 | \ 'pos': pos, 169 | \ 'minwidth': width, 170 | \ 'maxwidth': width, 171 | \ 'time': delay, 172 | \ 'close': 'click', 173 | \ 'title': title, 174 | \ 'padding': [0, 0, 0, 1], 175 | \ }) 176 | 177 | let opts.highlight = s:hl_by_type[type] 178 | call popup_hide(s:win) 179 | let s:win = popup_create(a:msg, opts) 180 | let s:last_msg = a:msg 181 | endfunction 182 | 183 | function! s:notification_echo(msg, opts) abort 184 | let type = get(a:opts, 'type', 'info') 185 | let title = get(a:opts, 'title', s:title) 186 | silent! exe 'echohl Echo'.s:hl_by_type[type] 187 | redraw! 188 | let title = !empty(title) ? title.' ' : '' 189 | if type(a:msg) ==? type('') 190 | echom title.a:msg 191 | elseif type(a:msg) !=? type([]) 192 | echom title.string(a:msg) 193 | else 194 | echom title.a:msg[0] 195 | for msg in a:msg[1:] 196 | echom msg 197 | endfor 198 | endif 199 | echohl None 200 | let s:last_msg = a:msg 201 | endfunction 202 | 203 | function! s:setup_colors() abort 204 | let warning_fg = '' 205 | let warning_bg = '' 206 | let error_fg = '' 207 | let error_bg = '' 208 | let normal_fg = '' 209 | let normal_bg = '' 210 | let warning_bg = synIDattr(hlID('WarningMsg'), 'bg') 211 | let warning_fg = synIDattr(hlID('WarningMsg'), 'fg') 212 | if empty(warning_bg) 213 | let warning_bg = warning_fg 214 | let warning_fg = '#FFFFFF' 215 | endif 216 | 217 | let error_bg = synIDattr(hlID('Error'), 'bg') 218 | let error_fg = synIDattr(hlID('Error'), 'fg') 219 | if empty(error_bg) 220 | let error_bg = error_fg 221 | let error_fg = '#FFFFFF' 222 | endif 223 | 224 | let normal_bg = synIDattr(hlID('Normal'), 'bg') 225 | let normal_fg = synIDattr(hlID('Normal'), 'fg') 226 | if empty(normal_bg) 227 | let normal_bg = normal_fg 228 | let normal_fg = '#FFFFFF' 229 | endif 230 | 231 | call s:set_hl('NotificationInfo', normal_bg, normal_fg) 232 | call s:set_hl('NotificationError', error_fg, error_bg) 233 | call s:set_hl('NotificationWarning', warning_fg, warning_bg) 234 | 235 | call s:set_hl('EchoNotificationInfo', normal_fg, 'NONE') 236 | call s:set_hl('EchoNotificationError', error_bg, 'NONE') 237 | call s:set_hl('EchoNotificationWarning', warning_bg, 'NONE') 238 | endfunction 239 | 240 | function! s:get_pos(pos, width) abort 241 | let min_col = s:neovim_float ? 1 : 2 242 | let min_row = s:neovim_float ? 0 : 1 243 | let max_col = &columns - 1 244 | let dbout_buffers = filter(range(1, winnr('$')), 'getwinvar(v:val, "&filetype") ==? "dbout"') 245 | let extra_height = 0 246 | if len(dbout_buffers) 247 | let extra_height = max(map(copy(dbout_buffers), 'winheight(v:val)')) 248 | let extra_height += 1 249 | endif 250 | let max_row = &lines - 3 - extra_height 251 | let pos_data = {'col': min_col, 'row': min_row} 252 | 253 | if a:pos ==? 'top' 254 | let pos_data.row = min_row 255 | let pos_data.col = (&columns / 2) - (a:width / 2) 256 | endif 257 | 258 | if a:pos ==? 'bottom' 259 | let pos_data.row = max_row 260 | let pos_data.col = (&columns / 2) - (a:width / 2) 261 | endif 262 | 263 | if a:pos ==? 'topright' 264 | let pos_data.col = max_col 265 | let pos_data.row = min_row 266 | endif 267 | 268 | if a:pos ==? 'botleft' 269 | let pos_data.col = min_col 270 | let pos_data.row = max_row 271 | endif 272 | 273 | if a:pos ==? 'botright' 274 | let pos_data.col = max_col 275 | let pos_data.row = max_row 276 | endif 277 | 278 | return pos_data 279 | endfunction 280 | 281 | function! s:set_hl(name, fg, bg) abort 282 | if !hlexists(a:name) 283 | silent! exe 'hi '.a:name.' guifg='.a:fg.' guibg='.a:bg 284 | endif 285 | endfunction 286 | 287 | function! s:hide_notifications() abort 288 | if has('nvim') 289 | return s:nvim_close() 290 | endif 291 | if exists('*popup_close') 292 | return popup_close(s:win) 293 | endif 294 | endfunction 295 | 296 | command DBUIHideNotifications call s:hide_notifications() 297 | -------------------------------------------------------------------------------- /autoload/db_ui/query.vim: -------------------------------------------------------------------------------- 1 | let s:query_instance = {} 2 | let s:query = {} 3 | let s:bind_param_rgx = '\(^\|[[:blank:]]\|[^:]\)\('.g:db_ui_bind_param_pattern.'\)' 4 | 5 | let s:query_info = { 6 | \ 'last_query_start_time': 0, 7 | \ 'last_query_time': 0 8 | \ } 9 | 10 | function! db_ui#query#new(drawer) abort 11 | let s:query_instance = s:query.new(a:drawer) 12 | return s:query_instance 13 | endfunction 14 | 15 | function! s:query.new(drawer) abort 16 | let instance = copy(self) 17 | let instance.drawer = a:drawer 18 | let instance.buffer_counter = {} 19 | let instance.last_query = [] 20 | augroup dbui_async_queries 21 | autocmd! 22 | autocmd User *DBExecutePre call s:start_query() 23 | autocmd User *DBExecutePost call s:print_query_time() 24 | augroup END 25 | return instance 26 | endfunction 27 | 28 | function! s:query.open(item, edit_action) abort 29 | let db = self.drawer.dbui.dbs[a:item.dbui_db_key_name] 30 | if a:item.type ==? 'buffer' 31 | return self.open_buffer(db, a:item.file_path, a:edit_action) 32 | endif 33 | let label = get(a:item, 'label', '') 34 | let table = '' 35 | let schema = '' 36 | if a:item.type !=? 'query' 37 | let suffix = a:item.table.'-'.a:item.label 38 | let table = a:item.table 39 | let schema = a:item.schema 40 | endif 41 | 42 | let buffer_name = self.generate_buffer_name(db, { 'schema': schema, 'table': table, 'label': label, 'filetype': db.filetype }) 43 | call self.open_buffer(db, buffer_name, a:edit_action, {'table': table, 'content': get(a:item, 'content'), 'schema': schema }) 44 | endfunction 45 | 46 | function! s:query.generate_buffer_name(db, opts) abort 47 | let time = exists('*strftime') ? strftime('%Y-%m-%d-%H-%M-%S') : localtime() 48 | let suffix = 'query' 49 | if !empty(a:opts.table) 50 | let suffix = printf('%s-%s', a:opts.table, a:opts.label) 51 | endif 52 | 53 | let buffer_name = db_ui#utils#slug(printf('%s-%s', a:db.name, suffix)) 54 | let buffer_name = printf('%s-%s', buffer_name, time) 55 | if type(g:Db_ui_buffer_name_generator) ==? type(function('tr')) 56 | let buffer_name = printf('%s-%s', a:db.name, call(g:Db_ui_buffer_name_generator, [a:opts])) 57 | endif 58 | 59 | if !empty(self.drawer.dbui.tmp_location) 60 | return printf('%s/%s', self.drawer.dbui.tmp_location, buffer_name) 61 | endif 62 | 63 | let tmp_name = printf('%s/%s', fnamemodify(tempname(), ':p:h'), buffer_name) 64 | call add(a:db.buffers.tmp, tmp_name) 65 | return tmp_name 66 | endfunction 67 | 68 | function! s:query.focus_window() abort 69 | let win_pos = g:db_ui_win_position ==? 'left' ? 'botright' : 'topleft' 70 | let win_cmd = 'vertical '.win_pos.' new' 71 | if winnr('$') ==? 1 72 | silent! exe win_cmd 73 | return 74 | endif 75 | 76 | let found = 0 77 | for win in range(1, winnr('$')) 78 | let buf = winbufnr(win) 79 | if !empty(getbufvar(buf, 'dbui_db_key_name')) 80 | let found = 1 81 | exe win.'wincmd w' 82 | break 83 | endif 84 | endfor 85 | 86 | if !found 87 | for win in range(1, winnr('$')) 88 | if getwinvar(win, '&filetype') !=? 'dbui' && getwinvar(win, '&buftype') !=? 'nofile' && getwinvar(win, '&modifiable') 89 | let found = 1 90 | exe win.'wincmd w' 91 | break 92 | endif 93 | endfor 94 | endif 95 | 96 | if (!found) 97 | silent! exe win_cmd 98 | endif 99 | endfunction 100 | 101 | function s:query.open_buffer(db, buffer_name, edit_action, ...) 102 | let opts = get(a:, '1', {}) 103 | let table = get(opts, 'table', '') 104 | let schema = get(opts, 'schema', '') 105 | let default_content = get(opts, 'content', g:db_ui_default_query) 106 | let was_single_win = winnr('$') ==? 1 107 | 108 | if a:edit_action ==? 'edit' 109 | call self.focus_window() 110 | let bufnr = bufnr(a:buffer_name) 111 | if bufnr > -1 112 | silent! exe 'b '.bufnr 113 | call self.setup_buffer(a:db, extend({'existing_buffer': 1 }, opts), a:buffer_name, was_single_win) 114 | return 115 | endif 116 | endif 117 | 118 | silent! exe a:edit_action.' '.a:buffer_name 119 | call self.setup_buffer(a:db, opts, a:buffer_name, was_single_win) 120 | 121 | if empty(table) 122 | return 123 | endif 124 | 125 | let optional_schema = schema ==? a:db.default_scheme ? '' : schema 126 | 127 | if !empty(optional_schema) 128 | if a:db.quote 129 | let optional_schema = '"'.optional_schema.'"' 130 | endif 131 | let optional_schema = optional_schema.'.' 132 | endif 133 | 134 | let content = substitute(default_content, '{table}', table, 'g') 135 | let content = substitute(content, '{optional_schema}', optional_schema, 'g') 136 | let content = substitute(content, '{schema}', schema, 'g') 137 | let db_name = !empty(schema) ? schema : a:db.db_name 138 | let content = substitute(content, '{dbname}', db_name, 'g') 139 | let content = substitute(content, '{last_query}', join(self.last_query, "\n"), 'g') 140 | silent 1,$delete _ 141 | call setline(1, split(content, "\n")) 142 | if g:db_ui_auto_execute_table_helpers 143 | if g:db_ui_execute_on_save 144 | write 145 | else 146 | call self.execute_query() 147 | endif 148 | endif 149 | endfunction 150 | 151 | function! s:query.setup_buffer(db, opts, buffer_name, was_single_win) abort 152 | call self.resize_if_single(a:was_single_win) 153 | let b:dbui_db_key_name = a:db.key_name 154 | let b:dbui_table_name = get(a:opts, 'table', '') 155 | let b:dbui_schema_name = get(a:opts, 'schema', '') 156 | let b:db = a:db.conn 157 | let is_existing_buffer = get(a:opts, 'existing_buffer', 0) 158 | let is_tmp = self.drawer.dbui.is_tmp_location_buffer(a:db, a:buffer_name) 159 | let db_buffers = self.drawer.dbui.dbs[a:db.key_name].buffers 160 | 161 | if index(db_buffers.list, a:buffer_name) ==? -1 162 | if empty(db_buffers.list) 163 | let db_buffers.expanded = 1 164 | endif 165 | call add(db_buffers.list, a:buffer_name) 166 | call self.drawer.render() 167 | endif 168 | 169 | if &filetype !=? a:db.filetype || !is_existing_buffer 170 | silent! exe 'setlocal noswapfile nowrap nospell modifiable filetype='.a:db.filetype 171 | endif 172 | let is_sql = &filetype ==? a:db.filetype 173 | nnoremap (DBUI_EditBindParameters) :call method('edit_bind_parameters') 174 | nnoremap (DBUI_ExecuteQuery) :call method('execute_query') 175 | vnoremap (DBUI_ExecuteQuery) :call method('execute_query', 1) 176 | if is_tmp && is_sql 177 | nnoremap (DBUI_SaveQuery) :call method('save_query') 178 | endif 179 | augroup db_ui_query 180 | autocmd! * 181 | if g:db_ui_execute_on_save && is_sql 182 | autocmd BufWritePost nested call s:method('execute_query') 183 | endif 184 | autocmd BufDelete,BufWipeout silent! call s:method('remove_buffer', str2nr(expand(''))) 185 | augroup END 186 | endfunction 187 | 188 | function! s:method(name, ...) abort 189 | if a:0 > 0 190 | return s:query_instance[a:name](a:1) 191 | endif 192 | 193 | return s:query_instance[a:name]() 194 | endfunction 195 | 196 | function! s:query.resize_if_single(is_single_win) abort 197 | if a:is_single_win 198 | exe self.drawer.get_winnr().'wincmd w' 199 | exe 'vertical resize '.g:db_ui_winwidth 200 | wincmd p 201 | endif 202 | endfunction 203 | 204 | function! s:query.remove_buffer(bufnr) 205 | let dbui_db_key_name = getbufvar(a:bufnr, 'dbui_db_key_name') 206 | let list = self.drawer.dbui.dbs[dbui_db_key_name].buffers.list 207 | let tmp = self.drawer.dbui.dbs[dbui_db_key_name].buffers.tmp 208 | call filter(list, 'v:val !=? bufname(a:bufnr)') 209 | call filter(tmp, 'v:val !=? bufname(a:bufnr)') 210 | return self.drawer.render() 211 | endfunction 212 | 213 | function! s:query.execute_query(...) abort 214 | let is_visual_mode = get(a:, 1, 0) 215 | let lines = self.get_lines(is_visual_mode) 216 | call s:start_query() 217 | if !is_visual_mode && search(s:bind_param_rgx, 'n') <= 0 218 | call db_ui#utils#print_debug({ 'message': 'Executing whole buffer', 'command': '%DB' }) 219 | silent! exe '%DB' 220 | else 221 | let db = self.drawer.dbui.dbs[b:dbui_db_key_name] 222 | call self.execute_lines(db, lines, is_visual_mode) 223 | endif 224 | let has_async = exists('*db#cancel') 225 | if has_async 226 | call db_ui#notifications#info('Executing query...') 227 | endif 228 | if !has_async 229 | call s:print_query_time() 230 | endif 231 | let self.last_query = lines 232 | endfunction 233 | 234 | function! s:query.execute_lines(db, lines, is_visual_mode) abort 235 | let filename = tempname().'.'.db#adapter#call(a:db.conn, 'input_extension', [], 'sql') 236 | let lines = copy(a:lines) 237 | let should_inject_vars = match(join(a:lines), s:bind_param_rgx) > -1 238 | 239 | if should_inject_vars 240 | try 241 | let lines = self.inject_variables(lines) 242 | catch /.*/ 243 | return db_ui#notifications#error(v:exception) 244 | endtry 245 | endif 246 | 247 | if len(lines) ==? 1 248 | call db_ui#utils#print_debug({'message': 'Executing single line', 'line': lines[0], 'command': 'DB '.lines[0] }) 249 | exe 'DB '.lines[0] 250 | return lines 251 | endif 252 | 253 | if empty(should_inject_vars) 254 | call db_ui#utils#print_debug({'message': 'Executing visual selection', 'command': "'<,'>DB"}) 255 | exe "'<,'>DB" 256 | else 257 | call db_ui#utils#print_debug({'message': 'Executing multiple lines', 'lines': lines, 'input_filename': filename, 'command': 'DB < '.filename }) 258 | call writefile(lines, filename) 259 | exe 'DB < '.filename 260 | endif 261 | 262 | return lines 263 | endfunction 264 | 265 | function! s:query.get_lines(is_visual_mode) abort 266 | if !a:is_visual_mode 267 | return getline(1, '$') 268 | endif 269 | 270 | let sel_save = &selection 271 | let &selection = 'inclusive' 272 | let reg_save = @@ 273 | silent exe 'normal! gvy' 274 | let lines = split(@@, "\n") 275 | let &selection = sel_save 276 | let @@ = reg_save 277 | return lines 278 | endfunction 279 | 280 | function! s:query.inject_variables(lines) abort 281 | let vars = [] 282 | for line in a:lines 283 | call substitute(line, s:bind_param_rgx, '\=add(vars, submatch(2))', 'g') 284 | endfor 285 | 286 | call filter(vars, {i,var -> !search(printf("'[^']*%s[^']*'", var), 'n')}) 287 | 288 | if !exists('b:dbui_bind_params') 289 | let b:dbui_bind_params = {} 290 | endif 291 | 292 | let existing_vars = keys(b:dbui_bind_params) 293 | let needs_prompt = !empty(filter(copy(vars), 'index(existing_vars, v:val) <= -1')) 294 | if needs_prompt 295 | echo "Please provide bind parameters. Empty values are ignored and considered a raw value.\n\n" 296 | endif 297 | 298 | let bind_params = copy(b:dbui_bind_params) 299 | for var in vars 300 | if !has_key(bind_params, var) 301 | let bind_params[var] = db_ui#utils#input('Enter value for bind parameter '.var.' -> ', '') 302 | endif 303 | endfor 304 | 305 | let b:dbui_bind_params = bind_params 306 | let content = [] 307 | 308 | for line in a:lines 309 | for [var, val] in items(b:dbui_bind_params) 310 | if trim(val) ==? '' 311 | continue 312 | endif 313 | let line = substitute(line, var, db_ui#utils#quote_query_value(val), 'g') 314 | endfor 315 | call add(content, line) 316 | endfor 317 | 318 | return content 319 | endfunction 320 | 321 | function! s:query.edit_bind_parameters() abort 322 | if !exists('b:dbui_bind_params') || empty(b:dbui_bind_params) 323 | return db_ui#notifications#info('No bind parameters to edit.') 324 | endif 325 | 326 | let variable_names = keys(b:dbui_bind_params) 327 | if len(variable_names) > 1 328 | let opts = ['Select bind parameter to edit/delete:'] + map(copy(variable_names), '(v:key + 1).") ".v:val." (".(trim(b:dbui_bind_params[v:val]) ==? "" ? "Not provided" : b:dbui_bind_params[v:val]).")"') 329 | let selection = db_ui#utils#inputlist(opts) 330 | 331 | if selection < 1 || selection > len(variable_names) 332 | return db_ui#notifications#error('Wrong selection.') 333 | endif 334 | 335 | let var_name = variable_names[selection - 1] 336 | let variable = b:dbui_bind_params[var_name] 337 | else 338 | let var_name = variable_names[0] 339 | let variable = b:dbui_bind_params[var_name] 340 | endif 341 | redraw! 342 | let action = confirm('Select action for '.var_name.' param? ', "&Edit\n&Delete\n&Cancel") 343 | if action ==? 1 344 | redraw! 345 | try 346 | let b:dbui_bind_params[var_name] = db_ui#utils#input('Enter new value: ', variable) 347 | catch /.*/ 348 | return db_ui#notifications#error(v:exception) 349 | endtry 350 | return db_ui#notifications#info('Changed.') 351 | endif 352 | 353 | if action ==? 2 354 | unlet b:dbui_bind_params[var_name] 355 | return db_ui#notifications#info('Deleted.') 356 | endif 357 | 358 | return db_ui#notifications#info('Canceled') 359 | endfunction 360 | 361 | function! s:query.save_query() abort 362 | try 363 | let db = self.drawer.dbui.dbs[b:dbui_db_key_name] 364 | if empty(db.save_path) 365 | throw 'Save location is empty. Please provide valid directory to g:db_ui_save_location' 366 | endif 367 | 368 | if !isdirectory(db.save_path) 369 | call mkdir(db.save_path, 'p') 370 | endif 371 | 372 | try 373 | let name = db_ui#utils#input('Save as: ', '') 374 | catch /.*/ 375 | return db_ui#notifications#error(v:exception) 376 | endtry 377 | 378 | if empty(trim(name)) 379 | throw 'No valid name provided.' 380 | endif 381 | 382 | let full_name = printf('%s/%s', db.save_path, name) 383 | 384 | if filereadable(full_name) 385 | throw 'That file already exists. Please choose another name.' 386 | endif 387 | 388 | exe 'write '.full_name 389 | call self.drawer.render({ 'queries': 1 }) 390 | call self.open_buffer(db, full_name, 'edit') 391 | catch /.*/ 392 | return db_ui#notifications#error(v:exception) 393 | endtry 394 | endfunction 395 | 396 | function! s:query.get_last_query_info() abort 397 | return { 398 | \ 'last_query': self.last_query, 399 | \ 'last_query_time': s:query_info.last_query_time 400 | \ } 401 | endfunction 402 | 403 | function! s:query.get_saved_query_db_name() abort 404 | let dbui = self.drawer.dbui 405 | if !empty(dbui.tmp_location) && dbui.tmp_location ==? expand('%:p:h') 406 | let filename = expand('%:t') 407 | if fnamemodify(filename, ':r') ==? 'db_ui' 408 | let filename = fnamemodify(filename, ':e') 409 | endif 410 | let db = get(filter(copy(dbui.dbs_list), 'filename =~? "^".v:val.name."-"'), 0, {}) 411 | if !empty(db) 412 | return db.name 413 | endif 414 | endif 415 | if expand('%:p:h:h') ==? dbui.save_path 416 | return expand('%:p:h:t') 417 | endif 418 | 419 | return '' 420 | endfunction 421 | 422 | function s:start_query() abort 423 | let s:query_info.last_query_start_time = reltime() 424 | endfunction 425 | 426 | function s:print_query_time() abort 427 | if empty(s:query_info.last_query_start_time) 428 | return 429 | endif 430 | let s:query_info.last_query_time = split(reltimestr(reltime(s:query_info.last_query_start_time)))[0] 431 | call db_ui#notifications#info('Done after '.s:query_info.last_query_time.' sec.') 432 | endfunction 433 | -------------------------------------------------------------------------------- /autoload/db_ui/schemas.vim: -------------------------------------------------------------------------------- 1 | function! s:strip_quotes(results) abort 2 | return split(substitute(join(a:results),'"','','g')) 3 | endfunction 4 | 5 | function! s:results_parser(results, delimiter, min_len) abort 6 | if a:min_len ==? 1 7 | return filter(a:results, '!empty(trim(v:val))') 8 | endif 9 | let mapped = map(a:results, {_,row -> filter(split(row, a:delimiter), '!empty(trim(v:val))')}) 10 | if a:min_len > 1 11 | return filter(mapped, 'len(v:val) ==? '.a:min_len) 12 | endif 13 | 14 | let counts = map(copy(mapped), 'len(v:val)') 15 | let min_len = max(counts) 16 | 17 | return filter(mapped,'len(v:val) ==? '.min_len) 18 | endfunction 19 | 20 | let s:postgres_foreign_key_query = " 21 | \ SELECT ccu.table_name AS foreign_table_name, ccu.column_name AS foreign_column_name, ccu.table_schema as foreign_table_schema 22 | \ FROM 23 | \ information_schema.table_constraints AS tc 24 | \ JOIN information_schema.key_column_usage AS kcu 25 | \ ON tc.constraint_name = kcu.constraint_name 26 | \ JOIN information_schema.constraint_column_usage AS ccu 27 | \ ON ccu.constraint_name = tc.constraint_name 28 | \ WHERE constraint_type = 'FOREIGN KEY' and kcu.column_name = '{col_name}' LIMIT 1" 29 | 30 | let s:postgres_list_schema_query = " 31 | \ SELECT nspname as schema_name 32 | \ FROM pg_catalog.pg_namespace 33 | \ WHERE nspname !~ '^pg_temp_' 34 | \ and pg_catalog.has_schema_privilege(current_user, nspname, 'USAGE') 35 | \ order by nspname" 36 | 37 | if empty(g:db_ui_use_postgres_views) 38 | let postgres_tables_and_views = " 39 | \ SELECT table_schema, table_name FROM information_schema.tables ;" 40 | else 41 | let postgres_tables_and_views = " 42 | \ SELECT table_schema, table_name FROM information_schema.tables UNION ALL 43 | \ select schemaname, matviewname from pg_matviews;" 44 | endif 45 | let s:postgres_tables_and_views = postgres_tables_and_views 46 | 47 | let s:postgresql = { 48 | \ 'args': ['-A', '-c'], 49 | \ 'foreign_key_query': s:postgres_foreign_key_query, 50 | \ 'schemes_query': s:postgres_list_schema_query, 51 | \ 'schemes_tables_query': s:postgres_tables_and_views, 52 | \ 'select_foreign_key_query': 'select * from "%s"."%s" where "%s" = %s', 53 | \ 'cell_line_number': 2, 54 | \ 'cell_line_pattern': '^-\++-\+', 55 | \ 'parse_results': {results,min_len -> s:results_parser(filter(results, '!empty(v:val)')[1:-2], '|', min_len)}, 56 | \ 'default_scheme': 'public', 57 | \ 'layout_flag': '\\x', 58 | \ 'quote': 1, 59 | \ } 60 | 61 | let s:sqlserver_foreign_keys_query = " 62 | \ SELECT TOP 1 c2.table_name as foreign_table_name, kcu2.column_name as foreign_column_name, kcu2.table_schema as foreign_table_schema 63 | \ from information_schema.table_constraints c 64 | \ inner join information_schema.key_column_usage kcu 65 | \ on c.constraint_schema = kcu.constraint_schema and c.constraint_name = kcu.constraint_name 66 | \ inner join information_schema.referential_constraints rc 67 | \ on c.constraint_schema = rc.constraint_schema and c.constraint_name = rc.constraint_name 68 | \ inner join information_schema.table_constraints c2 69 | \ on rc.unique_constraint_schema = c2.constraint_schema and rc.unique_constraint_name = c2.constraint_name 70 | \ inner join information_schema.key_column_usage kcu2 71 | \ on c2.constraint_schema = kcu2.constraint_schema and c2.constraint_name = kcu2.constraint_name and kcu.ordinal_position = kcu2.ordinal_position 72 | \ where c.constraint_type = 'FOREIGN KEY' 73 | \ and kcu.column_name = '{col_name}' 74 | \ " 75 | 76 | let s:sqlserver = { 77 | \ 'args': ['-h-1', '-W', '-s', '|', '-Q'], 78 | \ 'foreign_key_query': trim(s:sqlserver_foreign_keys_query), 79 | \ 'schemes_query': 'SELECT schema_name FROM INFORMATION_SCHEMA.SCHEMATA', 80 | \ 'schemes_tables_query': 'SELECT table_schema, table_name FROM INFORMATION_SCHEMA.TABLES', 81 | \ 'select_foreign_key_query': 'select * from %s.%s where %s = %s', 82 | \ 'cell_line_number': 2, 83 | \ 'cell_line_pattern': '^-\+.-\+', 84 | \ 'parse_results': {results, min_len -> s:results_parser(results[0:-3], '|', min_len)}, 85 | \ 'quote': 0, 86 | \ 'default_scheme': 'dbo', 87 | \ } 88 | 89 | let s:mysql_foreign_key_query = " 90 | \ SELECT referenced_table_name, referenced_column_name, referenced_table_schema 91 | \ from information_schema.key_column_usage 92 | \ where referenced_table_name is not null and column_name = '{col_name}' LIMIT 1" 93 | let s:mysql = { 94 | \ 'foreign_key_query': s:mysql_foreign_key_query, 95 | \ 'schemes_query': 'SELECT schema_name FROM information_schema.schemata', 96 | \ 'schemes_tables_query': 'SELECT table_schema, table_name FROM information_schema.tables', 97 | \ 'select_foreign_key_query': 'select * from %s.%s where %s = %s', 98 | \ 'cell_line_number': 3, 99 | \ 'requires_stdin': v:true, 100 | \ 'cell_line_pattern': '^+-\++-\+', 101 | \ 'parse_results': {results, min_len -> s:results_parser(results[1:], '\t', min_len)}, 102 | \ 'default_scheme': '', 103 | \ 'layout_flag': '\\G', 104 | \ 'quote': 0, 105 | \ 'filetype': 'mysql', 106 | \ } 107 | 108 | let s:oracle_args = join( 109 | \ [ 110 | \ 'SET linesize 4000', 111 | \ 'SET pagesize 4000', 112 | \ 'COLUMN owner FORMAT a20', 113 | \ 'COLUMN table_name FORMAT a25', 114 | \ 'COLUMN column_name FORMAT a25', 115 | \ '%s', 116 | \ ], 117 | \ ";\n" 118 | \ ).';' 119 | 120 | function! s:get_oracle_queries() 121 | let common_condition = "" 122 | 123 | if !g:db_ui_is_oracle_legacy 124 | let common_condition = "AND U.common = 'NO'" 125 | endif 126 | 127 | let foreign_key_query = " 128 | \SELECT /*csv*/ DISTINCT RFRD.table_name, RFRD.column_name, RFRD.owner 129 | \ FROM all_cons_columns RFRD 130 | \ JOIN all_constraints CON ON RFRD.constraint_name = CON.r_constraint_name 131 | \ JOIN all_cons_columns RFRING ON CON.constraint_name = RFRING.constraint_name 132 | \ JOIN all_users U ON CON.owner = U.username 133 | \ WHERE CON.constraint_type = 'R' 134 | \ " . common_condition . " 135 | \ AND RFRING.column_name = '{col_name}'" 136 | 137 | let schemes_query = " 138 | \SELECT /*csv*/ username 139 | \ FROM all_users U 140 | \ WHERE 1 = 1 141 | \ " . common_condition . " 142 | \ ORDER BY username" 143 | 144 | let schemes_tables_query = " 145 | \SELECT /*csv*/ T.owner, T.table_name 146 | \ FROM ( 147 | \ SELECT owner, table_name 148 | \ FROM all_tables 149 | \ UNION SELECT owner, view_name AS \"table_name\" 150 | \ FROM all_views 151 | \ ) T 152 | \ JOIN all_users U ON T.owner = U.username 153 | \ WHERE 1 = 1 154 | \ " . common_condition . " 155 | \ ORDER BY T.table_name" 156 | 157 | return { 158 | \ 'foreign_key_query': printf(s:oracle_args, foreign_key_query), 159 | \ 'schemes_query': printf(s:oracle_args, schemes_query), 160 | \ 'schemes_tables_query': printf(s:oracle_args, schemes_tables_query), 161 | \ } 162 | endfunction 163 | 164 | let oracle_queries = s:get_oracle_queries() 165 | 166 | let s:oracle = { 167 | \ 'callable': 'filter', 168 | \ 'cell_line_number': 1, 169 | \ 'cell_line_pattern': '^-\+\( \+-\+\)*', 170 | \ 'default_scheme': '', 171 | \ 'foreign_key_query': oracle_queries.foreign_key_query, 172 | \ 'has_virtual_results': v:true, 173 | \ 'parse_results': {results, min_len -> s:results_parser(results[3:], '\s\s\+', min_len)}, 174 | \ 'parse_virtual_results': {results, min_len -> s:results_parser(results[3:], '\s\s\+', min_len)}, 175 | \ 'requires_stdin': v:true, 176 | \ 'quote': v:true, 177 | \ 'schemes_query': oracle_queries.schemes_query, 178 | \ 'schemes_tables_query': oracle_queries.schemes_tables_query, 179 | \ 'select_foreign_key_query': printf(s:oracle_args, 'SELECT /*csv*/ * FROM "%s"."%s" WHERE "%s" = %s'), 180 | \ 'filetype': 'plsql', 181 | \ } 182 | 183 | if index(['sql', 'sqlcl'], get(g:, 'dbext_default_ORA_bin', '')) >= 0 184 | let s:oracle.parse_results = {results, min_len -> s:results_parser(s:strip_quotes(results[3:]), ',', min_len)} 185 | let s:oracle.parse_virtual_results = {results, min_len -> s:results_parser(s:strip_quotes(results[3:]), ',', min_len)} 186 | endif 187 | 188 | if !exists('g:db_adapter_bigquery_region') 189 | let g:db_adapter_bigquery_region = 'region-us' 190 | endif 191 | 192 | let s:bigquery_schemas_query = printf(" 193 | \ SELECT schema_name FROM `%s`.INFORMATION_SCHEMA.SCHEMATA 194 | \ ", g:db_adapter_bigquery_region) 195 | 196 | let s:bigquery_schema_tables_query = printf(" 197 | \ SELECT table_schema, table_name 198 | \ FROM `%s`.INFORMATION_SCHEMA.TABLES 199 | \ ", g:db_adapter_bigquery_region) 200 | 201 | let s:db_adapter_bigquery_max_results = 100000 202 | let s:bigquery = { 203 | \ 'callable': 'filter', 204 | \ 'args': ['--format=csv', '--max_rows=' .. s:db_adapter_bigquery_max_results], 205 | \ 'schemes_query': s:bigquery_schemas_query, 206 | \ 'schemes_tables_query': s:bigquery_schema_tables_query, 207 | \ 'parse_results': {results, min_len -> s:results_parser(results[1:], ',', min_len)}, 208 | \ 'layout_flag': '\\x', 209 | \ 'requires_stdin': v:true, 210 | \ } 211 | 212 | 213 | let s:clickhouse_schemes_query = " 214 | \ SELECT name as schema_name 215 | \ FROM system.databases 216 | \ ORDER BY name" 217 | 218 | let s:clickhouse_schemes_tables_query = " 219 | \ SELECT database AS table_schema, name AS table_name 220 | \ FROM system.tables 221 | \ ORDER BY table_name" 222 | 223 | let s:clickhouse = { 224 | \ 'args': ['-q'], 225 | \ 'schemes_query': trim(s:clickhouse_schemes_query), 226 | \ 'schemes_tables_query': trim(s:clickhouse_schemes_tables_query), 227 | \ 'cell_line_number': 1, 228 | \ 'cell_line_pattern': '^.*$', 229 | \ 'parse_results': {results, min_len -> s:results_parser(results, '\t', min_len)}, 230 | \ 'default_scheme': '', 231 | \ 'quote': 1, 232 | \ } 233 | 234 | " Add ClickHouse to the schemas dictionary 235 | let s:schemas = { 236 | \ 'postgres': s:postgresql, 237 | \ 'postgresql': s:postgresql, 238 | \ 'sqlserver': s:sqlserver, 239 | \ 'mysql': s:mysql, 240 | \ 'mariadb': s:mysql, 241 | \ 'oracle': s:oracle, 242 | \ 'bigquery': s:bigquery, 243 | \ 'clickhouse': s:clickhouse, 244 | \ } 245 | 246 | 247 | if !exists('g:db_adapter_postgres') 248 | let g:db_adapter_postgres = 'db#adapter#postgresql#' 249 | endif 250 | 251 | if !exists('g:db_adapter_sqlite3') 252 | let g:db_adapter_sqlite3 = 'db#adapter#sqlite#' 253 | endif 254 | 255 | function! db_ui#schemas#get(scheme) abort 256 | return get(s:schemas, a:scheme, {}) 257 | endfunction 258 | 259 | function! s:format_query(db, scheme, query) abort 260 | let conn = type(a:db) == v:t_string ? a:db : a:db.conn 261 | let callable = get(a:scheme, 'callable', 'interactive') 262 | let cmd = db#adapter#dispatch(conn, callable) + get(a:scheme, 'args', []) 263 | if get(a:scheme, 'requires_stdin', v:false) 264 | return [cmd, a:query] 265 | endif 266 | return [cmd + [a:query], ''] 267 | endfunction 268 | 269 | function! db_ui#schemas#query(db, scheme, query) abort 270 | let result = call('db#systemlist', s:format_query(a:db, a:scheme, a:query)) 271 | return map(result, {_, val -> substitute(val, "\r$", "", "")}) 272 | endfunction 273 | 274 | function db_ui#schemas#supports_schemes(scheme, parsed_url) 275 | let schema_support = !empty(get(a:scheme, 'schemes_query', 0)) 276 | if empty(schema_support) 277 | return 0 278 | endif 279 | let scheme_name = tolower(get(a:parsed_url, 'scheme', '')) 280 | " Mysql and MariaDB should not show schemas if the path (database name) is 281 | " defined 282 | if (scheme_name ==? 'mysql' || scheme_name ==? 'mariadb') && a:parsed_url.path !=? '/' 283 | return 0 284 | endif 285 | 286 | return 1 287 | endfunction 288 | -------------------------------------------------------------------------------- /autoload/db_ui/table_helpers.vim: -------------------------------------------------------------------------------- 1 | let s:basic_foreign_key_query = " 2 | \SELECT tc.constraint_name, tc.table_name, kcu.column_name, ccu.table_name AS foreign_table_name, ccu.column_name AS foreign_column_name, rc.update_rule, rc.delete_rule\n 3 | \FROM\n 4 | \ information_schema.table_constraints AS tc\n 5 | \ JOIN information_schema.key_column_usage AS kcu\n 6 | \ ON tc.constraint_name = kcu.constraint_name\n 7 | \ JOIN information_schema.referential_constraints as rc\n 8 | \ ON tc.constraint_name = rc.constraint_name\n 9 | \ JOIN information_schema.constraint_column_usage AS ccu\n 10 | \ ON ccu.constraint_name = tc.constraint_name\n" 11 | 12 | let s:bigquery = { 13 | \ 'List': 'select * from {optional_schema}{table} LIMIT 200', 14 | \ 'Columns': "select * from {schema}.INFORMATION_SCHEMA.COLUMNS where table_name='{table}'", 15 | \ } 16 | 17 | 18 | let s:postgres = { 19 | \ 'List': 'select * from {optional_schema}"{table}" LIMIT 200', 20 | \ 'Columns': "select * from information_schema.columns where table_name='{table}' and table_schema='{schema}'", 21 | \ 'Indexes': "SELECT * FROM pg_indexes where tablename='{table}' and schemaname='{schema}'", 22 | \ 'Foreign Keys': s:basic_foreign_key_query."WHERE constraint_type = 'FOREIGN KEY'\nand tc.table_name = '{table}'\nand tc.table_schema = '{schema}'", 23 | \ 'References': s:basic_foreign_key_query."WHERE constraint_type = 'FOREIGN KEY'\nand ccu.table_name = '{table}'\nand tc.table_schema = '{schema}'", 24 | \ 'Primary Keys': "SELECT * FROM information_schema.table_constraints WHERE constraint_type = 'PRIMARY KEY' AND table_schema = '{schema}' AND table_name = '{table}'", 25 | \ } 26 | 27 | let s:sqlite = { 28 | \ 'List': g:db_ui_default_query, 29 | \ 'Columns': "SELECT * FROM pragma_table_info('{table}')", 30 | \ 'Indexes': "SELECT * FROM pragma_index_list('{table}')", 31 | \ 'Foreign Keys': "SELECT * FROM pragma_foreign_key_list('{table}')", 32 | \ 'Primary Keys': "SELECT * FROM pragma_index_list('{table}') WHERE origin = 'pk'" 33 | \ } 34 | 35 | let s:mysql = { 36 | \ 'List': 'SELECT * from {optional_schema}`{table}` LIMIT 200', 37 | \ 'Columns': 'DESCRIBE {optional_schema}`{table}`', 38 | \ 'Indexes': 'SHOW INDEXES FROM {optional_schema}`{table}`', 39 | \ 'Foreign Keys': "SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA = '{schema}' AND TABLE_NAME = '{table}' AND CONSTRAINT_TYPE = 'FOREIGN KEY'", 40 | \ 'Primary Keys': "SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA = '{schema}' AND TABLE_NAME = '{table}' AND CONSTRAINT_TYPE = 'PRIMARY KEY'", 41 | \ } 42 | 43 | let s:oracle_from = " 44 | \FROM all_constraints N\n 45 | \JOIN all_cons_columns L\n\t 46 | \ON N.constraint_name = L.constraint_name\n\t 47 | \AND N.owner = L.owner" 48 | let s:oracle_qualify_and_order_by = " 49 | \L.table_name = '{table}'\n 50 | \ORDER BY\n\t" 51 | let s:oracle_key_cmd = " 52 | \SELECT\n\t 53 | \L.table_name,\n\t 54 | \L.column_name\n 55 | \" . s:oracle_from . "\n 56 | \WHERE\n\t 57 | \N.constraint_type = '%s'\n\t 58 | \AND " . s:oracle_qualify_and_order_by . "L.column_name" 59 | 60 | let s:oracle = { 61 | \ 'Columns': 'DESCRIBE "{schema}"."{table}"', 62 | \ 'Foreign Keys': printf(s:oracle_key_cmd, 'R'), 63 | \ 'Indexes': " 64 | \SELECT DISTINCT\n\t 65 | \N.owner,\n\t 66 | \N.index_name,\n\t 67 | \N.constraint_type\n 68 | \" . s:oracle_from . "\n 69 | \WHERE\n\t 70 | \" . s:oracle_qualify_and_order_by . "N.index_name", 71 | \ 'List': 'SELECT * FROM "{schema}"."{table}"', 72 | \ 'Primary Keys': printf(s:oracle_key_cmd, 'P'), 73 | \ 'References': " 74 | \SELECT\n\t 75 | \RFRING.owner,\n\t 76 | \RFRING.table_name,\n\t 77 | \RFRING.column_name\n 78 | \FROM all_cons_columns RFRING\n 79 | \JOIN all_constraints N\n\t 80 | \ON RFRING.constraint_name = N.constraint_name\n 81 | \JOIN all_cons_columns RFRD\n\t 82 | \ON N.r_constraint_name = RFRD.constraint_name\n 83 | \JOIN all_users U\n\t 84 | \ON N.owner = U.username\n 85 | \WHERE\n\t 86 | \N.constraint_type = 'R'\n 87 | \AND\n\t 88 | \U.common = 'NO'\n 89 | \AND\n\t 90 | \RFRD.owner = '{schema}'\n 91 | \AND\n\t 92 | \RFRD.table_name = '{table}'\n 93 | \ORDER BY\n\t 94 | \RFRING.owner,\n\t 95 | \RFRING.table_name,\n\t 96 | \RFRING.column_name", 97 | \ } 98 | 99 | for [helper, query] in items(s:oracle) 100 | let s:oracle[helper] = " 101 | \SET linesize 4000;\n 102 | \SET pagesize 4000;\n\n 103 | \COLUMN column_name FORMAT a20;\n 104 | \COLUMN constraint_type FORMAT a20;\n 105 | \COLUMN index_name FORMAT a20;\n 106 | \COLUMN owner FORMAT a20;\n 107 | \COLUMN table_name FORMAT a20;\n\n 108 | \" . query . "\n;" 109 | endfor 110 | 111 | let s:sqlserver_column_summary_query = " 112 | \ select c.column_name + ' (' + \n 113 | \ isnull(( select 'PK, ' from information_schema.table_constraints as k join information_schema.key_column_usage as kcu on k.constraint_name = kcu.constraint_name where constraint_type='PRIMARY KEY' and k.table_name = c.table_name and kcu.column_name = c.column_name), '') + \n 114 | \ isnull(( select 'FK, ' from information_schema.table_constraints as k join information_schema.key_column_usage as kcu on k.constraint_name = kcu.constraint_name where constraint_type='FOREIGN KEY' and k.table_name = c.table_name and kcu.column_name = c.column_name), '') + \n 115 | \ data_type + coalesce('(' + rtrim(cast(character_maximum_length as varchar)) + ')','(' + rtrim(cast(numeric_precision as varchar)) + ',' + rtrim(cast(numeric_scale as varchar)) + ')','(' + rtrim(cast(datetime_precision as varchar)) + ')','') + ', ' + \n 116 | \ case when is_nullable = 'YES' then 'null' else 'not null' end + ')' as Columns \n 117 | \ from information_schema.columns c where c.table_name='{table}' and c.TABLE_SCHEMA = '{schema}'" 118 | 119 | let s:sqlserver_foreign_keys_query = " 120 | \ SELECT c.constraint_name \n 121 | \ ,kcu.column_name as column_name \n 122 | \ ,c2.table_name as foreign_table_name \n 123 | \ ,kcu2.column_name as foreign_column_name \n 124 | \ from information_schema.table_constraints c \n 125 | \ inner join information_schema.key_column_usage kcu \n 126 | \ on c.constraint_schema = kcu.constraint_schema \n 127 | \ and c.constraint_name = kcu.constraint_name \n 128 | \ inner join information_schema.referential_constraints rc \n 129 | \ on c.constraint_schema = rc.constraint_schema \n 130 | \ and c.constraint_name = rc.constraint_name \n 131 | \ inner join information_schema.table_constraints c2 \n 132 | \ on rc.unique_constraint_schema = c2.constraint_schema \n 133 | \ and rc.unique_constraint_name = c2.constraint_name \n 134 | \ inner join information_schema.key_column_usage kcu2 \n 135 | \ on c2.constraint_schema = kcu2.constraint_schema \n 136 | \ and c2.constraint_name = kcu2.constraint_name \n 137 | \ and kcu.ordinal_position = kcu2.ordinal_position \n 138 | \ where c.constraint_type = 'FOREIGN KEY' \n 139 | \ and c.TABLE_NAME = '{table}' and c.TABLE_SCHEMA = '{schema}'" 140 | 141 | let s:sqlserver_references_query = " 142 | \ select kcu1.constraint_name as constraint_name \n 143 | \ ,kcu1.table_name as foreign_table_name \n 144 | \ ,kcu1.column_name as foreign_column_name \n 145 | \ ,kcu2.column_name as column_name \n 146 | \ from information_schema.referential_constraints as rc \n 147 | \ inner join information_schema.key_column_usage as kcu1 \n 148 | \ on kcu1.constraint_catalog = rc.constraint_catalog \n 149 | \ and kcu1.constraint_schema = rc.constraint_schema \n 150 | \ and kcu1.constraint_name = rc.constraint_name \n 151 | \ inner join information_schema.key_column_usage as kcu2 \n 152 | \ on kcu2.constraint_catalog = rc.unique_constraint_catalog \n 153 | \ and kcu2.constraint_schema = rc.unique_constraint_schema \n 154 | \ and kcu2.constraint_name = rc.unique_constraint_name \n 155 | \ and kcu2.ordinal_position = kcu1.ordinal_position \n 156 | \ where kcu2.table_name='{table}' and kcu2.table_schema = '{schema}'" 157 | 158 | let s:sqlserver_primary_keys = " 159 | \ select tc.constraint_name, kcu.column_name \n 160 | \ from \n 161 | \ information_schema.table_constraints AS tc \n 162 | \ JOIN information_schema.key_column_usage AS kcu \n 163 | \ ON tc.constraint_name = kcu.constraint_name \n 164 | \ JOIN information_schema.constraint_column_usage AS ccu \n 165 | \ ON ccu.constraint_name = tc.constraint_name \n 166 | \ where constraint_type = 'PRIMARY KEY' \n 167 | \ and tc.table_name = '{table}' and tc.table_schema = '{schema}'" 168 | 169 | let s:sqlserver_constraints_query = " 170 | \ SELECT u.CONSTRAINT_NAME, c.CHECK_CLAUSE FROM INFORMATION_SCHEMA.CONSTRAINT_TABLE_USAGE u \n 171 | \ inner join INFORMATION_SCHEMA.CHECK_CONSTRAINTS c on u.CONSTRAINT_NAME = c.CONSTRAINT_NAME \n 172 | \ where TABLE_NAME = '{table}' and u.TABLE_SCHEMA = '{schema}'" 173 | 174 | let s:sqlserver = { 175 | \ 'List': 'select top 200 * from {optional_schema}[{table}]', 176 | \ 'Columns': s:sqlserver_column_summary_query, 177 | \ 'Indexes': 'exec sp_helpindex ''{schema}.{table}''', 178 | \ 'Foreign Keys': s:sqlserver_foreign_keys_query, 179 | \ 'References': s:sqlserver_references_query, 180 | \ 'Primary Keys': s:sqlserver_primary_keys, 181 | \ 'Constraints': s:sqlserver_constraints_query, 182 | \ 'Describe': 'exec sp_help ''{schema}.{table}''', 183 | \ } 184 | 185 | let s:clickhouse = { 186 | \ 'List': "select * from `{schema}`.`{table}` limit 100 Format PrettyCompactMonoBlock", 187 | \ 'Columns': "select name from system.columns where database='{schema} and table={table}'", 188 | \ } 189 | 190 | let s:helpers = { 191 | \ 'bigquery': s:bigquery, 192 | \ 'postgresql': s:postgres, 193 | \ 'mysql': s:mysql, 194 | \ 'mariadb': s:mysql, 195 | \ 'oracle': s:oracle, 196 | \ 'sqlite': s:sqlite, 197 | \ 'sqlserver': s:sqlserver, 198 | \ 'clickhouse': s:clickhouse, 199 | \ 'mongodb': { 'List': '{table}.find()'}, 200 | \ } 201 | 202 | let s:all = {} 203 | 204 | for scheme in db#adapter#schemes() 205 | let s:all[scheme] = get(s:helpers, scheme, {}) 206 | endfor 207 | 208 | let s:all.postgres = s:all.postgresql 209 | let s:all.sqlite3 = s:all.sqlite 210 | 211 | let s:scheme_map = { 212 | \ 'postgres': 'postgresql', 213 | \ 'postgresql': 'postgres', 214 | \ 'sqlite3': 'sqlite', 215 | \ 'sqlite': 'sqlite3', 216 | \ } 217 | 218 | function! db_ui#table_helpers#get(scheme) abort 219 | let result = extend(get(s:all, a:scheme, { 'List': '' }), get(g:db_ui_table_helpers, a:scheme, {})) 220 | if has_key(s:scheme_map, a:scheme) 221 | let result = extend(result, get(g:db_ui_table_helpers, s:scheme_map[a:scheme], {})) 222 | endif 223 | 224 | return result 225 | endfunction 226 | -------------------------------------------------------------------------------- /autoload/db_ui/utils.vim: -------------------------------------------------------------------------------- 1 | function! db_ui#utils#slug(str) abort 2 | return substitute(a:str, '[^A-Za-z0-9_\-]', '', 'g') 3 | endfunction 4 | 5 | function! db_ui#utils#input(name, default) abort 6 | return input(a:name, a:default) 7 | endfunction 8 | 9 | function! db_ui#utils#inputlist(list) abort 10 | return inputlist(a:list) 11 | endfunction 12 | 13 | function! db_ui#utils#readfile(file) abort 14 | try 15 | let content = readfile(a:file) 16 | let content = json_decode(join(content, "\n")) 17 | if type(content) !=? type([]) 18 | throw 'Connections file not a valid array' 19 | endif 20 | return content 21 | catch /.*/ 22 | call db_ui#notifications#warning([ 23 | \ 'Error reading connections file.', 24 | \ printf('Validate that content of file %s is valid json array.', a:file), 25 | \ "If it's empty, feel free to delete it." 26 | \ ]) 27 | return [] 28 | endtry 29 | endfunction 30 | 31 | function! db_ui#utils#quote_query_value(val) abort 32 | if a:val =~? "^'.*'$" || a:val =~? '^[0-9]*$' || a:val =~? '^\(true\|false\)$' 33 | return a:val 34 | endif 35 | 36 | return "'".a:val."'" 37 | endfunction 38 | 39 | function! db_ui#utils#set_mapping(key, plug, ...) 40 | let mode = a:0 > 0 ? a:1 : 'n' 41 | 42 | if hasmapto(a:plug, mode) 43 | return 44 | endif 45 | 46 | let keys = a:key 47 | if type(a:key) ==? type('') 48 | let keys = [a:key] 49 | endif 50 | 51 | for key in keys 52 | silent! exe mode.'map '.key.' '.a:plug 53 | endfor 54 | endfunction 55 | 56 | function! db_ui#utils#print_debug(msg) abort 57 | if !g:db_ui_debug 58 | return 59 | endif 60 | 61 | echom '[DBUI Debug] '.string(a:msg) 62 | endfunction 63 | -------------------------------------------------------------------------------- /doc/dadbod-ui.txt: -------------------------------------------------------------------------------- 1 | *dadbod-ui.txt* 2 | 3 | Simple UI for https://github.com/tpope/vim-dadbod 4 | 5 | Author: Kristijan 6 | License: MIT 7 | 8 | vim-dadbod-ui *vim-dadbod-ui* 9 | 10 | 1. Introduction |vim-dadbod-ui-introduction| 11 | 2. Install |vim-dadbod-ui-install| 12 | 3. Commands |vim-dadbod-ui-commands| 13 | 4. Connections |vim-dadbod-ui-connections| 14 | 4.1 Through environment variables |vim-dadbod-ui-connections-env| 15 | 4.2 Via `g:db` and `g:dbs` global variable|vim-dadbod-ui-connections-g:dbs| 16 | 4.3 Using `DBUIAddConnection` comand |vim-dadbod-ui-connections-add| 17 | 5. Mappings |vim-dadbod-ui-mappings| 18 | 6. Table helpers |vim-dadbod-ui-table-helpers| 19 | 7. Bind parameters |vim-dadbod-ui-bind-parameters| 20 | 8. Settings |vim-dadbod-ui-settings| 21 | 9. Functions |vim-dadbod-ui-functions| 22 | 10. Autocommands |vim-dadbod-ui-autocommands| 23 | 11. Highlights |vim-dadbod-ui-highlights| 24 | 25 | ============================================================================== 26 | 1. Introduction *vim-dadbod-ui-introduction* 27 | 28 | Vim dadbod UI is simple UI for tpope's awesome vim-dadbod plugin. 29 | 30 | Main features: 31 | 32 | 1. Navigate through multiple databases and it's tables 33 | 2. Several ways to define your connections 34 | 3. Save queries on single location for later use 35 | 4. Define custom table helpers 36 | 5. Bind parameters 37 | 38 | ============================================================================== 39 | 2. Install *vim-dadbod-ui-install* 40 | 41 | Requirements: 42 | - https://github.com/tpope/vim-dadbod 43 | 44 | Install with your favorite package manager. If you don't have one, I suggest 45 | 46 | Configuration with lazy.nvim (https://github.com/folke/lazy.nvim) 47 | > 48 | return { 49 | 'kristijanhusak/vim-dadbod-ui', 50 | dependencies = { 51 | { 'tpope/vim-dadbod', lazy = true }, 52 | { 'kristijanhusak/vim-dadbod-completion', ft = { 'sql', 'mysql', 'plsql' }, lazy = true }, -- Optional 53 | }, 54 | cmd = { 55 | 'DBUI', 56 | 'DBUIToggle', 57 | 'DBUIAddConnection', 58 | 'DBUIFindBuffer', 59 | }, 60 | init = function() 61 | -- Your DBUI configuration 62 | vim.g.db_ui_use_nerd_fonts = 1 63 | end, 64 | } 65 | < 66 | 67 | Or vim-plug (https://github.com/junegunn/vim-plug) 68 | > 69 | Plug 'tpope/vim-dadbod' 70 | Plug 'kristijanhusak/vim-dadbod-ui' 71 | Plug 'kristijanhusak/vim-dadbod-completion' "Optional 72 | < 73 | 74 | Define a connection |vim-dadbod-ui-connections| 75 | Execute `:DBUI` command 76 | 77 | ============================================================================== 78 | 3. Commands *vim-dadbod-ui-commands* 79 | 80 | *DBUI* 81 | DBUI 82 | Open drawer with available connections, or an option to add 83 | connection if there isn't any. Supports ``, which means 84 | that you can open the drawer for example in new tab like this: 85 | `:tab DBUI` 86 | 87 | `:DBUI` 88 | 89 | *DBUIToggle* 90 | DBUIToggle 91 | Toggle the drawer. When closed, same as calling `DBUI`. When 92 | opened, same as doing `q` in drawer. 93 | 94 | `:DBUIToggle` 95 | 96 | *DBUIAddConnection* 97 | DBUIAddConnection 98 | Open a prompt to enter new connection, by providing database 99 | url and connection name. Once entered, it will be saved in 100 | connections file located in |g:db_ui_save_location| folder. 101 | This can also be triggered using key `A` from the drawer. See 102 | |(DBUI_AddConnection)| 103 | 104 | `:DBUIAddConnection` 105 | 106 | *DBUIFindBuffer* 107 | DBUIFindBuffer 108 | Find currently opened buffer in DBUI drawer, or if it wasn't 109 | opened from DBUI, assigns the buffer to specified db. 110 | 111 | `:DBUIFindBuffer` 112 | 113 | *DBUIRenameBuffer* 114 | DBUIRenameBuffer 115 | Rename currently opened buffer or saved query. If it's buffer, 116 | it must be written first. 117 | 118 | `:DBUIRenameBuffer` 119 | 120 | *DBUILastQueryInfo* 121 | DBUILastQueryInfo 122 | Print information about last query that was ran. 123 | It prints the query, and the time it took to finish. 124 | 125 | `:DBUILastQueryInfo` 126 | 127 | *DBUIHideNotifications* 128 | DBUIHideNotifications 129 | Hide all floating notifications that are currently shown. 130 | Does not work with `vim.notify` and `echo` mode 131 | 132 | `:DBUIHideNotifications` 133 | 134 | ============================================================================== 135 | 4. Connections *vim-dadbod-ui-connections* 136 | 137 | There are multiple ways to set up your database connections: 138 | 139 | 1. Through environment variables |vim-dadbod-ui-connections-env| 140 | 2. Via `g:db` and `g:dbs` global variable |vim-dadbod-ui-connections-g:dbs| 141 | 3. Via |DBUIAddConnection| command 142 | 143 | It is possible to combine all 3 types, but it's not possible to have same 144 | connection name from same source. 145 | 146 | ============================================================================== 147 | 4.1 Through environment variables *vim-dadbod-ui-connections-env* 148 | 149 | There are 2 ways to define connections using environment variables: 150 | 151 | 1. Using regular environment variable 152 | This option reads environment variable(s) called `$DBUI_URL` and `$DBUI_NAME`. 153 | `$DBUI_URL` contains connection url 154 | `$DBUI_NAME` contains connection name 155 | if only `$DBUI_URL` is defined,`$DBUI_NAME` is parsed from connection url. 156 | To change name of the variables that are read, change 157 | |g:db_ui_env_variable_url| and |g:db_ui_env_variable_name|. 158 | Note that this option can also leverage `dotenv.vim` since it exports dotenv 159 | variables as regular environment variables. 160 | 161 | 2. Using https://github.com/tpope/vim-dotenv 162 | This option allows defining multiple connections with multiple env variables 163 | inside your `.env`. For example, this would create two connections: 164 | 165 | * `DB_UI_DEV=postgres://postgres:rootpw@localhost:5432/dev-db` 166 | * `DB_UI_PRODUCTION=postgres://postgres:rootpw@localhost:5432/dev-db` 167 | 168 | One will be called `dev`, and another one `production`. Connection name is 169 | parsed from the variable name (everything after `DB_UI_` lowercased). To 170 | change the prefix, change `g:db_ui_dotenv_variable_prefix` value. 171 | 172 | 173 | ============================================================================== 174 | 4.2 Via `g:db` and `g:dbs` global variable *vim-dadbod-ui-connections-g:dbs* 175 | 176 | This option gives a bit more flexibility, but it's harder to keep it out of 177 | version control. 178 | vim-dadbod `g:db` variable is read first. 179 | `g:dbs` can be defined as an object, or as an array of objects: 180 | 181 | Object example: 182 | > 183 | function s:resolve_production_url() 184 | return system('get-prod-url') 185 | end 186 | 187 | let g:dbs = { 188 | \ 'dev': 'postgres://postgres:mypassword@localhost:5432/my-dev-db', 189 | \ 'staging': 'postgres://postgres:mypassword@localhost:5432/my-staging-db', 190 | \ 'wp': 'mysql://root@localhost/wp_awesome', 191 | \ 'production': function('s:resolve_production_url') 192 | \ } 193 | < 194 | Array of objects example: 195 | > 196 | let g:dbs = [ 197 | \ { 'name': 'dev', 'url': 'postgres://postgres:mypassword@localhost:5432/my-dev-db' } 198 | \ { 'name': 'staging', 'url': 'postgres://postgres:mypassword@localhost:5432/my-staging-db' }, 199 | \ { 'name': 'wp', 'url': 'mysql://root@localhost/wp_awesome' }, 200 | \ { 'name': 'production', 'url': function('s:resolve_production_url') }, 201 | \ ] 202 | < 203 | 204 | If you use Neovim, you can also use lua to define the connections: 205 | > 206 | vim.g.dbs = { 207 | { 208 | { name = 'dev', url = 'postgres://postgres:mypassword@localhost:5432/my-dev-db' } 209 | { name = 'staging', url = 'postgres://postgres:mypassword@localhost:5432/my-staging-db' }, 210 | { name = 'wp', url = 'mysql://root@localhost/wp_awesome' }, 211 | { 212 | name = 'production', 213 | url = function() 214 | return vim.fn.system('get-prod-url') 215 | end 216 | }, 217 | } 218 | } 219 | < 220 | 221 | 222 | Currently, only difference between these two methods is that array ensures 223 | order, while order of connections with g:dbs as object has arbitrary order. 224 | 225 | If you use this method, make sure to `keep it out of version control` . 226 | One way to ensure it's not commited is to use `exrc` option, which allows 227 | creating project level vimrc to hold this configuration. After that, add that 228 | file to your global gitignore file, and you're safe. 229 | Other solution is to have it as a function that resolves a value dynamically. 230 | 231 | ============================================================================== 232 | 4.3 Using `DBUIAddConnection` command *vim-dadbod-ui-connections-add* 233 | 234 | Executing |DBUIAddConnection| opens up a prompt to enter connection that will 235 | be saved in a `connections.json` file in |g:db_ui_save_location| folder. These 236 | connections will be available from everywhere. If you want to delete certain 237 | connection, open up DBUI drawer and press `d` (|(DBUI_DeleteLine|) on 238 | the connection you want to delete. 239 | 240 | ============================================================================== 241 | 242 | 5. Mappings *vim-dadbod-ui-mappings* 243 | 244 | *(DBUI_SelectLine)* 245 | (DBUI_SelectLine) 246 | This mapping is used for toggling and opening everything in 247 | the DBUI drawer. 248 | 249 | By default, mapped to `o`, `Enter` and `Double click` . 250 | 251 | *(DBUI_SelectLineVsplit)* 252 | (DBUI_SelectLineVsplit) 253 | This mapping is used for opening all non-toggle items from 254 | DBUI drawer in a vertical split. 255 | 256 | By default, mapped to `S`. 257 | 258 | *(DBUI_DeleteLine)* 259 | (DBUI_DeleteLine) 260 | This mapping is used deleting certain items from the DBUI 261 | drawer. It will work on these: 262 | 1. Buffers 263 | 2. Saved queries 264 | 3. Connections added via |DBUIAddConnection| 265 | Confirm prompt is opened before deleting to avoid accidents. 266 | 267 | By default, mapped to `d`. 268 | 269 | *(DBUI_JumpToForeignKey)* 270 | (DBUI_JumpToForeignKey) 271 | This mapping is used to jump to foreign key in a table from 272 | dbout buffer. Currently supported for MySQL, Sqlserver and 273 | Postgres. Here's an example: 274 | 275 | Having table called "Users": 276 | > 277 | _____________________________ 278 | | id | username | password | 279 | -------+----------+---------- 280 | | 1 | test | test1 | 281 | | 2 | john | johntest | 282 | ------------------------------ 283 | < 284 | And table "Posts" with a defined foreign key to table "Users": 285 | > 286 | _____________________________ 287 | | id | userId | subject | 288 | -------+----------+---------- 289 | | 1 | 1 | hello | 290 | | 2 | 2 | world | 291 | ------------------------------ 292 | < 293 | After doing a query to fetch "Posts", and in `.dbout` preview 294 | window going to the cell with user id of 1 (Note the cursor 295 | in middle of cell where userId is 1) 296 | > 297 | ___________________________________ 298 | | id | userId | subject | 299 | -------+----------------+---------- 300 | | 1 | 1 |<- cursor | hello | 301 | | 2 | 2 | world | 302 | ------------------------------------ 303 | < 304 | And then executing this mapping (by default ``), it will 305 | do a query behind the scenes to fetch the user from table 306 | "Users" with that id. After executing the mapping, you should 307 | get this result 308 | > 309 | _____________________________ 310 | | id | username | password | 311 | -------+----------+---------- 312 | | 1 | test | test1 | 313 | ------------------------------ 314 | < 315 | pressing `r` (dadbods mapping for populating cmd line with 316 | last query), will give you this: 317 | 318 | `:DB select * from Users where id = 1` 319 | 320 | *vim-dadbod-ic* 321 | ic 322 | Operator pending mapping that works on cell value in results 323 | buffer. For example, to yank cell value under cursor, do `yic`. 324 | To visually select it, do `vic`. Also supports custom 325 | registers. 326 | 327 | Mapped to `ic`. 328 | 329 | *(DBUI_AddConnection)* 330 | (DBUI_AddConnection) 331 | This mapping is used adding a new connection. It is same as 332 | executing |DBUIAddConnection| command. 333 | 334 | By default, mapped to `A`. 335 | 336 | *(DBUI_ToggleDetails)* 337 | (DBUI_ToggleDetails) 338 | This mapping is used for toggling small note beside all 339 | connections to see where this connection is defined. 340 | For example, if you have connection called `dev` in `g:dbs` 341 | variable, and connection called `production` in your 342 | connections file, you will see something like this: 343 | > 344 | > dev (g:dbs) 345 | > production (file) 346 | < 347 | By default, mapped to `H`. 348 | 349 | *(DBUI_Redraw)* 350 | (DBUI_Redraw) 351 | This mapping is used for redrawing the DBUI drawer. It will 352 | refresh the tables on all connections that were opened. This 353 | is usually not needed, only when tables are 354 | added/edited/deleted via query. 355 | 356 | By default, mapped to `R`. 357 | 358 | *(DBUI_RenameLine)* 359 | (DBUI_RenameLine) 360 | Rename buffer or saved query under cursor, or edit a 361 | connection added via |DBUIAddConnection|. If it's a buffer, 362 | it must be written (executed) at least once. Alternative is to 363 | use |DBUIRenameBuffer| from the buffer. 364 | 365 | By default, mapped to `r`. 366 | 367 | *(DBUI_SaveQuery)* 368 | (DBUI_SaveQuery) 369 | This mapping is used in `sql` files to save the query in 370 | `g:db_ui_save_location` for later use, since all queries are 371 | written to temp folder by default. Once saved, it will be 372 | available in the connection tree under `Saved queries` . 373 | 374 | By default, mapped to `W`. 375 | 376 | *(DBUI_ExecuteQuery)* 377 | (DBUI_ExecuteQuery) 378 | Execute query in sql buffer. Supports normal and visual mode. 379 | Normal mode executes all content in a file, where visual mode 380 | executes only the selected content. 381 | 382 | By default, mapped to `S`. 383 | 384 | *(DBUI_ToggleResultLayout)* 385 | (DBUI_ToggleResultLayout) 386 | Toggle expanded view in the results buffer (column and value 387 | per line). Currently supported only for MySQL and PostgreSQL. 388 | 389 | By default, mapped to `R`. 390 | 391 | *(DBUI_EditBindParameters)* 392 | (DBUI_EditBindParameters) 393 | This mapping is used in `sql` files to edit bind parameters if 394 | there are any. See |vim-dadbod-ui-bind-parameters|. 395 | 396 | By default, mapped to `E`. 397 | 398 | *?* 399 | ? 400 | This mapping is used to show help in the DBUI drawer, that 401 | contains mappings that are available. To hide 402 | `Press ? for help`, see |g:db_ui_show_help| 403 | 404 | *(DBUI_Quit)* 405 | (DBUI_Quit) 406 | Mapping for closing the drawer. 407 | 408 | By default, mapped to `q`. 409 | 410 | *(DBUI_GotoFirstSibling)* 411 | (DBUI_GotoFirstSibling) 412 | Go to first sibling in current context. 413 | 414 | By default, mapped to ``. 415 | 416 | *(DBUI_GotoLastSibling)* 417 | (DBUI_GotoLastSibling) 418 | Go to last sibling in current context. 419 | 420 | By default, mapped to ``. 421 | 422 | *(DBUI_GotoPrevSibling)* 423 | (DBUI_GotoPrevSibling) 424 | Go to previous sibling in current context. 425 | 426 | By default, mapped to `K`. 427 | 428 | *(DBUI_GotoNextSibling)* 429 | (DBUI_GotoNextSibling) 430 | Go to previous sibling in current context. 431 | 432 | By default, mapped to `J`. 433 | 434 | *(DBUI_GotoParentNode)* 435 | (DBUI_GotoParentNode) 436 | Go to parent node from current context. 437 | 438 | By default, mapped to ``. 439 | 440 | *(DBUI_GotoChildNode)* 441 | (DBUI_GotoChildNode) 442 | Go to child node from current context. It works only on 443 | entries that can be toggled (dbs, schemas, tables, ...) 444 | 445 | By default, mapped to ``. 446 | 447 | ============================================================================== 448 | 6. Table helpers *vim-dadbod-ui-table-helpers* 449 | 450 | Table helper is a predefined query that is easily available for each table. 451 | By default, all database schemes available in `vim-dadbod` have a `List` table 452 | helper, which is just a simple query to list the data from the table. 453 | Certain schemes (postgesql, mysql, sqlite) have few more helpers, like 454 | `Indexes`, `Forein Keys`, `Primary Keys`, etc. 455 | 456 | To define your own helper for a specific scheme, add it through 457 | `g:db_ui_table_helpers` variable like this: 458 | 459 | > 460 | let g:db_ui_table_helpers = { 461 | \ 'postgresql': { 462 | \ 'Count': 'select count(*) from {optional_schema}{table}' 463 | \ 'Explain': 'EXPLAIN ANALYZE {last_query}' 464 | \ } 465 | \ } 466 | < 467 | This will make `Count` table helper available for all postgresql connections 468 | for each table. Five variables are available as part of the table helper 469 | content: `{table}`, `{schema}`, `{optional_schema}`, `{dbname}` and `{last_query}`. You can also override the 470 | defaults by passing in the matching helper name. To override `List`, do this: 471 | 472 | > 473 | let g:db_ui_table_helpers = { 474 | \ 'postgresql': { 475 | \ 'List': 'select * from {table} order by id asc' 476 | \ } 477 | \ } 478 | 479 | Note that `{last_query}` will be empty if no queries were ran before opening 480 | that helper. Also, in the `EXPLAIN` example above, running the explain helper 481 | and then running it again for another table will print double `EXPLAIN ANALYZE` 482 | because first explain query is also considered a valid query for `{last_query}`. 483 | 484 | `{optional_schema}`is provided only when the current table schema is not the 485 | default one. If provided, it goes in format `schemaname.`, so if you need to 486 | write select queries, add it like this: 487 | `select * from {optional_schema}{table}`, which will expand into for example: 488 | `select * from information_schema.columns`, or for public schema in postgres, 489 | `select * from mytable` 490 | 491 | ============================================================================== 492 | 7. Bind parameters *vim-dadbod-ui-bind-parameters* 493 | 494 | Bind parameters are variables that can be injected into the query at execution 495 | time. For example, when executing this query 496 | > 497 | select * from contacts where id = :contactId 498 | < 499 | A prompt will pop up to enter a value for `:contactId`. Once you enter the 500 | value, it will execute the query with that value injected. It will not modify 501 | the content of the buffer, but replace the parameter with value on execution 502 | time. This will happen for every other time that you run the query from this 503 | buffer with that parameter name. Values for parameters are saved in the buffer 504 | variable. 505 | To edit or delete a bind parameter, use |(DBUI_EditBindParameters)| 506 | mapping (by default `E`). It will open a list of all parameters 507 | defined, where you can selecting and then edit/delete it. 508 | In cases where a certain value from query is read as bind parameter (For 509 | example, your sql contains a string that really needs to check for some value 510 | with a colon prefix), just leave the bind parameter empty, and it will be 511 | considered a raw value (not replaced). 512 | For example, if you have this query: 513 | > 514 | select * from posts where body LIKE '%:thething%' 515 | < 516 | `:thething` will be considered a bind parameter. Do not define a value, and it 517 | will be ignored. 518 | 519 | To pass an empty string, use two single quotes as value. Example: 520 | > 521 | select * from posts where subject != :subject 522 | < 523 | On prompt, entering `''` will run this query: 524 | > 525 | select * from posts where subject != '' 526 | < 527 | 528 | Certain values are parsed so they are injected as right type: 529 | 530 | 1. Numbers - If the content of parameter is only a number, it will not be 531 | quoted 532 | 2. Booleans - `true` and `false` are not quoted 533 | 534 | Every other value is automatically quoted. If you want to force quotes on a 535 | certain value (for example, to check the number as a string), just add quotes 536 | when defining the value and it will be treated as string. 537 | 538 | ============================================================================== 539 | 540 | 8. Settings *vim-dadbod-ui-settings* 541 | 542 | *g:db_ui_save_location* 543 | g:db_ui_save_location 544 | Path to folder where all connections and saved queries will be 545 | stored. It can be ralative or absolute path. Do not add a 546 | leading slash. 547 | 548 | Default value: `~/.local/share/db_ui` 549 | 550 | *g:db_ui_tmp_query_location* 551 | g:db_ui_tmp_query_location 552 | By default, all queries are created in temp folder, which is 553 | cleared on vim quit. If you want all your queries to be 554 | persisted in different location and read after vim restart 555 | as buffers, set this value to some folder location. 556 | For example, if you want to keep all your queries in folder 557 | named `queries` in your home folder, you would add this: 558 | `let g:db_ui_tmp_query_location = '~/queries'` 559 | 560 | Default value: `''` 561 | 562 | *g:db_ui_table_helpers* 563 | g:db_ui_table_helpers 564 | Dictionary containing custom table helpers. For more, see 565 | |vim-dadbod-ui-table-helpers| 566 | 567 | Default value: `{}` 568 | 569 | *g:db_ui_execute_on_save* 570 | g:db_ui_execute_on_save 571 | If this is set to `1`, queries are automatically executed on 572 | sql buffer save. 573 | 574 | Default value: `1` 575 | 576 | *g:db_ui_auto_execute_table_helpers* 577 | g:db_ui_auto_execute_table_helpers 578 | If this is set to `1`, opening any table helper will 579 | automatically write the query and execute it. 580 | 581 | Default value: `0` 582 | 583 | *g:db_ui_env_variable_url* 584 | g:db_ui_env_variable_url 585 | This value defines which environment variable is read for 586 | |vim-dadbod-ui-connections-env| connection url. 587 | 588 | Default value: `DBUI_URL` 589 | 590 | *g:db_ui_env_variable_name* 591 | g:db_ui_env_variable_name 592 | This value defines which environment variable is read for 593 | |vim-dadbod-ui-connections-env| connection name. 594 | 595 | Default value: `DBUI_NAME` 596 | 597 | *g:db_ui_dotenv_variable_prefix* 598 | g:db_ui_dotenv_variable_prefix 599 | This value defines a prefix that is read for 600 | |vim-dadbod-ui-connections-env| `dotenv.vim` method. 601 | 602 | Default value: `DB_UI_` 603 | 604 | *g:db_ui_notification_width* 605 | g:db_ui_notification_width 606 | Number of columns used for default notification width. 607 | 608 | Default value: `40` 609 | 610 | *g:db_ui_winwidth* 611 | g:db_ui_winwidth 612 | Number of columns used for default DBUI drawer width. 613 | 614 | Default value: `40` 615 | 616 | *g:db_ui_win_position* 617 | g:db_ui_win_position 618 | On which side of the screen should DBUI drawer open. 619 | Possible values: `left` and `right` 620 | 621 | Default value: `left` 622 | 623 | *g:db_ui_disable_mappings* 624 | g:db_ui_disable_mappings 625 | If this is set to `1`, no default mappings are defined. 626 | 627 | Default value: `0` 628 | 629 | *g:db_ui_disable_mappings_dbui* 630 | g:db_ui_disable_mappings_dbui 631 | If this is set to `1`, no default mappings for DBUI drawer 632 | are defined. 633 | 634 | Default value: `0` 635 | 636 | *g:db_ui_disable_mappings_dbout* 637 | g:db_ui_disable_mappings_dbout 638 | If this is set to `1`, no default mappings for dbout buffers 639 | are defined. 640 | 641 | Default value: `0` 642 | 643 | *g:db_ui_disable_mappings_sql* 644 | g:db_ui_disable_mappings_sql 645 | If this is set to `1`, no default mappings for SQL buffers 646 | are defined. 647 | 648 | Default value: `0` 649 | 650 | *g:db_ui_disable_mappings_javascript* 651 | g:db_ui_disable_mappings_javascript 652 | If this is set to `1`, no default mappings for Javascript buffers 653 | are defined. 654 | Note that these mappings are set only when buffer is created 655 | with vim-dadbod-ui, to not interfere with default javascript 656 | behavior. 657 | 658 | Default value: `0` 659 | 660 | *g:db_ui_bind_param_pattern* 661 | g:db_ui_bind_param_pattern 662 | Change the regex pattern used to match bind params. 663 | By default, it matches format `:variable_name`. 664 | To use `$1`,`$2` format, you would set it to: `\$\d\+` 665 | 666 | Default value: `:\w\+` 667 | 668 | *g:db_ui_show_database_icon* 669 | g:db_ui_show_database_icon 670 | If this is set to 1, will add an icon on database name. Notice 671 | this option depends on nerdfonts. So if you want enable it,you 672 | need install nerdfonts. 673 | 674 | Default value: `0` 675 | 676 | *g:db_ui_icons* 677 | g:db_ui_icons 678 | This value holds the icons that are used in drawer. You can 679 | override all or only some of them by passing the values you 680 | want to override. For example: 681 | > 682 | let g:db_ui_icons = { 683 | \ 'expanded': { 684 | \ 'db': '-', 685 | \ 'buffers': '-', 686 | \ 'saved_queries': '-', 687 | \ 'schemas': '-', 688 | \ 'schema': '-', 689 | \ 'tables': '-', 690 | \ 'table': '-', 691 | \ }, 692 | \ 'collapsed': { 693 | \ 'db': '+', 694 | \ 'buffers': '+', 695 | \ 'saved_queries': '+', 696 | \ 'schemas': '+', 697 | \ 'schema': '+', 698 | \ 'tables': '+', 699 | \ 'table': '+', 700 | \ }, 701 | \ } 702 | < 703 | Default value: 704 | > 705 | >{ 706 | 'expanded': { 707 | 'db': '▾', 708 | 'buffers': '▾', 709 | 'saved_queries': '▾', 710 | 'schemas': '▾', 711 | 'schema': '▾', 712 | 'tables': '▾', 713 | 'table': '▾', 714 | }, 715 | 'collapsed': { 716 | 'db': '▸', 717 | 'buffers': '▸', 718 | 'saved_queries': '▸', 719 | 'schemas': '▸', 720 | 'schema': '▸', 721 | 'tables': '▸', 722 | 'table': '▸', 723 | }, 724 | 'saved_query': '*', 725 | 'new_query': '+', 726 | 'tables': '~', 727 | 'buffers': '»', 728 | 'add_connection': '[+]', 729 | 'connection_ok': '✓', 730 | 'connection_error': '✕', 731 | } 732 | < 733 | *g:db_ui_use_nerd_fonts* 734 | g:db_ui_use_nerd_fonts 735 | When set to `1`, Uses nerd fonts. They can be overriden in the 736 | same way as regular icons. These are the default nerd icons: 737 | 738 | > 739 | { 740 | 'expanded': { 741 | 'db': '▾ ', 742 | 'buffers': '▾ ', 743 | 'saved_queries': '▾ ', 744 | 'schemas': '▾ ', 745 | 'schema': '▾ פּ', 746 | 'tables': '▾ 藺', 747 | 'table': '▾ ', 748 | }, 749 | 'collapsed': { 750 | 'db': '▸ ', 751 | 'buffers': '▸ ', 752 | 'saved_queries': '▸ ', 753 | 'schemas': '▸ ', 754 | 'schema': '▸ פּ', 755 | 'tables': '▸ 藺', 756 | 'table': '▸ ', 757 | }, 758 | 'saved_query': '', 759 | 'new_query': '璘', 760 | 'tables': '離', 761 | 'buffers': '﬘', 762 | 'add_connection': '', 763 | 'connection_ok': '✓', 764 | 'connection_error': '✕', 765 | } 766 | < 767 | 768 | Default value: `0` 769 | 770 | *g:db_ui_show_help* 771 | g:db_ui_show_help 772 | When set to `0`, hides `Press ? for help` from the DBUI 773 | drawer. Mapping will continue to work no matter of this value. 774 | 775 | Default value: `1` 776 | 777 | *g:db_ui_debug* 778 | g:db_ui_debug 779 | Set this to `1` to enable debug mode. This will print out some 780 | additional information in the command line about the executed 781 | query. 782 | 783 | Default value: `0` 784 | 785 | *g:db_ui_force_echo_notifications* 786 | g:db_ui_force_echo_notifications 787 | Notifications are shown via popups when supported. To force 788 | echoing messages to command line set this value to 1. 789 | 790 | Default value: `0` 791 | 792 | *g:db_ui_disable_info_notifications* 793 | g:db_ui_disable_info_notifications 794 | Disable all notifications with "info" severity. 795 | 796 | Default value: `0` 797 | 798 | *g:db_ui_use_postgres_views* 799 | g:db_ui_use_postgres_views 800 | Toggle showing postgres views in the drawer. 801 | This option must be disabled (set to 0) for Redshift. 802 | 803 | Default value: 1 804 | 805 | *g:db_ui_disable_progress_bar* 806 | g:db_ui_disable_progress_bar 807 | Toggle query execution progress bar. 808 | This option is available to prevent conflict with a custom progress bar 809 | or if you want to disable the progress entirely set to 1. 810 | 811 | Default value: 0 812 | 813 | *g:db_ui_use_nvim_notify* 814 | g:db_ui_use_nvim_notify 815 | Use Neovim's `vim.notify` API for notifications. 816 | 817 | Default value: `0` 818 | 819 | *g:db_ui_hide_schemas* 820 | g:db_ui_hide_schemas 821 | Hide specific schemas from the drawer. Useful for `postgres` 822 | connections to hide `pg_temp` tables. Each items is checked 823 | with |match()| function. 824 | Example: 825 | > 826 | let g:db_ui_hide_schemas = ['pg_catalog', 'pg_toast_temp.*'] 827 | < 828 | Default value: `[]` 829 | 830 | *g:Db_ui_buffer_name_generator* 831 | g:Db_ui_buffer_name_generator 832 | Custom function for generating buffer names from drawer. 833 | Function accepts the option with three properties: 834 | * label - label of the entry you selected. 835 | Example: "New Query", "List", "Indexes" 836 | * table - table name if buffer was created from table helper, 837 | or empty string 838 | * schema - schema name if buffer was created from table 839 | helper, or empty string 840 | * filetype - filetype that will be used for the buffer 841 | Note the uppercase `D`, which is required by vim for 842 | functions. 843 | Also, `make sure that name is always unique` to avoid any issues 844 | with overlapping names. 845 | Example: 846 | > 847 | function s:buffer_name_generator(opts) 848 | if empty(a:opts.table) 849 | return 'myquery-'.localtime() 850 | endif 851 | return 'myquery-fortable-'.a:opts.table.'-'.localtime() 852 | endfunction 853 | 854 | let g:Db_ui_buffer_name_generator = function('s:buffer_name_generator') 855 | < 856 | 857 | Default value: `0` 858 | 859 | *g:Db_ui_table_name_sorter 860 | g:Db_ui_table_name_sorter 861 | Custom function for sorting table names. 862 | Function accepts the table name list and return the sorted 863 | table name list. 864 | Note the uppercase `D`, which is required by vim for 865 | functions. 866 | Also, `make sure that name is always unique` to avoid any issues 867 | with overlapping names. 868 | Example: 869 | > 870 | function s:table_name_sorter(tables) 871 | return sort(tables) 872 | endfunction 873 | 874 | let g:Db_ui_table_name_sorter = function('s:table_name_sorter') 875 | < 876 | 877 | Default value: `0` 878 | 879 | *g:db_ui_drawer_sections* 880 | g:db_ui_drawer_sections 881 | This value defines which sections appear in the drawer and in 882 | what order. Available sections are: 'new_query', 'buffers', 883 | 'saved_queries', and 'schemas'. If not set, all sections 884 | are shown in the default order. 885 | 886 | Example to only show schemas/tables and new query: 887 | > 888 | let g:db_ui_drawer_sections = ['schemas', 'new_query'] 889 | < 890 | Default value: `['new_query', 'buffers', 'saved_queries', 'schemas']` 891 | 892 | *g:db_ui_default_query* 893 | g:db_ui_default_query (DEPRECATED) 894 | This value was intially used as a default value for the table 895 | queries. It is still used as a default value for certain 896 | schemes. It is deprecated in favor of |g:db_ui_table_helpers| 897 | 898 | Default value: `SELECT * from "{table}" LIMIT 200;` 899 | 900 | ============================================================================== 901 | 902 | 9. Functions *vim-dadbod-ui-functions* 903 | 904 | *db_ui#statusline()* 905 | db_ui#statusline({opts}) 906 | Return statusline information. For example, when table `posts` 907 | is opened from schema named `public` in database named `my-blog-db`, 908 | result will be this: 909 | > 910 | DBUI: my_blog -> public -> posts 911 | < 912 | Function accepts optional dictionary with these properties set 913 | as default: 914 | > 915 | { 916 | 'prefix': 'DBUI: ', 917 | 'separator': ' -> ', 918 | 'show': ['db_name', 'schema', 'table'] 919 | } 920 | < 921 | For example, just to show DB name and table without prefix 922 | separated by a dash (`my_blog - posts`), you would call statusline like this: 923 | > 924 | call db_ui#statusline({ 925 | \ 'show': ['db_name', 'table'], 926 | \ 'separator' : ' - ', 927 | \ 'prefix': '' 928 | \ }) 929 | < 930 | 931 | *db_ui#query()* 932 | db_ui#query({query}) 933 | Execute query and return results as array of arrays. 934 | Works only for `PostgreSQL`, `MySQL` and `Sqlserver`. 935 | Requires valid dbui sql buffer, or a `b:db` connection variable. 936 | There are also 2 more buffer vars available: 937 | * `b:dbui_table_name` - Name of the table for current buffer 938 | * `b:dbui_schema_name` - Name of the schema for current buffer 939 | 940 | For example, to generate an `INSERT` query with a mapping for 941 | current dbui sql buffer, you would have something like this in 942 | your vimrc: 943 | > 944 | function! s:populate_query() abort 945 | let rows = db_ui#query(printf( 946 | \ "select column_name, data_type from information_schema.columns where table_name='%s' and table_schema='%s'", 947 | \ b:dbui_table_name, 948 | \ b:dbui_schema_name 949 | \ )) 950 | let lines = ['INSERT INTO '.b:dbui_table_name.' ('] 951 | for [column, datatype] in rows 952 | call add(lines, column) 953 | endfor 954 | call add(lines, ') VALUES (') 955 | for [column, datatype] in rows 956 | call add(lines, printf('%s <%s>', column, datatype)) 957 | endfor 958 | call add(lines, ')') 959 | call setline(1, lines) 960 | endfunction 961 | 962 | autocmd FileType sql nnoremap i :call populate_query() 963 | < 964 | *db_ui#connections_list()* 965 | db_ui#connections_list() 966 | Return list of all available connections. These properties are 967 | returned: 968 | 969 | * name - Connection name 970 | * source - Where is connection defined (file, g:dbs, env, dotenv) 971 | * url - Connection url 972 | * is_connected - Is DBUI connected to this database 973 | 974 | ============================================================================== 975 | 10. Autocommands *vim-dadbod-ui-autocommands* 976 | 977 | *DBUIOpened* 978 | DBUIOpened 979 | This autocommand is triggered when dbui drawer is opened, and 980 | everything is set up. For example, to load additional 981 | connections from custom env file using `vim-dotenv`, you could 982 | do have something like this in your vimrc to load them: 983 | 984 | `autocmd User DBUIOpened let b:dotenv = DotenvRead('.envrc') | norm R` 985 | 986 | ============================================================================== 987 | 11. Highlights *vim-dadbod-ui-highlights* 988 | 989 | To customize the colors of the notifications, add these custom hl groups: 990 | * `NotificationInfo` 991 | * `NotificationWarning` 992 | * `NotificationError` 993 | 994 | For example, to use white text on blue(info) / yellow(warning) / red(error) background, 995 | add this to your vimrc/init.vim: 996 | > 997 | hi NotificationInfo guifg=#FFFFFF guibg=#0000FF 998 | hi NotificationWarning guifg=#FFFFFF guibg=#FFFF00 999 | hi NotificationError guifg=#FFFFFF guibg=#FF0000 1000 | < 1001 | 1002 | vim:tw=78:ts=8:ft=help:norl:noet:fen:noet: 1003 | -------------------------------------------------------------------------------- /ftplugin/dbout.vim: -------------------------------------------------------------------------------- 1 | nnoremap (DBUI_JumpToForeignKey) :call db_ui#dbout#jump_to_foreign_table() 2 | nnoremap (DBUI_YankCellValue) :call db_ui#dbout#get_cell_value() 3 | nnoremap (DBUI_YankHeader) :call db_ui#dbout#yank_header() 4 | nnoremap (DBUI_ToggleResultLayout) :call db_ui#dbout#toggle_layout() 5 | omap ic :call db_ui#dbout#get_cell_value() 6 | 7 | setlocal foldmethod=expr foldexpr=db_ui#dbout#foldexpr(v:lnum) | silent! normal!zo 8 | 9 | if get(g:, 'db_ui_disable_mappings', 0) || get(g:, 'db_ui_disable_mappings_dbout', 0) 10 | finish 11 | endif 12 | 13 | call db_ui#utils#set_mapping('', '(DBUI_JumpToForeignKey)') 14 | call db_ui#utils#set_mapping('vic', '(DBUI_YankCellValue)') 15 | call db_ui#utils#set_mapping('yh', '(DBUI_YankHeader)') 16 | call db_ui#utils#set_mapping('R', '(DBUI_ToggleResultLayout)') 17 | -------------------------------------------------------------------------------- /ftplugin/dbui.vim: -------------------------------------------------------------------------------- 1 | if get(g:, 'db_ui_disable_mappings', 0) || get(g:, 'db_ui_disable_mappings_dbui', 0) 2 | finish 3 | endif 4 | 5 | call db_ui#utils#set_mapping(['o', '', '<2-LeftMouse>'], '(DBUI_SelectLine)') 6 | call db_ui#utils#set_mapping('S', '(DBUI_SelectLineVsplit)') 7 | call db_ui#utils#set_mapping('R', '(DBUI_Redraw)') 8 | call db_ui#utils#set_mapping('d', '(DBUI_DeleteLine)') 9 | call db_ui#utils#set_mapping('A', '(DBUI_AddConnection)') 10 | call db_ui#utils#set_mapping('H', '(DBUI_ToggleDetails)') 11 | call db_ui#utils#set_mapping('r', '(DBUI_RenameLine)') 12 | call db_ui#utils#set_mapping('q', '(DBUI_Quit)') 13 | call db_ui#utils#set_mapping('', '(DBUI_GotoFirstSibling)') 14 | call db_ui#utils#set_mapping('', '(DBUI_GotoLastSibling)') 15 | call db_ui#utils#set_mapping('', '(DBUI_GotoParentNode)') 16 | call db_ui#utils#set_mapping('', '(DBUI_GotoChildNode)') 17 | call db_ui#utils#set_mapping('K', '(DBUI_GotoPrevSibling)') 18 | call db_ui#utils#set_mapping('J', '(DBUI_GotoNextSibling)') 19 | -------------------------------------------------------------------------------- /ftplugin/javascript.vim: -------------------------------------------------------------------------------- 1 | if get(g:, 'db_ui_disable_mappings', 0) || get(g:, 'db_ui_disable_mappings_javascript', 0) || get(b:, 'dbui_db_key_name', '') == '' 2 | finish 3 | endif 4 | 5 | call db_ui#utils#set_mapping('W', '(DBUI_SaveQuery)') 6 | call db_ui#utils#set_mapping('E', '(DBUI_EditBindParameters)') 7 | call db_ui#utils#set_mapping('S', '(DBUI_ExecuteQuery)') 8 | call db_ui#utils#set_mapping('S', '(DBUI_ExecuteQuery)', 'v') 9 | -------------------------------------------------------------------------------- /ftplugin/mysql.vim: -------------------------------------------------------------------------------- 1 | let s:current_folder = expand(':p:h') 2 | silent exe 'source '.s:current_folder.'/sql.vim' 3 | -------------------------------------------------------------------------------- /ftplugin/plsql.vim: -------------------------------------------------------------------------------- 1 | let s:current_folder = expand(':p:h') 2 | silent exe 'source '.s:current_folder.'/sql.vim' 3 | -------------------------------------------------------------------------------- /ftplugin/sql.vim: -------------------------------------------------------------------------------- 1 | if get(g:, 'db_ui_disable_mappings', 0) || get(g:, 'db_ui_disable_mappings_sql', 0) 2 | finish 3 | endif 4 | 5 | call db_ui#utils#set_mapping('W', '(DBUI_SaveQuery)') 6 | call db_ui#utils#set_mapping('E', '(DBUI_EditBindParameters)') 7 | call db_ui#utils#set_mapping('S', '(DBUI_ExecuteQuery)') 8 | call db_ui#utils#set_mapping('S', '(DBUI_ExecuteQuery)', 'v') 9 | -------------------------------------------------------------------------------- /plugin/db_ui.vim: -------------------------------------------------------------------------------- 1 | if exists('g:loaded_dbui') 2 | finish 3 | endif 4 | let g:loaded_dbui = 1 5 | 6 | let g:db_ui_disable_progress_bar = get(g:, 'db_ui_disable_progress_bar', 0) 7 | let g:db_ui_use_postgres_views = get(g:, 'db_ui_use_postgres_views', 1) 8 | let g:db_ui_notification_width = get(g:, 'db_ui_notification_width', 40) 9 | let g:db_ui_winwidth = get(g:, 'db_ui_winwidth', 40) 10 | let g:db_ui_win_position = get(g:, 'db_ui_win_position', 'left') 11 | let g:db_ui_default_query = get(g:, 'db_ui_default_query', 'SELECT * from "{table}" LIMIT 200;') 12 | let g:db_ui_save_location = get(g:, 'db_ui_save_location', '~/.local/share/db_ui') 13 | let g:db_ui_tmp_query_location = get(g:, 'db_ui_tmp_query_location', '') 14 | let g:db_ui_dotenv_variable_prefix = get(g:, 'db_ui_dotenv_variable_prefix', 'DB_UI_') 15 | let g:db_ui_env_variable_url = get(g:, 'db_ui_env_variable_url', 'DBUI_URL') 16 | let g:db_ui_env_variable_name = get(g:, 'db_ui_env_variable_name', 'DBUI_NAME') 17 | let g:db_ui_disable_mappings = get(g:, 'db_ui_disable_mappings', 0) 18 | let g:db_ui_disable_mappings_dbui = get(g:, 'db_ui_disable_mappings_dbui', 0) 19 | let g:db_ui_disable_mappings_dbout = get(g:, 'db_ui_disable_mappings_dbout', 0) 20 | let g:db_ui_disable_mappings_sql = get(g:, 'db_ui_disable_mappings_sql', 0) 21 | let g:db_ui_disable_mappings_javascript = get(g:, 'db_ui_disable_mappings_javascript', 0) 22 | let g:db_ui_table_helpers = get(g:, 'db_ui_table_helpers', {}) 23 | let g:db_ui_auto_execute_table_helpers = get(g:, 'db_ui_auto_execute_table_helpers', 0) 24 | let g:db_ui_show_help = get(g:, 'db_ui_show_help', 1) 25 | let g:db_ui_use_nerd_fonts = get(g:, 'db_ui_use_nerd_fonts', 0) 26 | let g:db_ui_execute_on_save = get(g:, 'db_ui_execute_on_save', 1) 27 | let g:db_ui_force_echo_notifications = get(g:, 'db_ui_force_echo_notifications', 0) 28 | let g:db_ui_disable_info_notifications = get(g:, 'db_ui_disable_info_notifications', 0) 29 | let g:db_ui_use_nvim_notify = get(g:, 'db_ui_use_nvim_notify', 0) 30 | let g:Db_ui_buffer_name_generator = get(g:, 'Db_ui_buffer_name_generator', 0) 31 | let g:Db_ui_table_name_sorter = get(g:, 'Db_ui_table_name_sorter', 0) 32 | let g:db_ui_debug = get(g:, 'db_ui_debug', 0) 33 | let g:db_ui_hide_schemas = get(g:, 'db_ui_hide_schemas', []) 34 | let g:db_ui_bind_param_pattern = get(g: , 'db_ui_bind_param_pattern', ':\w\+') 35 | let g:db_ui_is_oracle_legacy = get(g:, 'db_ui_is_oracle_legacy', 0) 36 | let g:db_ui_drawer_sections = get(g:, 'db_ui_drawer_sections', ['new_query', 'buffers', 'saved_queries', 'schemas']) 37 | 38 | let s:dbui_icons = get(g:, 'db_ui_icons', {}) 39 | let s:expanded_icon = get(s:dbui_icons, 'expanded', '▾') 40 | let s:collapsed_icon = get(s:dbui_icons, 'collapsed', '▸') 41 | let s:expanded_icons = {} 42 | let s:collapsed_icons = {} 43 | 44 | if type(s:expanded_icon) !=? type('') 45 | let s:expanded_icons = s:expanded_icon 46 | let s:expanded_icon = '▾' 47 | else 48 | silent! call remove(s:dbui_icons, 'expanded') 49 | endif 50 | 51 | if type(s:collapsed_icon) !=? type('') 52 | let s:collapsed_icons = s:collapsed_icon 53 | let s:collapsed_icon = '▸' 54 | else 55 | silent! call remove(s:dbui_icons, 'collapsed') 56 | endif 57 | 58 | let g:db_ui_icons = { 59 | \ 'expanded': { 60 | \ 'db': s:expanded_icon, 61 | \ 'buffers': s:expanded_icon, 62 | \ 'saved_queries': s:expanded_icon, 63 | \ 'schemas': s:expanded_icon, 64 | \ 'schema': s:expanded_icon, 65 | \ 'tables': s:expanded_icon, 66 | \ 'table': s:expanded_icon, 67 | \ }, 68 | \ 'collapsed': { 69 | \ 'db': s:collapsed_icon, 70 | \ 'buffers': s:collapsed_icon, 71 | \ 'saved_queries': s:collapsed_icon, 72 | \ 'schemas': s:collapsed_icon, 73 | \ 'schema': s:collapsed_icon, 74 | \ 'tables': s:collapsed_icon, 75 | \ 'table': s:collapsed_icon, 76 | \ }, 77 | \ 'saved_query': '*', 78 | \ 'new_query': '+', 79 | \ 'tables': '~', 80 | \ 'buffers': '»', 81 | \ 'add_connection': '[+]', 82 | \ 'connection_ok': '✓', 83 | \ 'connection_error': '✕', 84 | \ } 85 | 86 | if g:db_ui_use_nerd_fonts 87 | let g:db_ui_icons = { 88 | \ 'expanded': { 89 | \ 'db': s:expanded_icon.' 󰆼', 90 | \ 'buffers': s:expanded_icon.' ', 91 | \ 'saved_queries': s:expanded_icon.' ', 92 | \ 'schemas': s:expanded_icon.' ', 93 | \ 'schema': s:expanded_icon.' 󰙅', 94 | \ 'tables': s:expanded_icon.' 󰓱', 95 | \ 'table': s:expanded_icon.' ', 96 | \ }, 97 | \ 'collapsed': { 98 | \ 'db': s:collapsed_icon.' 󰆼', 99 | \ 'buffers': s:collapsed_icon.' ', 100 | \ 'saved_queries': s:collapsed_icon.' ', 101 | \ 'schemas': s:collapsed_icon.' ', 102 | \ 'schema': s:collapsed_icon.' 󰙅', 103 | \ 'tables': s:collapsed_icon.' 󰓱', 104 | \ 'table': s:collapsed_icon.' ', 105 | \ }, 106 | \ 'saved_query': ' ', 107 | \ 'new_query': ' 󰓰', 108 | \ 'tables': ' 󰓫', 109 | \ 'buffers': ' ', 110 | \ 'add_connection': ' 󰆺', 111 | \ 'connection_ok': '✓', 112 | \ 'connection_error': '✕', 113 | \ } 114 | endif 115 | 116 | let g:db_ui_icons.expanded = extend(g:db_ui_icons.expanded, s:expanded_icons) 117 | let g:db_ui_icons.collapsed = extend(g:db_ui_icons.collapsed, s:collapsed_icons) 118 | silent! call remove(s:dbui_icons, 'expanded') 119 | silent! call remove(s:dbui_icons, 'collapsed') 120 | let g:db_ui_icons = extend(g:db_ui_icons, s:dbui_icons) 121 | 122 | augroup dbui 123 | autocmd! 124 | autocmd BufRead,BufNewFile *.dbout set filetype=dbout 125 | autocmd BufReadPost *.dbout nested call db_ui#save_dbout(expand('')) 126 | autocmd FileType dbout,dbui autocmd BufEnter,WinEnter stopinsert 127 | augroup END 128 | 129 | command! DBUI call db_ui#open('') 130 | command! DBUIToggle call db_ui#toggle() 131 | command! DBUIClose call db_ui#close() 132 | command! DBUIAddConnection call db_ui#connections#add() 133 | command! DBUIFindBuffer call db_ui#find_buffer() 134 | command! DBUIRenameBuffer call db_ui#rename_buffer() 135 | command! DBUILastQueryInfo call db_ui#print_last_query_info() 136 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ ! -d "vim-themis" ]; then 4 | git clone https://github.com/thinca/vim-themis 5 | fi 6 | 7 | if [ ! -d "vim-dadbod" ]; then 8 | git clone https://github.com/tpope/vim-dadbod 9 | fi 10 | 11 | if [ ! -d "vim-dotenv" ]; then 12 | git clone https://github.com/tpope/vim-dotenv 13 | fi 14 | 15 | ./vim-themis/bin/themis 16 | -------------------------------------------------------------------------------- /syntax/dbui.vim: -------------------------------------------------------------------------------- 1 | syntax clear 2 | for [icon_name, icon] in items(g:db_ui_icons) 3 | if type(icon) ==? type({}) 4 | for [nested_icon_name, nested_icon] in items(icon) 5 | let name = 'dbui_'.icon_name.'_'.nested_icon_name 6 | exe 'syn match '.name.' /^[[:blank:]]*'.escape(nested_icon, '*[]\/~').'/' 7 | exe 'hi default link '.name.' Directory' 8 | endfor 9 | else 10 | exe 'syn match dbui_'.icon_name. ' /^[[:blank:]]*'.escape(icon, '*[]\/~').'/' 11 | endif 12 | endfor 13 | 14 | exe 'syn match dbui_connection_source /\('.g:db_ui_icons.expanded.db.'\s\|'.g:db_ui_icons.collapsed.db.'\s\)\@ -1 29 | let g:db_ui_string_content_not_considered_bind_param = 0 30 | return '' 31 | endif 32 | if stridx(a:msg, '$1') > -1 33 | return '1' 34 | endif 35 | if stridx(a:msg, '$2') > -1 36 | return 'John' 37 | endif 38 | if stridx(a:msg, '$3') > -1 39 | return '' 40 | endif 41 | endfunction 42 | write 43 | call s:expect(g:db_ui_cast_not_considered_bind_param).to_equal(1) 44 | call s:expect(g:db_ui_string_content_not_considered_bind_param).to_equal(1) 45 | call s:expect(get(b:, 'dbui_bind_params')).to_be_dict() 46 | call s:expect(b:dbui_bind_params['$1']).to_equal(1) 47 | call s:expect(b:dbui_bind_params['$2']).to_equal('John') 48 | call s:expect(b:dbui_bind_params['$3']).to_equal('') 49 | endfunction 50 | 51 | function! s:suite.should_prompt_to_edit_bind_parameters_with_custom_pattern() abort 52 | let g:db_ui_test_option = 1 53 | function! db_ui#utils#inputlist(msg) abort 54 | return g:db_ui_test_option 55 | endfunction 56 | 57 | let g:db_ui_bind_param_keys = keys(b:dbui_bind_params) 58 | let g:db_ui_new_bind_params = { 59 | \ '$1': '2', 60 | \ '$2': 'Peter', 61 | \ '$3': '', 62 | \ } 63 | 64 | function! db_ui#utils#input(msg, default) abort 65 | return g:db_ui_new_bind_params[g:db_ui_bind_param_keys[g:db_ui_test_option - 1]] 66 | endfunction 67 | norm ,E 68 | let g:db_ui_test_option = 2 69 | norm ,E 70 | let g:db_ui_test_option = 3 71 | norm ,E 72 | call s:expect(b:dbui_bind_params['$1']).to_equal(2) 73 | call s:expect(b:dbui_bind_params['$2']).to_equal('Peter') 74 | call s:expect(b:dbui_bind_params['$3']).to_equal('') 75 | endfunction 76 | -------------------------------------------------------------------------------- /test/test-bind-parameters.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Bind parameters') 2 | let s:expect = themis#helper('expect') 3 | let s:bufnr = '' 4 | 5 | function! s:suite.before() abort 6 | call SetupTestDbs() 7 | " Sleep 1 sec to avoid overlapping temp names 8 | sleep 1 9 | endfunction 10 | 11 | function! s:suite.after() abort 12 | call Cleanup() 13 | endfunction 14 | 15 | function! s:suite.should_prompt_to_set_bind_parameters() abort 16 | :DBUI 17 | norm ojo 18 | call s:expect(&filetype).to_equal('sql') 19 | let g:db_ui_cast_not_considered_bind_param = 1 20 | let g:db_ui_string_content_not_considered_bind_param = 1 21 | norm!Iselect *, name::text from contacts where id = :contactId and first_name = :firstName and last_name = ":shouldSkip" and settings = '{:ignored 123}' 22 | runtime autoload/db_ui/utils.vim 23 | function! db_ui#utils#input(msg, default) abort 24 | if stridx(a:msg, ':ignored') > -1 25 | let g:db_ui_string_content_not_considered_bind_param = 0 26 | return '' 27 | endif 28 | if stridx(a:msg, ':text') > -1 29 | let g:db_ui_cast_not_considered_bind_param = 0 30 | return '' 31 | endif 32 | if stridx(a:msg, ':contactId') > -1 33 | return '1' 34 | endif 35 | if stridx(a:msg, ':firstName') > -1 36 | return 'John' 37 | endif 38 | if stridx(a:msg, ':shouldSkip') > -1 39 | return '' 40 | endif 41 | endfunction 42 | write 43 | call s:expect(g:db_ui_cast_not_considered_bind_param).to_equal(1) 44 | call s:expect(g:db_ui_string_content_not_considered_bind_param).to_equal(1) 45 | call s:expect(get(b:, 'dbui_bind_params')).to_be_dict() 46 | call s:expect(b:dbui_bind_params[':contactId']).to_equal(1) 47 | call s:expect(b:dbui_bind_params[':firstName']).to_equal('John') 48 | call s:expect(b:dbui_bind_params[':shouldSkip']).to_equal('') 49 | endfunction 50 | 51 | function! s:suite.should_prompt_to_edit_bind_parameters() abort 52 | let g:db_ui_test_option = 1 53 | function! db_ui#utils#inputlist(msg) abort 54 | return g:db_ui_test_option 55 | endfunction 56 | 57 | let g:db_ui_bind_param_keys = keys(b:dbui_bind_params) 58 | let g:db_ui_new_bind_params = { 59 | \ ':contactId': '2', 60 | \ ':shouldSkip': '', 61 | \ ':firstName': 'Peter' 62 | \ } 63 | 64 | function! db_ui#utils#input(msg, default) abort 65 | return g:db_ui_new_bind_params[g:db_ui_bind_param_keys[g:db_ui_test_option - 1]] 66 | endfunction 67 | norm ,E 68 | let g:db_ui_test_option = 2 69 | norm ,E 70 | let g:db_ui_test_option = 3 71 | norm ,E 72 | call s:expect(b:dbui_bind_params[':contactId']).to_equal(2) 73 | call s:expect(b:dbui_bind_params[':firstName']).to_equal('Peter') 74 | call s:expect(b:dbui_bind_params[':shouldSkip']).to_equal('') 75 | endfunction 76 | -------------------------------------------------------------------------------- /test/test-corrupted-connections-file.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Handle corrupted connections file') 2 | let s:expect = themis#helper('expect') 3 | 4 | function s:suite.before() abort 5 | call writefile(['{}'], g:db_ui_save_location.'/connections.json') 6 | endfunction 7 | 8 | function s:suite.after() abort 9 | call delete(g:db_ui_save_location.'/connections.json') 10 | call Cleanup() 11 | endfunction 12 | 13 | function! s:suite.should_show_error_for_corrupted_file_and_continue() 14 | :DBUI 15 | call s:expect(db_ui#notifications#get_last_msg()).to_match('^Error reading connections file.') 16 | endfunction 17 | 18 | -------------------------------------------------------------------------------- /test/test-custom-buffer-name-generator.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Custom icons') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:suite.before() abort 5 | call SetupTestDbs() 6 | endfunction 7 | 8 | function! s:suite.after() abort 9 | call UnsetOptionVariable('Db_ui_buffer_name_generator') 10 | call Cleanup() 11 | endfunction 12 | 13 | function! s:name_generator(opts) 14 | if empty(a:opts.table) 15 | return 'query-from-test-suite-'.localtime().'.'.a:opts.filetype 16 | endif 17 | 18 | return 'query-from-test-suite-'.a:opts.table.'-'.localtime().'.'.a:opts.filetype 19 | endfunction 20 | 21 | function! s:suite.should_use_custom_icons() abort 22 | :DBUI 23 | norm ojo 24 | :DBUI 25 | call s:expect(getline(4)).to_match('^ '.g:db_ui_icons.buffers.' query-.*$') 26 | call SetOptionVariable('Db_ui_buffer_name_generator', function('s:name_generator')) 27 | norm ggjo 28 | :DBUI 29 | call s:expect(getline(5)).to_match('^ '.g:db_ui_icons.buffers.' query-from-test-suite-\d\+\.sql\s\*$') 30 | norm gg6jojojo 31 | :DBUI 32 | call s:expect(getline(6)).to_match('^ '.g:db_ui_icons.buffers.' query-from-test-suite-contacts-\d\+\.sql\s\*$') 33 | endfunction 34 | -------------------------------------------------------------------------------- /test/test-custom-icons.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Custom icons') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:suite.before() abort 5 | call SetupTestDbs() 6 | call SetOptionVariable('db_ui_icons', { 'expanded': '[-]', 'collapsed': '[+]' }) 7 | endfunction 8 | 9 | function! s:suite.after() abort 10 | call UnsetOptionVariable('db_ui_icons') 11 | call Cleanup() 12 | endfunction 13 | 14 | function! s:suite.should_use_custom_icons() abort 15 | :DBUI 16 | call s:expect(getline(1, '$')).to_equal([ 17 | \ '[+] dadbod_ui_test', 18 | \ '[+] dadbod_ui_testing', 19 | \ ]) 20 | endfunction 21 | -------------------------------------------------------------------------------- /test/test-db-navigation.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Db navigation') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:suite.before() abort 5 | call SetupTestDbs() 6 | endfunction 7 | 8 | function! s:suite.after() abort 9 | call Cleanup() 10 | endfunction 11 | 12 | function! s:suite.should_open_dbui() abort 13 | :DBUI 14 | call s:expect(&filetype).to_equal('dbui') 15 | call s:expect(getline(1, '$')).to_equal([ 16 | \ '▸ dadbod_ui_test', 17 | \ '▸ dadbod_ui_testing', 18 | \ ]) 19 | endfunction 20 | 21 | function! s:suite.should_open_db_navigation() abort 22 | normal o 23 | call s:expect(getline(1, '$')).to_equal([ 24 | \ '▾ dadbod_ui_test '.g:db_ui_icons.connection_ok, 25 | \ ' + New query', 26 | \ ' ▸ Saved queries (0)', 27 | \ ' ▸ Tables (2)', 28 | \ '▸ dadbod_ui_testing', 29 | \ ]) 30 | endfunction 31 | -------------------------------------------------------------------------------- /test/test-delete-buffer.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Delete buffer') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:suite.before() abort 5 | call SetupTestDbs() 6 | endfunction 7 | 8 | function! s:suite.after() abort 9 | call Cleanup() 10 | endfunction 11 | 12 | function! s:suite.should_delete_buffer() 13 | :DBUI 14 | norm ojo 15 | :DBUI 16 | call s:expect(search('Buffers', 'w')).to_be_greater_than(0) 17 | call s:expect(search(g:db_ui_icons.buffers.' query', 'w')).to_be_greater_than(0) 18 | norm d 19 | call s:expect(search('Buffers', 'w')).to_equal(0) 20 | call s:expect(search(g:db_ui_icons.buffers.' query', 'w')).to_equal(0) 21 | endfunction 22 | -------------------------------------------------------------------------------- /test/test-disable-mappings.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Disable mappings') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:suite.before() abort 5 | call SetOptionVariable('db_ui_disable_mappings', 1) 6 | call SetupTestDbs() 7 | endfunction 8 | 9 | function! s:suite.after() abort 10 | call UnsetOptionVariable('db_ui_disable_mappings') 11 | call Cleanup() 12 | endfunction 13 | 14 | function! s:suite.should_not_map_o() abort 15 | :DBUI 16 | try 17 | norm o 18 | catch /.*/ 19 | call s:expect(v:exception).to_equal('Vim(normal):E21: Cannot make changes, ''modifiable'' is off') 20 | endtry 21 | call s:expect(getline(1, '$')).to_equal([ 22 | \ '▸ dadbod_ui_test', 23 | \ '▸ dadbod_ui_testing', 24 | \ ]) 25 | endfunction 26 | -------------------------------------------------------------------------------- /test/test-drawer-sections.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Drawer sections') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:suite.before() abort 5 | call SetupTestDbs() 6 | endfunction 7 | 8 | function! s:suite.after() abort 9 | call Cleanup() 10 | endfunction 11 | 12 | function! s:suite.should_show_default_sections() abort 13 | :DBUI 14 | call s:expect(&filetype).to_equal('dbui') 15 | normal o 16 | call s:expect(getline(1, '$')).to_equal([ 17 | \ '▾ dadbod_ui_test '.g:db_ui_icons.connection_ok, 18 | \ ' + New query', 19 | \ ' ▸ Saved queries (0)', 20 | \ ' ▸ Tables (2)', 21 | \ '▸ dadbod_ui_testing', 22 | \ ]) 23 | normal o 24 | endfunction 25 | 26 | function! s:suite.should_show_only_schemas_section() abort 27 | call SetOptionVariable('db_ui_drawer_sections', ['schemas']) 28 | :DBUI 29 | call s:expect(&filetype).to_equal('dbui') 30 | normal o 31 | call s:expect(getline(1, '$')).to_equal([ 32 | \ '▾ dadbod_ui_test '.g:db_ui_icons.connection_ok, 33 | \ ' ▸ Tables (2)', 34 | \ '▸ dadbod_ui_testing', 35 | \ ]) 36 | normal o 37 | call UnsetOptionVariable('db_ui_drawer_sections') 38 | endfunction 39 | 40 | function! s:suite.should_show_only_saved_queries_section() abort 41 | call SetOptionVariable('db_ui_drawer_sections', ['saved_queries']) 42 | :DBUI 43 | call s:expect(&filetype).to_equal('dbui') 44 | normal o 45 | call s:expect(getline(1, '$')).to_equal([ 46 | \ '▾ dadbod_ui_test '.g:db_ui_icons.connection_ok, 47 | \ ' ▸ Saved queries (0)', 48 | \ '▸ dadbod_ui_testing', 49 | \ ]) 50 | normal o 51 | call UnsetOptionVariable('db_ui_drawer_sections') 52 | endfunction 53 | 54 | function! s:suite.should_show_only_new_query_section() abort 55 | call SetOptionVariable('db_ui_drawer_sections', ['new_query']) 56 | :DBUI 57 | call s:expect(&filetype).to_equal('dbui') 58 | normal o 59 | call s:expect(getline(1, '$')).to_equal([ 60 | \ '▾ dadbod_ui_test '.g:db_ui_icons.connection_ok, 61 | \ ' + New query', 62 | \ '▸ dadbod_ui_testing', 63 | \ ]) 64 | normal o 65 | call UnsetOptionVariable('db_ui_drawer_sections') 66 | endfunction 67 | 68 | function! s:suite.should_show_custom_section_order() abort 69 | call SetOptionVariable('db_ui_drawer_sections', ['schemas', 'new_query', 'saved_queries']) 70 | :DBUI 71 | call s:expect(&filetype).to_equal('dbui') 72 | normal o 73 | call s:expect(getline(1, '$')).to_equal([ 74 | \ '▾ dadbod_ui_test '.g:db_ui_icons.connection_ok, 75 | \ ' ▸ Tables (2)', 76 | \ ' + New query', 77 | \ ' ▸ Saved queries (0)', 78 | \ '▸ dadbod_ui_testing', 79 | \ ]) 80 | normal o 81 | call UnsetOptionVariable('db_ui_drawer_sections') 82 | endfunction 83 | -------------------------------------------------------------------------------- /test/test-duplicate-connections-from-same-source.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Connections') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:suite.before() abort 5 | let g:dbs = [ 6 | \ {'name': 'db-ui-database', 'url': 'sqlite:test/dadbod_ui_test.db'}, 7 | \ {'name': 'db-ui-database', 'url': 'sqlite:test/dadbod_ui_test.db'}, 8 | \ ] 9 | endfunction 10 | 11 | function! s:suite.after() abort 12 | call Cleanup() 13 | endfunction 14 | 15 | function! s:suite.should_return_error_on_duplicate_connnections_from_same_source() abort 16 | :DBUI 17 | call s:expect(db_ui#notifications#get_last_msg()).to_equal('Warning: Duplicate connection name "db-ui-database" in "g:dbs" source. First one added has precedence.') 18 | endfunction 19 | -------------------------------------------------------------------------------- /test/test-find-buffer-in-drawer.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Find buffer') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:suite.before() abort 5 | let self.filename = 'test_random_query.sql' 6 | call writefile(['SELECT * FROM contacts ORDER BY created_at DESC'], self.filename) 7 | call SetupTestDbs() 8 | endfunction 9 | 10 | function! s:suite.after() abort 11 | call Cleanup() 12 | call delete(self.filename) 13 | endfunction 14 | 15 | function! s:suite.should_find_buffer_in_dbui_drawer() abort 16 | :DBUI 17 | norm o3jojojo 18 | call s:expect(getline(1)).to_equal('SELECT * from "contacts" LIMIT 200;') 19 | let bufnr = bufnr('') 20 | :DBUI 21 | norm jo 22 | exe 'b'.bufnr 23 | :DBUI 24 | call s:expect(getline('.')).to_equal(g:db_ui_icons.expanded.db.' dadbod_ui_test '.g:db_ui_icons.connection_ok) 25 | wincmd p 26 | :DBUIFindBuffer 27 | call s:expect(&filetype).to_equal('sql') 28 | wincmd p 29 | call s:expect(&filetype).to_equal('dbui') 30 | call s:expect(getline('.')).to_match('^ '.g:db_ui_icons.buffers.' contacts-List-.*$') 31 | endfunction 32 | 33 | function! s:suite.should_find_non_dbui_buffer_in_dbui_drawer() abort 34 | let filename = fnamemodify(self.filename, ':p') 35 | exe 'edit '.filename 36 | call s:expect(expand('%:p')).to_equal(filename) 37 | 38 | runtime autoload/db_ui/utils.vim 39 | function! db_ui#utils#inputlist(list) 40 | return 1 41 | endfunction 42 | :DBUIFindBuffer 43 | call s:expect(&filetype).to_equal('sql') 44 | call s:expect(b:dbui_db_key_name).to_equal('dadbod_ui_test_g:dbs') 45 | call s:expect(b:db).to_equal(g:dbs[0].url) 46 | wincmd p 47 | call s:expect(&filetype).to_equal('dbui') 48 | call s:expect(getline(5)).to_equal(' '.g:db_ui_icons.buffers.' '.self.filename) 49 | wincmd p 50 | write 51 | call s:expect(bufname('.dbout')).not.to_be_empty() 52 | call s:expect(getwinvar(bufwinnr('.dbout'), '&previewwindow')).to_equal(1) 53 | endfunction 54 | -------------------------------------------------------------------------------- /test/test-goto-sibling-and-node.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Jump to sibling/node') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:suite.before() abort 5 | call SetupTestDbs() 6 | call SetOptionVariable('db_ui_table_helpers', { 7 | \ 'sqlite': { 'List': 'SELECT * FROM {table}', 'Count': 'select count(*) from {table}', 'Explain': 'EXPLAIN ANALYZE {last_query}' } 8 | \ }) 9 | endfunction 10 | 11 | function! s:suite.after() abort 12 | call SetOptionVariable('db_ui_table_helpers', {'sqlite': {'List': g:db_ui_default_query }}) 13 | call Cleanup() 14 | endfunction 15 | 16 | function! s:suite.should_jump_to_first_last_sibling() abort 17 | :DBUI 18 | norm oj 19 | call s:expect(line('.')).to_equal(2) 20 | exe "norm \" 21 | call s:expect(line('.')).to_equal(4) 22 | exe "norm \" 23 | call s:expect(line('.')).to_equal(2) 24 | norm 2jojo 25 | call s:expect(line('.')).to_equal(5) 26 | exe "norm \" 27 | call s:expect(line('.')).to_equal(13) 28 | exe "norm \" 29 | call s:expect(line('.')).to_equal(5) 30 | norm j 31 | call s:expect(line('.')).to_equal(6) 32 | exe "norm \" 33 | call s:expect(line('.')).to_equal(12) 34 | exe "norm \" 35 | call s:expect(line('.')).to_equal(6) 36 | exe "norm \" 37 | call s:expect(line('.')).to_equal(12) 38 | endfunction 39 | 40 | function! s:suite.should_jump_to_parent_child_node() 41 | exe "norm \" 42 | call s:expect(line('.')).to_equal(5) 43 | exe "norm \" 44 | call s:expect(line('.')).to_equal(4) 45 | exe "norm \" 46 | call s:expect(line('.')).to_equal(1) 47 | 48 | exe "norm \" 49 | call s:expect(line('.')).to_equal(2) 50 | exe "norm \" 51 | call s:expect(line('.')).to_equal(2) 52 | norm 2jo 53 | call s:expect(line('.')).to_equal(4) 54 | call s:expect(getline('.')).to_equal(' ▸ Tables (2)') 55 | exe "norm \" 56 | call s:expect(line('.')).to_equal(5) 57 | call s:expect(getline(4)).to_equal(' ▾ Tables (2)') 58 | exe "norm \" 59 | call s:expect(line('.')).to_equal(6) 60 | endfunction 61 | 62 | function! s:suite.should_jump_to_prev_next_sibling() 63 | norm k 64 | call s:expect(line('.')).to_equal(5) 65 | norm J 66 | call s:expect(line('.')).to_equal(13) 67 | norm K 68 | call s:expect(line('.')).to_equal(5) 69 | endfunction 70 | 71 | function! s:suite.should_jump_to_last_line() 72 | norm! gg 73 | norm J 74 | call s:expect(line('.')).to_equal(14) 75 | norm oj 76 | call s:expect(line('.')).to_equal(15) 77 | exe "norm \" 78 | call s:expect(line('.')).to_equal(17) 79 | exe "norm \" 80 | call s:expect(line('.')).to_equal(15) 81 | exe "norm \" 82 | call s:expect(line('.')).to_equal(17) 83 | endfunction 84 | -------------------------------------------------------------------------------- /test/test-initialization-no-connections.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Initialization no connections') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:suite.after() abort 5 | call delete(g:db_ui_save_location.'/connections.json') 6 | call Cleanup() 7 | endfunction 8 | 9 | function! s:suite.should_open_empty_dbui_with_button_to_add_connection() abort 10 | :DBUI 11 | call s:expect(&filetype).to_equal('dbui') 12 | call s:expect(getline(1)).to_equal('" No connections') 13 | call s:expect(getline(2)).to_equal(g:db_ui_icons.add_connection.' Add connection') 14 | endfunction 15 | 16 | function! s:suite.should_add_connection_from_empty_dbui_drawer() abort 17 | runtime autoload/db_ui/utils.vim 18 | function! db_ui#utils#input(name, val) 19 | if a:name ==? 'Enter connection url: ' 20 | return 'sqlite:test/dadbod_ui_test.db' 21 | endif 22 | 23 | if a:name ==? 'Enter name: ' 24 | return 'test-add-from-empty' 25 | endif 26 | endfunction 27 | 28 | norm jo 29 | call s:expect(getline(1)).to_equal(g:db_ui_icons.collapsed.db.' test-add-from-empty') 30 | endfunction 31 | -------------------------------------------------------------------------------- /test/test-initialization-with-custom-env-variable.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Initialization with custom environment variable') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:suite.before() abort 5 | call SetOptionVariable('db_ui_env_variable_url', 'DATABASE_URL') 6 | let $DATABASE_URL = 'sqlite:test/dadbod_ui_test.db' 7 | endfunction 8 | 9 | function! s:suite.after() abort 10 | call UnsetOptionVariable('db_ui_env_variable_url') 11 | unlet $DATABASE_URL 12 | call Cleanup() 13 | endfunction 14 | 15 | function! s:suite.should_read_env_variable() abort 16 | :DBUI 17 | cal s:expect(&filetype).to_equal('dbui') 18 | call s:expect(getline(1)).to_equal(printf('%s %s', g:db_ui_icons.collapsed.db, 'dadbod_ui_test.db')) 19 | endfunction 20 | -------------------------------------------------------------------------------- /test/test-initialization-with-dbs-array-function-variable.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Initialization with g:dbs variable as functions in a dictionary') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:db_conn() abort 5 | return 'sqlite:test/dadbod_ui_test.db' 6 | endfunction 7 | 8 | function! s:suite.before() abort 9 | call SetupTestDbs() 10 | let g:dbs = [ 11 | \ { 'name': 'dadbod_gdb_test_function', 'url': function('s:db_conn') }, 12 | \ { 'name': 'dadbod_gdb_test_str', 'url': 'sqlite:test/dadbod_ui_test' }, 13 | \ ] 14 | endfunction 15 | 16 | function! s:suite.after() abort 17 | call Cleanup() 18 | endfunction 19 | 20 | function! s:suite.should_read_global_dbs_variable() abort 21 | :DBUI 22 | call s:expect(&filetype).to_equal('dbui') 23 | call s:expect(getline(1)).to_equal(printf('%s %s', g:db_ui_icons.collapsed.db, 'dadbod_gdb_test_function')) 24 | call s:expect(getline(2)).to_equal(printf('%s %s', g:db_ui_icons.collapsed.db, 'dadbod_gdb_test_str')) 25 | norm o 26 | call s:expect(getline(1, '$')).to_equal([ 27 | \ '▾ dadbod_gdb_test_function '.g:db_ui_icons.connection_ok, 28 | \ ' + New query', 29 | \ ' ▸ Saved queries (0)', 30 | \ ' ▸ Tables (2)', 31 | \ '▸ dadbod_gdb_test_str', 32 | \ ]) 33 | endfunction 34 | -------------------------------------------------------------------------------- /test/test-initialization-with-dbs-dictionary-function-variable.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Initialization with g:dbs variable as functions in a dictionary') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:db_conn() abort 5 | return 'sqlite:test/dadbod_ui_test.db' 6 | endfunction 7 | 8 | function! s:suite.before() abort 9 | call SetupTestDbs() 10 | let g:dbs = { 11 | \ 'dadbod_gdb_test_fn': function('s:db_conn'), 12 | \ 'dadbod_gdb_test_string': 'sqlite:test/dadbod_ui_test', 13 | \ } 14 | endfunction 15 | 16 | function! s:suite.after() abort 17 | call Cleanup() 18 | endfunction 19 | 20 | function! s:suite.should_read_global_dbs_variable() abort 21 | :DBUI 22 | call s:expect(&filetype).to_equal('dbui') 23 | call s:expect(getline(1)).to_equal(printf('%s %s', g:db_ui_icons.collapsed.db, 'dadbod_gdb_test_string')) 24 | call s:expect(getline(2)).to_equal(printf('%s %s', g:db_ui_icons.collapsed.db, 'dadbod_gdb_test_fn')) 25 | norm jo 26 | call s:expect(getline(1, '$')).to_equal([ 27 | \ '▸ dadbod_gdb_test_string', 28 | \ '▾ dadbod_gdb_test_fn '.g:db_ui_icons.connection_ok, 29 | \ ' + New query', 30 | \ ' ▸ Saved queries (0)', 31 | \ ' ▸ Tables (2)', 32 | \ ]) 33 | endfunction 34 | -------------------------------------------------------------------------------- /test/test-initialization-with-dbs-variable.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Initialization with g:dbs variable') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:suite.before() abort 5 | call SetupTestDbs() 6 | let g:db = 'sqlite:test/dadbod_gdb_test' 7 | endfunction 8 | 9 | function! s:suite.after() abort 10 | unlet g:db 11 | call Cleanup() 12 | endfunction 13 | 14 | function! s:suite.should_read_global_dbs_variable() abort 15 | :DBUI 16 | call s:expect(&filetype).to_equal('dbui') 17 | call s:expect(getline(1)).to_equal(printf('%s %s', g:db_ui_icons.collapsed.db, 'dadbod_gdb_test')) 18 | call s:expect(getline(2)).to_equal(printf('%s %s', g:db_ui_icons.collapsed.db, 'dadbod_ui_test')) 19 | call s:expect(getline(3)).to_equal(printf('%s %s', g:db_ui_icons.collapsed.db, 'dadbod_ui_testing')) 20 | endfunction 21 | -------------------------------------------------------------------------------- /test/test-initialization-with-dotenv-variable.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Initialization with dotenv variables') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:suite.before() abort 5 | let self.env_filename = '.env' 6 | call writefile(['DB_UI_DEV_DB:sqlite:test/dadbod_ui_test.db', 'DB_UI_PROD_DB:sqlite:test/dadbod_ui_test.db'], self.env_filename) 7 | endfunction 8 | 9 | function! s:suite.after() abort 10 | call delete(self.env_filename) 11 | unlet self.env_filename 12 | unlet $DB_UI_DEV_DB 13 | unlet $DB_UI_PROD_DB 14 | call Cleanup() 15 | endfunction 16 | 17 | function! s:suite.should_read_dotenv_variables() 18 | edit LICENSE 19 | :DBUI 20 | call s:expect(&filetype).to_equal('dbui') 21 | call s:expect(getline(1)).to_match(printf('%s %s', g:db_ui_icons.collapsed.db, '\(dev_db\|prod_db\)')) 22 | call s:expect(getline(2)).to_match(printf('%s %s', g:db_ui_icons.collapsed.db, '\(dev_db\|prod_db\)')) 23 | endfunction 24 | -------------------------------------------------------------------------------- /test/test-initialization-with-env-and-dbs-variable.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Initialization with g:dbs and env variable') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:suite.before() abort 5 | let $DBUI_URL = 'sqlite:test/dadbod_ui_test.db' 6 | let $DBUI_NAME = 'dbui_test' 7 | let g:dbs = {'dbui_testing': 'sqlite:test/dadbod_ui_test.db'} 8 | endfunction 9 | 10 | function! s:suite.after() abort 11 | unlet $DBUI_URL 12 | unlet $DBUI_NAME 13 | call Cleanup() 14 | endfunction 15 | 16 | function! s:suite.should_read_both_env_and_global_dbs_variable() 17 | :DBUI 18 | call s:expect(&filetype).to_equal('dbui') 19 | call s:expect(getline(1)).to_equal(printf('%s %s', g:db_ui_icons.collapsed.db, 'dbui_test')) 20 | call s:expect(getline(2)).to_equal(printf('%s %s', g:db_ui_icons.collapsed.db, 'dbui_testing')) 21 | endfunction 22 | -------------------------------------------------------------------------------- /test/test-initialization-with-env-variable.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Initialization with env variable') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:suite.before() abort 5 | let $DBUI_URL = 'sqlite:test/dadbod_ui_test.db' 6 | endfunction 7 | 8 | function! s:suite.after() abort 9 | unlet $DBUI_URL 10 | call Cleanup() 11 | endfunction 12 | 13 | function! s:suite.should_read_env_variable_and_parse_name_from_connection() 14 | :DBUI 15 | call s:expect(&filetype).to_equal('dbui') 16 | call s:expect(getline(1)).to_equal(printf('%s %s', g:db_ui_icons.collapsed.db, 'dadbod_ui_test.db')) 17 | endfunction 18 | -------------------------------------------------------------------------------- /test/test-mods.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Test mods') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:suite.before() abort 5 | call SetupTestDbs() 6 | endfunction 7 | 8 | function! s:suite.after() abort 9 | call Cleanup() 10 | endfunction 11 | 12 | function! s:suite.should_open_in_new_tab() abort 13 | call s:expect(tabpagenr('$')).to_equal(1) 14 | :tab DBUI 15 | call s:expect(tabpagenr('$')).to_equal(2) 16 | call s:expect(tabpagenr()).to_equal(2) 17 | call s:expect(&filetype).to_equal('dbui') 18 | call s:expect(getline(1)).to_equal(printf('%s %s', g:db_ui_icons.collapsed.db, 'dadbod_ui_test')) 19 | call s:expect(getline(2)).to_equal(printf('%s %s', g:db_ui_icons.collapsed.db, 'dadbod_ui_testing')) 20 | endfunction 21 | -------------------------------------------------------------------------------- /test/test-open-query.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Open query') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:suite.before() abort 5 | call SetupTestDbs() 6 | " Sleep 1 sec to avoid overlapping temp names 7 | sleep 1 8 | endfunction 9 | 10 | function! s:suite.after() abort 11 | call Cleanup() 12 | endfunction 13 | 14 | function! s:suite.should_open_new_query_buffer() abort 15 | :DBUI 16 | norm ojo 17 | call s:expect(&filetype).to_equal('sql') 18 | call s:expect(getline(1)).to_be_empty() 19 | endfunction 20 | 21 | function! s:suite.should_open_contacts_table_list_query() abort 22 | :DBUI 23 | norm 4jojojo 24 | call s:expect(getline(1)).to_equal('SELECT * from "contacts" LIMIT 200;') 25 | call s:expect(db_ui#statusline()).to_equal('DBUI: dadbod_ui_test -> contacts') 26 | call s:expect(db_ui#statusline({'prefix': ''})).to_equal('dadbod_ui_test -> contacts') 27 | call s:expect(db_ui#statusline({'prefix': '', 'separator': ' / '})).to_equal('dadbod_ui_test / contacts') 28 | call s:expect(db_ui#statusline({'prefix': '', 'show': ['db_name'] })).to_equal('dadbod_ui_test') 29 | call s:expect(b:dbui_table_name).to_equal('contacts') 30 | endfunction 31 | 32 | function! s:suite.should_write_query() abort 33 | write 34 | call s:expect(bufname('.dbout')).not.to_be_empty() 35 | call s:expect(getwinvar(bufwinnr('.dbout'), '&previewwindow')).to_equal(1) 36 | pclose 37 | :DBUI 38 | norm G 39 | call s:expect(getline('.')).to_equal(g:db_ui_icons.collapsed.saved_queries.' Query results (1)') 40 | norm o 41 | call s:expect(getline('.')).to_equal(g:db_ui_icons.expanded.saved_queries.' Query results (1)') 42 | norm j 43 | call s:expect(getline('.')).to_match('\'.g:db_ui_icons.tables.' \d\+\.dbout') 44 | call s:expect(bufwinnr('.dbout')).to_equal(-1) 45 | norm o 46 | call s:expect(bufwinnr('.dbout')).to_be_greater_than(-1) 47 | endfunction 48 | -------------------------------------------------------------------------------- /test/test-rename-buffer-and-file.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Rename buffer and saved query') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:suite.before() abort 5 | call SetupTestDbs() 6 | " Sleep 1 sec to avoid overlapping temp names 7 | sleep 1 8 | endfunction 9 | 10 | function s:suite.after() abort 11 | call delete(g:db_ui_save_location.'/dadbod_ui_test', 'rf') 12 | call Cleanup() 13 | endfunction 14 | 15 | function! s:suite.should_rename_buffer() abort 16 | runtime autoload/db_ui/utils.vim 17 | function! db_ui#utils#input(name, val) 18 | return 'custom-buffer-name' 19 | endfunction 20 | 21 | :DBUI 22 | normal ojo 23 | call s:expect(&filetype).to_equal('sql') 24 | call setline(1, ['select * from contacts']) 25 | write 26 | :DBUIFindBuffer 27 | wincmd p 28 | normal r 29 | call s:expect(getline('.')).to_equal(' '.g:db_ui_icons.buffers.' custom-buffer-name *') 30 | endfunction 31 | 32 | function! s:suite.should_rename_saved_query() abort 33 | function! db_ui#utils#input(name, val) 34 | if a:name ==? 'Save as: ' 35 | return 'saved_query.sql' 36 | endif 37 | if a:name ==? 'Enter new name: ' 38 | return 'new_query_name.sql' 39 | endif 40 | endfunction 41 | normal o 42 | normal ,W 43 | call s:expect(filereadable(printf('%s/%s/%s', g:db_ui_save_location, 'dadbod_ui_test', 'saved_query.sql'))).to_be_true() 44 | :DBUI 45 | /Saved queries 46 | norm oj 47 | normal r 48 | call s:expect(filereadable(printf('%s/%s/%s', g:db_ui_save_location, 'dadbod_ui_test', 'new_query_name.sql'))).to_be_true() 49 | call s:expect(filereadable(printf('%s/%s/%s', g:db_ui_save_location, 'dadbod_ui_test', 'saved_query.sql'))).to_be_false() 50 | call s:expect(search('new_query_name.sql')).to_be_greater_than(0) 51 | endfunction 52 | 53 | function! s:suite.should_rename_current_buffer() abort 54 | function! db_ui#utils#input(name, val) 55 | if a:name ==? 'Enter new name: ' 56 | return 'from-command' 57 | endif 58 | endfunction 59 | /custom-buffer-name 60 | norm o 61 | :DBUIRenameBuffer 62 | call s:expect(match(bufname('%'), 'from-command')).to_be_greater_than(0) 63 | :DBUI 64 | call s:expect(search('from-command')).to_be_greater_than(0) 65 | endfunction 66 | -------------------------------------------------------------------------------- /test/test-save-query-to-file.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Save query to file') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:suite.before() abort 5 | call SetupTestDbs() 6 | endfunction 7 | 8 | function s:suite.after() abort 9 | call delete(g:db_ui_save_location.'/dadbod_ui_test', 'rf') 10 | call Cleanup() 11 | endfunction 12 | 13 | function! s:suite.should_save_query_to_file() 14 | runtime autoload/db_ui/utils.vim 15 | function! db_ui#utils#input(name, val) 16 | if a:name ==? 'Save as: ' 17 | return 'test-saved-query' 18 | endif 19 | endfunction 20 | 21 | :DBUI 22 | normal o3jojojo 23 | call s:expect(&filetype).to_equal('sql') 24 | call s:expect(getline(1)).to_equal('SELECT * from "contacts" LIMIT 200;') 25 | normal ,W 26 | :DBUI 27 | /Saved queries 28 | norm oj 29 | call s:expect(getline('.')).to_equal(' '.g:db_ui_icons.saved_query.' test-saved-query') 30 | call s:expect(filereadable(printf('%s/%s/%s', g:db_ui_save_location, 'dadbod_ui_test', 'test-saved-query'))).to_be_true() 31 | endfunction 32 | 33 | function! s:suite.should_delete_saved_query() abort 34 | norm gg6jd 35 | call s:expect(search('test-saved-query')).to_equal(0) 36 | call s:expect(filereadable(printf('%s/%s/%s', g:db_ui_save_location, 'dadbod_ui_test', 'test-saved-query'))).to_be_false() 37 | endfunction 38 | -------------------------------------------------------------------------------- /test/test-show-help.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Help') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:suite.before() abort 5 | call SetOptionVariable('db_ui_show_help', 1) 6 | call SetupTestDbs() 7 | endfunction 8 | 9 | function! s:suite.after() abort 10 | call SetOptionVariable('db_ui_show_help', 0) 11 | call Cleanup() 12 | endfunction 13 | 14 | function! s:suite.should_show_help_text() abort 15 | :DBUI 16 | call s:expect(getline(1)).to_equal('" Press ? for help') 17 | call s:expect(getline(2)).to_be_empty() 18 | call s:expect(getline(3)).to_equal('▸ dadbod_ui_test') 19 | call s:expect(getline(4)).to_equal('▸ dadbod_ui_testing') 20 | normal ? 21 | 22 | call s:expect(getline(3)).to_equal('" o - Open/Toggle selected item') 23 | call s:expect(getline(4)).to_equal('" S - Open/Toggle selected item in vertical split') 24 | call s:expect(getline(5)).to_equal('" d - Delete selected item') 25 | call s:expect(getline(6)).to_equal('" R - Redraw') 26 | call s:expect(getline(7)).to_equal('" A - Add connection') 27 | call s:expect(getline(8)).to_equal('" H - Toggle database details') 28 | call s:expect(getline(9)).to_equal('" r - Rename/Edit buffer/connection/saved query') 29 | call s:expect(getline(10)).to_equal('" q - Close drawer') 30 | call s:expect(getline(11)).to_equal('" / - Go to last/first sibling', 'noaction', 'help', '', '', 0) 31 | call s:expect(getline(12)).to_equal('" K/J - Go to prev/next sibling', 'noaction', 'help', '', '', 0) 32 | call s:expect(getline(13)).to_equal('" / - Go to parent/child node', 'noaction', 'help', '', '', 0) 33 | call s:expect(getline(14)).to_equal('" W - (sql) Save currently opened query') 34 | call s:expect(getline(15)).to_equal('" E - (sql) Edit bind parameters in opened query') 35 | call s:expect(getline(16)).to_equal('" S - (sql) Execute query in visual or normal mode') 36 | call s:expect(getline(17)).to_equal('" - (.dbout) Go to entry from foreign key cell') 37 | call s:expect(getline(18)).to_equal('" ic - (.dbout) Operator pending mapping for cell value') 38 | call s:expect(getline(19)).to_equal('" R - (.dbout) Toggle expanded view') 39 | normal ? 40 | call s:expect(getline(1)).to_equal('" Press ? for help') 41 | call s:expect(getline(2)).to_be_empty() 42 | call s:expect(getline(3)).to_equal('▸ dadbod_ui_test') 43 | call s:expect(getline(4)).to_equal('▸ dadbod_ui_testing') 44 | endfunction 45 | -------------------------------------------------------------------------------- /test/test-table-helpers.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Table helpers') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:suite.before() abort 5 | call SetupTestDbs() 6 | call SetOptionVariable('db_ui_table_helpers', { 7 | \ 'sqlite': { 'List': 'SELECT * FROM {table}', 'Count': 'select count(*) from {table}', 'Explain': 'EXPLAIN ANALYZE {last_query}' } 8 | \ }) 9 | endfunction 10 | 11 | function! s:suite.after() abort 12 | call SetOptionVariable('db_ui_table_helpers', {'sqlite': {'List': g:db_ui_default_query }}) 13 | call Cleanup() 14 | endfunction 15 | 16 | function! s:suite.should_open_table_list_query_changed() abort 17 | :DBUI 18 | normal o3jojojo 19 | call s:expect(&filetype).to_equal('sql') 20 | call s:expect(getline(1)).to_equal('SELECT * FROM contacts') 21 | call s:expect(b:dbui_table_name).to_equal('contacts') 22 | endfunction 23 | 24 | function! s:suite.should_open_custom_count_helper() abort 25 | :DBUI 26 | /Count 27 | normal o 28 | call s:expect(&filetype).to_equal('sql') 29 | call s:expect(getline(1)).to_equal('select count(*) from contacts') 30 | write 31 | endfunction 32 | 33 | function! s:suite.should_open_custom_explain_helper_with_last_query_injected() abort 34 | :DBUI 35 | /Explain 36 | normal o 37 | call s:expect(&filetype).to_equal('sql') 38 | call s:expect(getline(1)).to_equal('EXPLAIN ANALYZE select count(*) from contacts') 39 | endfunction 40 | -------------------------------------------------------------------------------- /test/test-tmp-query-location.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Open query') 2 | let s:expect = themis#helper('expect') 3 | 4 | let s:query_folder = g:db_ui_save_location.'/queries' 5 | 6 | function! s:suite.before() abort 7 | call SetOptionVariable('db_ui_tmp_query_location', s:query_folder) 8 | call SetupTestDbs() 9 | endfunction 10 | 11 | function! s:suite.after() abort 12 | call delete(s:query_folder, 'rf') 13 | call Cleanup() 14 | call SetOptionVariable('db_ui_tmp_query_location', '') 15 | endfunction 16 | 17 | function! s:suite.should_open_contacts_table_list_query() abort 18 | :DBUI 19 | norm jo2jojojojo 20 | call s:expect(getline(1)).to_equal('SELECT * from "contacts" LIMIT 200;') 21 | call s:expect(b:dbui_table_name).to_equal('contacts') 22 | write 23 | call s:expect(filereadable(bufname('%'))).to_be_true() 24 | norm q 25 | call Cleanup() 26 | endfunction 27 | 28 | function! s:suite.should_have_saved_buffers_after_reopen() abort 29 | call SetupTestDbs() 30 | :DBUI 31 | norm jo2joj 32 | call s:expect(getline('.')).to_match('contacts-List') 33 | norm o 34 | call s:expect(getline(1)).to_equal('SELECT * from "contacts" LIMIT 200;') 35 | endfunction 36 | -------------------------------------------------------------------------------- /test/test-toggle-details.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Toggle details') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:suite.before() abort 5 | call SetupTestDbs() 6 | endfunction 7 | 8 | function! s:suite.after() abort 9 | call Cleanup() 10 | endfunction 11 | 12 | function! s:suite.should_show_details() abort 13 | :DBUI 14 | call s:expect(&filetype).to_equal('dbui') 15 | call s:expect(getline(1, '$')).to_equal([ 16 | \ '▸ dadbod_ui_test', 17 | \ '▸ dadbod_ui_testing', 18 | \ ]) 19 | norm H 20 | call s:expect(getline(1, '$')).to_equal([ 21 | \ '▸ dadbod_ui_test (sqlite - g:dbs)', 22 | \ '▸ dadbod_ui_testing (sqlite - g:dbs)', 23 | \ ]) 24 | norm H 25 | call s:expect(getline(1, '$')).to_equal([ 26 | \ '▸ dadbod_ui_test', 27 | \ '▸ dadbod_ui_testing', 28 | \ ]) 29 | endfunction 30 | -------------------------------------------------------------------------------- /test/test-toggle-drawer-and-quit.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Test toggle drawer and quit') 2 | let s:expect = themis#helper('expect') 3 | 4 | function! s:suite.before() abort 5 | call SetupTestDbs() 6 | endfunction 7 | 8 | function! s:suite.after() abort 9 | call Cleanup() 10 | endfunction 11 | 12 | function! s:suite.should_toggle_drawer() abort 13 | :DBUIToggle 14 | call s:expect(&filetype).to_equal('dbui') 15 | call s:expect(getline(1, '$')).to_equal([ 16 | \ '▸ dadbod_ui_test', 17 | \ '▸ dadbod_ui_testing', 18 | \ ]) 19 | :DBUIToggle 20 | call s:expect(bufwinnr('dbui')).to_equal(-1) 21 | endfunction 22 | 23 | function! s:suite.should_quit_drawer() abort 24 | :DBUIToggle 25 | call s:expect(&filetype).to_equal('dbui') 26 | norm q 27 | call s:expect(bufwinnr('dbui')).to_equal(-1) 28 | endfunction 29 | --------------------------------------------------------------------------------