aboutsummaryrefslogtreecommitdiff
path: root/vim/autoload/Pl/Parser.vim
blob: 75c0b8ffe3201a85e47a8782b6bd3bd3fa571d8e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
let g:Pl#Parser#Symbols = {
	\ 'compatible': {
		\   'dividers': [ '', [0x2502], '', [0x2502] ]
		\ , 'symbols' : {
			\   'BRANCH': 'BR:'
			\ , 'RO'    : 'RO'
			\ , 'FT'    : 'FT'
			\ , 'LINE'  : 'LN'
			\ , 'COL'   : 'C'
		\ }
	\ },
	\ 'unicode': {
		\   'dividers': [ [0x25b6], [0x276f], [0x25c0], [0x276e]  ]
		\ , 'symbols' : {
			\   'BRANCH': [0x26a1]
			\ , 'RO'    : [0x2613]
			\ , 'FT'    : [0x2691]
			\ , 'LINE'  : [0x204b]
			\ , 'COL'   : [0x2551]
		\ },
	\ },
	\ 'fancy': {
		\   'dividers': [ [0x2b80], [0x2b81], [0x2b82], [0x2b83] ]
		\ , 'symbols' : {
			\   'BRANCH': [0x2b60]
			\ , 'RO'    : [0x2b64]
			\ , 'FT'    : [0x2b62, 0x2b63]
			\ , 'LINE'  : [0x2b61]
			\ , 'COL'   : [0x2551]
		\ }
	\ }
\ }

let s:LEFT_SIDE = 0
let s:RIGHT_SIDE = 2

let s:PADDING = 1

let s:EMPTY_SEGMENT = { 'type': 'empty' }

let s:HARD_DIVIDER = 0
let s:SOFT_DIVIDER = 1

function! Pl#Parser#GetStatusline(segments) " {{{
	let statusline = {
		\   'n': ''
		\ , 'N': ''
		\ , 'v': ''
		\ , 'i': ''
		\ , 'r': ''
		\ , 's': ''
		\ }

	" Run through the different modes and create the statuslines
	for mode in keys(statusline)
		" Create an empty statusline list
		let stl = []

		call extend(stl, s:ParseSegments(mode, s:LEFT_SIDE, a:segments))

		let statusline[mode] .= join(stl, '')
	endfor

	return statusline
endfunction " }}}
function! Pl#Parser#ParseChars(arg) " {{{
	" Handles symbol arrays which can be either an array of hex values,
	" or a string. Will convert the hex array to a string, or return the
	" string as-is.
	let arg = a:arg

	if type(arg) == type([])
		" Hex array
		call map(arg, 'nr2char(v:val)')

		return join(arg, '')
	endif

	" Anything else, just return it as it is
	return arg
