diff --git a/autoload/airline/builder.vim b/autoload/airline/builder.vim index 38dc83c..5853c23 100644 --- a/autoload/airline/builder.vim +++ b/autoload/airline/builder.vim @@ -22,7 +22,19 @@ function! s:prototype.add_raw(text) dict call add(self._sections, ['', a:text]) endfunction -function! s:get_prev_group(sections, i) +function! s:prototype.insert_section(group, contents, position) dict + call insert(self._sections, [a:group, a:contents], a:position) +endfunction + +function! s:prototype.insert_raw(text, position) dict + call insert(self._sections, ['', a:text], a:position) +endfunction + +function! s:prototype.get_position() dict + return len(self._sections) +endfunction + +function! airline#builder#get_prev_group(sections, i) let x = a:i - 1 while x >= 0 let group = a:sections[x][0] @@ -34,6 +46,19 @@ function! s:get_prev_group(sections, i) return '' endfunction +function! airline#builder#get_next_group(sections, i) + let x = a:i + 1 + let l = len(a:sections) + while x < l + let group = a:sections[x][0] + if group != '' && group != '|' + return group + endif + let x = x + 1 + endwhile + return '' +endfunction + function! s:prototype.build() dict let side = 1 let line = '' @@ -48,7 +73,7 @@ function! s:prototype.build() dict let group = section[0] let contents = section[1] let pgroup = prev_group - let prev_group = s:get_prev_group(self._sections, i) + let prev_group = airline#builder#get_prev_group(self._sections, i) if group ==# 'airline_c' && &buftype ==# 'terminal' && self._context.active let group = 'airline_term' elseif group ==# 'airline_c' && !self._context.active && has_key(self._context, 'bufnr') @@ -102,7 +127,7 @@ function! s:prototype.build() dict return line endfunction -function! s:should_change_group(group1, group2) +function! airline#builder#should_change_group(group1, group2) if a:group1 == a:group2 return 0 endif @@ -132,7 +157,7 @@ function! s:get_transitioned_seperator(self, prev_group, group, side) endfunction function! s:get_seperator(self, prev_group, group, side) - if s:should_change_group(a:prev_group, a:group) + if airline#builder#should_change_group(a:prev_group, a:group) return s:get_transitioned_seperator(a:self, a:prev_group, a:group, a:side) else return a:side ? a:self._context.left_alt_sep : a:self._context.right_alt_sep diff --git a/autoload/airline/extensions/tabline.vim b/autoload/airline/extensions/tabline.vim index 9700023..52cf19b 100644 --- a/autoload/airline/extensions/tabline.vim +++ b/autoload/airline/extensions/tabline.vim @@ -181,7 +181,7 @@ function! airline#extensions#tabline#new_builder() let builder_context.left_alt_sep = get(g:, 'airline#extensions#tabline#left_alt_sep' , '|') endif - return airline#builder#new(builder_context) + return airline#extensions#tabline#builder#new(builder_context) endfunction function! airline#extensions#tabline#group_of_bufnr(tab_bufs, bufnr) diff --git a/autoload/airline/extensions/tabline/buffers.vim b/autoload/airline/extensions/tabline/buffers.vim index f137ef1..ea19df1 100644 --- a/autoload/airline/extensions/tabline/buffers.vim +++ b/autoload/airline/extensions/tabline/buffers.vim @@ -52,7 +52,7 @@ function! airline#extensions#tabline#buffers#get() " no-op endtry let cur = bufnr('%') - if cur == s:current_bufnr + if cur == s:current_bufnr && &columns == s:column_width if !g:airline_detect_modified || getbufvar(cur, '&modified') == s:current_modified return s:current_tabline endif @@ -69,24 +69,45 @@ function! airline#extensions#tabline#buffers#get() if show_buf_label_first call airline#extensions#tabline#add_label(b, 'buffers') endif - let pgroup = '' - for nr in s:get_visible_buffers() - if nr < 0 - call b.add_raw('%#airline_tabhid#...') - continue + + let b.tab_bufs = tabpagebuflist(tabpagenr()) + + let b.overflow_group = 'airline_tabhid' + let b.buffers = airline#extensions#tabline#buflist#list() + if get(g:, 'airline#extensions#tabline#current_first', 0) + if index(b.buffers, cur) > -1 + call remove(b.buffers, index(b.buffers, cur)) endif + let b.buffers = [cur] + b.buffers + endif - let group = airline#extensions#tabline#group_of_bufnr(tab_bufs, nr) - - if nr == cur + function! b.get_group(i) dict + let bufnum = get(self.buffers, a:i, -1) + if bufnum == -1 + return '' + endif + let group = airline#extensions#tabline#group_of_bufnr(self.tab_bufs, bufnum) + if bufnum == bufnr('%') let s:current_modified = (group == 'airline_tabmod') ? 1 : 0 endif + return group + endfunction - " Neovim feature: Have clickable buffers - if has("tablineat") - call b.add_raw('%'.nr.'@airline#extensions#tabline#buffers#clickbuf@') - endif + if has("tablineat") + function! b.get_pretitle(i) dict + let bufnum = get(self.buffers, a:i, -1) + return '%'.bufnum.'@airline#extensions#tabline#buffers#clickbuf@' + endfunction + function b.get_posttitle(i) dict + return '%X' + endfunction + endif + + function! b.get_title(i) dict + let bufnum = get(self.buffers, a:i, -1) + let group = self.get_group(a:i) + let pgroup = self.get_group(a:i - 1) if get(g:, 'airline_powerline_fonts', 0) let space = s:spc else @@ -95,20 +116,18 @@ function! airline#extensions#tabline#buffers#get() if get(g:, 'airline#extensions#tabline#buffer_idx_mode', 0) if len(s:number_map) > 0 - call b.add_section(group, space. get(s:number_map, index, '') . '%(%{airline#extensions#tabline#get_buffer_name('.nr.')}%)' . s:spc) + return space. get(s:number_map, a:i, '') . '%(%{airline#extensions#tabline#get_buffer_name('.bufnum.')}%)' . s:spc else - call b.add_section(group, '['.index.s:spc.'%(%{airline#extensions#tabline#get_buffer_name('.nr.')}%)'.']') + return '['.a:i.s:spc.'%(%{airline#extensions#tabline#get_buffer_name('.bufnum.')}%)'.']' endif - let index += 1 else - call b.add_section(group, space.'%(%{airline#extensions#tabline#get_buffer_name('.nr.')}%)'.s:spc) + return space.'%(%{airline#extensions#tabline#get_buffer_name('.bufnum.')}%)'.s:spc endif + endfunction - if has("tablineat") - call b.add_raw('%X') - endif - let pgroup=group - endfor + let current_buffer = max([index(b.buffers, cur), 0]) + let last_buffer = len(b.buffers) - 1 + call b.insert_titles(current_buffer, 0, last_buffer) call b.add_section('airline_tabfill', '') call b.split() @@ -122,69 +141,18 @@ function! airline#extensions#tabline#buffers#get() endif let s:current_bufnr = cur + let s:column_width = &columns let s:current_tabline = b.build() + let s:current_visible_buffers = copy(b.buffers) + if b._right_title <= last_buffer + call remove(s:current_visible_buffers, b._right_title, last_buffer) + endif + if b._left_title > 0 + call remove(s:current_visible_buffers, 0, b._left_title) + endif return s:current_tabline endfunction -function! s:get_visible_buffers() - let buffers = airline#extensions#tabline#buflist#list() - let cur = bufnr('%') - if get(g:, 'airline#extensions#tabline#current_first', 0) - if index(buffers, cur) > -1 - call remove(buffers, index(buffers, cur)) - endif - let buffers = [cur] + buffers - endif - - let total_width = 0 - let max_width = 0 - - for nr in buffers - let width = len(airline#extensions#tabline#get_buffer_name(nr)) + 4 - let total_width += width - let max_width = max([max_width, width]) - endfor - - " only show current and surrounding buffers if there are too many buffers - let position = index(buffers, cur) - let vimwidth = &columns - if total_width > vimwidth && position > -1 - let buf_count = len(buffers) - - " determine how many buffers to show based on the longest buffer width, - " use one on the right side and put the rest on the left - let buf_max = vimwidth / max_width - let buf_right = 1 - let buf_left = max([0, buf_max - buf_right]) - - let start = max([0, position - buf_left]) - let end = min([buf_count, position + buf_right]) - - " fill up available space on the right - if position < buf_left - let end += (buf_left - position) - endif - - " fill up available space on the left - if end > buf_count - 1 - buf_right - let start -= max([0, buf_right - (buf_count - 1 - position)]) - endif - - let buffers = eval('buffers[' . start . ':' . end . ']') - - if start > 0 - call insert(buffers, -1, 0) - endif - - if end < buf_count - 1 - call add(buffers, -1) - endif - endif - - let s:current_visible_buffers = buffers - return buffers -endfunction - function! s:select_tab(buf_index) " no-op when called in 'keymap_ignored_filetypes' if count(get(g:, 'airline#extensions#tabline#keymap_ignored_filetypes', diff --git a/autoload/airline/extensions/tabline/builder.vim b/autoload/airline/extensions/tabline/builder.vim new file mode 100644 index 0000000..20964b1 --- /dev/null +++ b/autoload/airline/extensions/tabline/builder.vim @@ -0,0 +1,232 @@ +" MIT License. Copyright (c) 2013-2018 Bailey Ling et al. +" vim: et ts=2 sts=2 sw=2 + +scriptencoding utf-8 + +let s:prototype = {} + +" Set the point in the tabline where the builder should insert the titles. +" +" Subsequent calls will overwrite the previous ones, so only the last call +" determines to location to insert titles. +" +" NOTE: The titles are not inserted until |build| is called, so that the +" remaining contents of the tabline can be taken into account. +" +" Callers should define at least |get_title| and |get_group| on the host +" object before calling |build|. +function! s:prototype.insert_titles(current, first, last) dict + let self._first_title = a:first " lowest index + let self._last_title = a:last " highest index + let self._left_title = a:current " next index to add on the left + let self._right_title = a:current + 1 " next index to add on the right + let self._left_position = self.get_position() " left end of titles + let self._right_position = self._left_position " right end of the titles +endfunction + +" Insert a title for entry number |index|, of group |group| at position |pos|, +" if there is space for it. Returns 1 if it is inserted, 0 otherwise +" +" |force| inserts the title even if there isn't enough space left for it. +" |sep_size| adjusts the size change that the title is considered to take up, +" to account for changes to the separators +" +" The title is defined by |get_title| on the hosting object, called with +" |index| as its only argument. +" |get_pretitle| and |get_posttitle| may be defined on the host object to +" insert some formatting before or after the title. These should be 0-width. +" +" This method updates |_right_position| and |_remaining_space| on the host +" object, if the title is inserted. +function! s:prototype.try_insert_title(index, group, pos, sep_size, force) dict + let title = self.get_title(a:index) + let title_size = s:tabline_evaluated_length(title) + a:sep_size + if a:force || self._remaining_space >= title_size + let pos = a:pos + if has_key(self, "get_pretitle") + call self.insert_raw(self.get_pretitle(a:index), pos) + let self._right_position += 1 + let pos += 1 + endif + + call self.insert_section(a:group, title, pos) + let self._right_position += 1 + let pos += 1 + + if has_key(self, "get_posttitle") + call self.insert_raw(self.get_posttitle(a:index), pos) + let self._right_position += 1 + let pos += 1 + endif + + let self._remaining_space -= title_size + return 1 + endif + return 0 +endfunction + +function! s:get_separator_change(new_group, old_group, end_group, sep_size, alt_sep_size) + return s:get_separator_change_with_end(a:new_group, a:old_group, a:end_group, a:end_group, a:sep_size, a:alt_sep_size) +endfunction + +" Compute the change in size of the tabline caused by separators +" +" This should be kept up-to-date with |s:get_transitioned_seperator| and +" |s:get_separator| in autoload/airline/builder.vim +function! s:get_separator_change_with_end(new_group, old_group, new_end_group, old_end_group, sep_size, alt_sep_size) + let sep_change = 0 + if !empty(a:new_end_group) " Separator between title and the end + let sep_change += airline#builder#should_change_group(a:new_group, a:new_end_group) ? a:sep_size : a:alt_sep_size + endif + if !empty(a:old_group) " Separator between the title and the one adjacent + let sep_change += airline#builder#should_change_group(a:new_group, a:old_group) ? a:sep_size : a:alt_sep_size + if !empty(a:old_end_group) " Remove mis-predicted separator + let sep_change -= airline#builder#should_change_group(a:old_group, a:old_end_group) ? a:sep_size : a:alt_sep_size + endif + endif + return sep_change +endfunction + +" This replaces the build function of the |airline#builder#new| object, to +" insert titles as specified by the last call to |insert_titles| before +" passing to the original build function. +" +" Callers should define at least |get_title| and |get_group| on the host +" object if |insert_titles| has been called on it. +function! s:prototype.build() dict + if has_key(self, '_left_position') && self._first_title <= self._last_title + let self._remaining_space = &columns - s:tabline_evaluated_length(self._build()) + + let center_active = get(g:, 'airline#extensions#tabline#center_active', 0) + + let sep_size = s:tabline_evaluated_length(self._context.left_sep) + let alt_sep_size = s:tabline_evaluated_length(self._context.left_alt_sep) + + let outer_left_group = airline#builder#get_prev_group(self._sections, self._left_position) + let outer_right_group = airline#builder#get_next_group(self._sections, self._right_position) + + let overflow_marker = get(g:, 'airline#extensions#tabline#overflow_marker', g:airline_symbols.ellipsis) + let overflow_marker_size = s:tabline_evaluated_length(overflow_marker) + " Allow space for the markers before we begin filling in titles. + if self._left_title > self._first_title + let self._remaining_space -= overflow_marker_size + + \ s:get_separator_change(self.overflow_group, "", outer_left_group, sep_size, alt_sep_size) + endif + if self._left_title < self._last_title + let self._remaining_space -= overflow_marker_size + + \ s:get_separator_change(self.overflow_group, "", outer_right_group, sep_size, alt_sep_size) + endif + + " Add the current title + let group = self.get_group(self._left_title) + if self._left_title == self._first_title + let sep_change = s:get_separator_change(group, "", outer_left_group, sep_size, alt_sep_size) + else + let sep_change = s:get_separator_change(group, "", self.overflow_group, sep_size, alt_sep_size) + endif + if self._left_title == self._last_title + let sep_change += s:get_separator_change(group, "", outer_right_group, sep_size, alt_sep_size) + else + let sep_change += s:get_separator_change(group, "", self.overflow_group, sep_size, alt_sep_size) + endif + let left_group = group + let right_group = group + let self._left_title -= + \ self.try_insert_title(self._left_title, group, self._left_position, sep_change, 1) + + if get(g:, 'airline#extensions#tabline#current_first', 0) + " always have current title first + let self._left_position += 1 + endif + + if !center_active && self._right_title <= self._last_title + " Add the title to the right + let group = self.get_group(self._right_title) + if self._right_title == self._last_title + let sep_change = s:get_separator_change_with_end(group, right_group, outer_right_group, self.overflow_group, sep_size, alt_sep_size) - overflow_marker_size + else + let sep_change = s:get_separator_change(group, right_group, self.overflow_group, sep_size, alt_sep_size) + endif + let right_group = group + let self._right_title += + \ self.try_insert_title(self._right_title, group, self._right_position, sep_change, 1) + endif + + while self._remaining_space > 0 + let done = 0 + if self._left_title >= self._first_title + " Insert next title to the left + let group = self.get_group(self._left_title) + if self._left_title == self._first_title + let sep_change = s:get_separator_change_with_end(group, left_group, outer_left_group, self.overflow_group, sep_size, alt_sep_size) - overflow_marker_size + else + let sep_change = s:get_separator_change(group, left_group, self.overflow_group, sep_size, alt_sep_size) + endif + let left_group = group + let done = self.try_insert_title(self._left_title, group, self._left_position, sep_change, 0) + let self._left_title -= done + endif + " If center_active is set, this |if| operates as an independent |if|, + " otherwise as an |elif|. + if self._right_title <= self._last_title && (center_active || !done) + " Insert next title to the right + let group = self.get_group(self._right_title) + if self._right_title == self._last_title + let sep_change = s:get_separator_change_with_end(group, right_group, outer_right_group, self.overflow_group, sep_size, alt_sep_size) - overflow_marker_size + else + let sep_change = s:get_separator_change(group, right_group, self.overflow_group, sep_size, alt_sep_size) + endif + let right_group = group + let done = self.try_insert_title(self._right_title, group, self._right_position, sep_change, 0) + let self._right_title += done + endif + if !done + break + endif + endwhile + + if self._left_title >= self._first_title + if get(g:, 'airline#extensions#tabline#current_first', 0) + let self._left_position -= 1 + endif + call self.insert_section(self.overflow_group, overflow_marker, self._left_position) + let self._right_position += 1 + endif + + if self._right_title <= self._last_title + call self.insert_section(self.overflow_group, overflow_marker, self._right_position) + endif + endif + + return self._build() +endfunction + +let s:prototype.overflow_group = 'airline_tab' + +" Extract the text content a tabline will render. (Incomplete). +" +" See :help 'statusline' for the list of fields. +function! s:evaluate_tabline(tabline) + let tabline = a:tabline + let tabline = substitute(tabline, '%{\([^}]\+\)}', '\=eval(submatch(1))', 'g') + let tabline = substitute(tabline, '%#[^#]\+#', '', 'g') + let tabline = substitute(tabline, '%(\([^)]\+\)%)', '\1', 'g') + let tabline = substitute(tabline, '%\d\+[TX]', '', 'g') + let tabline = substitute(tabline, '%=', '', 'g') + let tabline = substitute(tabline, '%\d*\*', '', 'g') + if has('tablineat') + let tabline = substitute(tabline, '%@[^@]\+@', '', 'g') + endif + return tabline +endfunction + +function! s:tabline_evaluated_length(tabline) + return airline#util#strchars(s:evaluate_tabline(a:tabline)) +endfunction + +function! airline#extensions#tabline#builder#new(context) + let builder = airline#builder#new(a:context) + let builder._build = builder.build + call extend(builder, s:prototype, 'force') + return builder +endfunction diff --git a/autoload/airline/extensions/tabline/tabs.vim b/autoload/airline/extensions/tabline/tabs.vim index a808fda..08e3000 100644 --- a/autoload/airline/extensions/tabline/tabs.vim +++ b/autoload/airline/extensions/tabline/tabs.vim @@ -33,45 +33,45 @@ function! airline#extensions#tabline#tabs#get() catch " no-op endtry - if curbuf == s:current_bufnr && curtab == s:current_tabnr + if curbuf == s:current_bufnr && curtab == s:current_tabnr && &columns == s:column_width if !g:airline_detect_modified || getbufvar(curbuf, '&modified') == s:current_modified return s:current_tabline endif endif - let tab_nr_type = get(g:, 'airline#extensions#tabline#tab_nr_type', 0) let b = airline#extensions#tabline#new_builder() call airline#extensions#tabline#add_label(b, 'tabs') - " always have current tabpage first - let tablist = range(1, tabpagenr('$')) - if get(g:, 'airline#extensions#tabline#current_first', 0) - if index(tablist, curtab) > -1 - call remove(tablist, index(tablist, curtab)) - endif - let tablist = [curtab] + tablist - endif - for i in tablist - if i == curtab + + function! b.get_group(i) dict + let curtab = tabpagenr() + let group = 'airline_tab' + if a:i == curtab let group = 'airline_tabsel' if g:airline_detect_modified - for bi in tabpagebuflist(i) + for bi in tabpagebuflist(curtab) if getbufvar(bi, '&modified') let group = 'airline_tabmod' endif endfor endif let s:current_modified = (group == 'airline_tabmod') ? 1 : 0 - else - let group = 'airline_tab' endif + return group + endfunction + + function! b.get_title(i) dict let val = '%(' if get(g:, 'airline#extensions#tabline#show_tab_nr', 1) - let val .= airline#extensions#tabline#tabs#tabnr_formatter(tab_nr_type, i) + let tab_nr_type = get(g:, 'airline#extensions#tabline#tab_nr_type', 0) + let val .= airline#extensions#tabline#tabs#tabnr_formatter(tab_nr_type, a:i) endif - call b.add_section(group, val.'%'.i.'T %{airline#extensions#tabline#title('.i.')} %)') - endfor + + return val.'%'.a:i.'T %{airline#extensions#tabline#title('.a:i.')} %)' + endfunction + + call b.insert_titles(curtab, 1, tabpagenr('$')) call b.add_section('airline_tabfill', '') call b.split() @@ -95,6 +95,7 @@ function! airline#extensions#tabline#tabs#get() let s:current_bufnr = curbuf let s:current_tabnr = curtab + let s:column_width = &columns let s:current_tabline = b.build() return s:current_tabline endfunction diff --git a/autoload/airline/init.vim b/autoload/airline/init.vim index cc00950..7fab767 100644 --- a/autoload/airline/init.vim +++ b/autoload/airline/init.vim @@ -70,7 +70,8 @@ function! airline#init#bootstrap() \ 'spell': 'SPELL', \ 'modified': '+', \ 'space': ' ', - \ 'keymap': 'Keymap:' + \ 'keymap': 'Keymap:', + \ 'ellipsis': '...' \ }, 'keep') if get(g:, 'airline_powerline_fonts', 0) diff --git a/autoload/airline/util.vim b/autoload/airline/util.vim index e0bec92..23c44c4 100644 --- a/autoload/airline/util.vim +++ b/autoload/airline/util.vim @@ -86,3 +86,13 @@ else return 0 endfunction endif + +" Compatibility wrapper for strchars, in case this vim version does not +" have it natively +function! airline#util#strchars(str) + if exists('*strchars') + return strchars(a:str) + else + return strlen(substitute(a:str, '.', 'a', 'g')) + endif +endfunction diff --git a/doc/airline.txt b/doc/airline.txt index ad6ce40..f3b683b 100644 --- a/doc/airline.txt +++ b/doc/airline.txt @@ -709,6 +709,9 @@ with the middle mouse button to delete that buffer. * rename label for tabs (default: 'tabs') (c) > let g:airline#extensions#tabline#tabs_label = 't' +* change the symbol for skipped tabs/buffers (default '...') > + let g:airline#extensions#tabline#overflow_marker = '…' + * always show current tabpage/buffer first > let airline#extensions#tabline#current_first = 1 < default: 0