endfunction " }}}
function! s:ParseSegments(mode, side, segments, ...) " {{{
	let mode     = a:mode
	let side     = a:side
	let segments = a:segments

	let level      = exists('a:1') ? a:1 : 0
	let base_color = exists('a:2') ? a:2 : {}

	let ret = []

	for i in range(0, len(segments) - 1)
		unlet! seg_prev seg_curr seg_next

		" Prepare some resources (fetch previous, current and next segment)
		let seg_curr = deepcopy(get(segments, i))

		" Find previous segment
		let seg_prev = s:EMPTY_SEGMENT

		" If we're currently at i = 0 we have to start on 0 or else we will start on the last segment (list[-1])
		let range_start = (i == 0 ? i : i - 1)
		for j in range(range_start, 0, -1)
			let seg = deepcopy(get(segments, j))
			if get(seg, 'name') ==# 'TRUNCATE'
				" Skip truncate segments
				continue
			endif

			" Look ahead for another segment that's visible in this mode
			if index(get(seg, 'modes'), mode) != -1
				" Use this segment
				let seg_prev = seg

				break
			endif
		endfor

		"" Find next segment
		let seg_next = s:EMPTY_SEGMENT

		" If we're currently at i = len(segments) - 1 we have to start on i or else we will get an error because the index doesn't exist
		let range_start = (i == len(segments) - 1 ? i : i + 1)
		for j in range(range_start, len(segments) - 1, 1)
			let seg = deepcopy(get(segments, j))
			if get(seg, 'name') ==# 'TRUNCATE'
				" Skip truncate segments
				continue
			endif

			" Look ahead for another segment that's visible in this mode
			if index(get(seg, 'modes'), mode) != -1
				" Use this segment
				let seg_next = seg

				break
			endif
		endfor

		if index(get(seg_curr, 'modes', []), mode) == -1
			" The segment is invisible in this mode, skip it
			" FIXME When two segments after each other are hidden, a gap appears where the segments would be, this is probably due to segment padding
			continue
		endif

		" Handle the different segment types
		if seg_curr.type == 'segment'
			if seg_curr.name ==# 'TRUNCATE'
				" Truncate statusline
				call add(ret, '%<')
			elseif seg_curr.name ==# 'SPLIT'
				" Split statusline

				" Switch sides
				let side = s:RIGHT_SIDE

				" Handle highlighting
				let mode_colors = get(seg_curr.colors, mode, seg_curr.colors['n'])
				let hl_group = s:HlCreate(mode_colors)

				" Add segment text
				call add(ret, '%#'. hl_group .'#%=')
			else
				" Add segment text
				let text = seg_curr.text

				" Decide on whether to use the group's colors or the segment's colors
				let colors = get(seg_curr, 'colors', base_color)

				" Fallback to normal (current) highlighting if we don't have mode-specific highlighting
				let mode_colors = get(colors, mode, get(colors, 'n', {}))

				if empty(mode_colors)
					echom 'Segment doesn''t have any colors! NS: "'. seg_curr.ns .'" SEG: "'. seg_curr.name .'"'

					continue
				endif

				" Check if we're in a group (level > 0)
				if level > 0
					" If we're in a group we don't have dividers between segments, so we should only pad one side
					let padding_right = (side == s:LEFT_SIDE  ? repeat(' ', s:PADDING) : '')
					let padding_left  = (side == s:RIGHT_SIDE ? repeat(' ', s:PADDING) : '')

					" Check if we lack a bg/fg color for this segment
					" If we do, use the bg/fg color from base_color
					let base_color_mode = ! has_key(base_color, mode) ? base_color['n'] : base_color[mode]

					for col in ['ctermbg', 'ctermfg', 'guibg', 'guifg']
						if empty(mode_colors[col])
							let mode_colors[col] = base_color_mode[col]
						endif
					endfor
				else
					"" If we're outside a group we have dividers and must pad both sides
					let padding_left  = repeat(' ', s:PADDING)
					let padding_right = repeat(' ', s:PADDING)
				endif

				" Get main hl group for segment
				let hl_group = s:HlCreate(mode_colors)

				" Prepare segment text
				let text = '%(%#'. hl_group .'#'. padding_left . text . padding_right . '%)'

				if level == 0
					" Add divider to single segments
					let text = s:AddDivider(text, side, mode, colors, seg_prev, seg_curr, seg_next)
				endif

				call add(ret, text)
			endif
		elseif seg_curr.type == 'segment_group'
			" Recursively parse segment group
			let func_params = [mode, side, seg_curr.segments, level + 1]

			if has_key(seg_curr, 'colors')
				" Pass the base colors on to the child segments
				call add(func_params, seg_curr.colors)
			endif

			" Get segment group string
			let text = join(call('s:ParseSegments', func_params), '')

			" Pad on the opposite side of the divider
			let padding_right = (side == s:RIGHT_SIDE ? repeat(' ', s:PADDING) : '')
			let padding_left  = (side == s:LEFT_SIDE  ? repeat(' ', s:PADDING) : '')

			let text = s:AddDivider(padding_left . text . padding_right, side, mode, seg_curr.colors, seg_prev, seg_curr, seg_next)

			call add(ret, text)
		endif
	endfor

	return ret
endfunction " }}}
function! s:HlCreate(hl) " {{{
	" Create a short and unique highlighting group name
	" It uses the hex values of all the color properties and an attribute flag at the end
	" NONE colors are translated to NN for cterm and NNNNNN for gui colors
	let hi_group = printf('Pl%s%s%s%s%s'
		\ , (a:hl['ctermfg'] == 'NONE' ? 'NN'     : printf('%02x', a:hl['ctermfg']))
		\ , (a:hl['guifg']   == 'NONE' ? 'NNNNNN' : printf('%06x', a:hl['guifg']  ))
		\ , (a:hl['ctermbg'] == 'NONE' ? 'NN'     : printf('%02x', a:hl['ctermbg']))
		\ , (a:hl['guibg']   == 'NONE' ? 'NNNNNN' : printf('%06x', a:hl['guibg']  ))
		\ , substitute(a:hl['attr'], '\v([a-zA-Z])[a-zA-Z]*,?', '\1', 'g')
		\ )

	if ! s:HlExists(hi_group)
		let ctermbg = a:hl['ctermbg'] == 'NONE' ? 'NONE' : printf('%d', a:hl['ctermbg'])
		if (has('win32') || has('win64')) && !has('gui_running') && ctermbg != 'NONE' && ctermbg > 128
			let ctermbg -= 128
		endif
		let hi_cmd = printf('hi %s ctermfg=%s ctermbg=%s cterm=%s guifg=%s guibg=%s gui=%s'
			\ , hi_group
			\ , a:hl['ctermfg'] == 'NONE' ? 'NONE' : printf('%d', a:hl['ctermfg'])
			\ , ctermbg
			\ , a:hl['attr']
			\ , (a:hl['guifg'] == 'NONE' ? 'NONE' : printf('#%06x', a:hl['guifg']))
			\ , (a:hl['guibg'] == 'NONE' ? 'NONE' : printf('#%06x', a:hl['guibg']))
			\ , a:hl['attr']
			\ )

		exec hi_cmd

		" Add command to Pl#HL list for caching
		call add(g:Pl#HL, hi_cmd)
	endif

	" Return only the highlighting group name
	return hi_group
endfunction " }}}
function! s:HlExists(hl) " {{{
	if ! hlexists(a:hl)
		return 0
	endif

	redir => hlstatus
	silent exec 'hi' a:hl
	redir END

	return (hlstatus !~ 'cleared')
endfunction " }}}
function! s:AddDivider(text, side, mode, colors, prev, curr, next) " {{{
	let seg_prev = a:prev
	let seg_curr = a:curr
	let seg_next = a:next

	" Set default color/type for the divider
	let div_colors = get(a:colors, a:mode, a:colors['n'])
	let div_type = s:SOFT_DIVIDER

	" Define segment to compare
	let cmp_seg = a:side == s:LEFT_SIDE ? seg_next : seg_prev

	let cmp_all_colors = get(cmp_seg, 'colors', {})
	let cmp_colors = get(cmp_all_colors, a:mode, get(cmp_all_colors, 'n', {}))

	if ! empty(cmp_colors)
		" Compare the highlighting groups
		"
		" If the background color for cterm is equal, use soft divider with the current segment's highlighting
		" If not, use hard divider with a new highlighting group
		"
		" Note that if the previous/next segment is the split, a hard divider is always used
		if get(div_colors, 'ctermbg') != get(cmp_colors, 'ctermbg') || get(seg_next, 'name') ==# 'SPLIT' || get(seg_prev, 'name') ==# 'SPLIT'
			let div_type = s:HARD_DIVIDER

			" Create new highlighting group
			" Use FG = CURRENT BG, BG = CMP BG
			let div_colors['ctermfg'] = get(div_colors, 'ctermbg')
			let div_colors['guifg']   = get(div_colors, 'guibg')

			let div_colors['ctermbg'] = get(cmp_colors, 'ctermbg')
			let div_colors['guibg']   = get(cmp_colors, 'guibg')

			let div_colors['attr']    = 'NONE'
		endif
	endif

	" Prepare divider
	let divider_raw = deepcopy(g:Pl#Parser#Symbols[g:Powerline_symbols].dividers[a:side + div_type])
	let divider = Pl#Parser#ParseChars(divider_raw)

	" Don't add dividers for segments adjacent to split (unless it's a hard divider)
	if ((get(seg_next, 'name') ==# 'SPLIT' || get(seg_prev, 'name') ==# 'SPLIT') && div_type != s:HARD_DIVIDER)
		return ''
	endif

	if a:side == s:LEFT_SIDE
		" Left side
		" Divider to the right
		return printf('%%(%s%%#%s#%s%%)', a:text, s:HlCreate(div_colors), divider)
	else
		" Right side
		" Divider to the left
		return printf('%%(%%#%s#%s%s%%)', s:HlCreate(div_colors), divider, a:text)
	endif
endfunction " }}}