I went a little overboard… turns out parsing the isfname
format is hard!
This parses iskeyword
into a single list of strings. Each string is either a single character (included) or a character preceded by ^
(excluded). Also, the strings isalpha()
(included) and ^isalpha()
(excluded) stand for all characters accepted by the C function.
You could shorten the range displays if you like with some tweaks. You could also put in join(',')
and get a comma-separated list.
For example, after :setlocal iskeyword=@,48-57,_,192-255,-
, we can do
:echo ParseIsFnameChars(&l:iskeyword)
['-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '_', 'isalpha()', 'À', 'Á', 'Â', 'Ã', 'Ä', 'Å', 'Æ', 'Ç', 'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï', 'Ð', 'Ñ', 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', '×', 'Ø', 'Ù', 'Ú', 'Û', 'Ü', 'Ý', 'Þ', 'ß', 'à', 'á', 'â', 'ã', 'ä', 'å', 'æ', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï', 'ð', 'ñ', 'ò', 'ó', 'ô', 'õ', 'ö', '÷', 'ø', 'ù', 'ú', 'û', 'ü', 'ý', 'þ', 'ÿ']
Here's the function. Suggestions for improvement welcome 😅
" The format of this option is a list of parts, separated with commas.
" Each part can be a single character number or a range. A range is two
" character numbers with '-' in between. A character number can be a
" decimal number between 0 and 255 or the ASCII character itself (does
" not work for digits). Example:
" "_,-,128-140,#-43" (include '_' and '-' and the range
" 128 to 140 and '#' to 43)
" If a part starts with '^', the following character number or range
" will be excluded from the option. The option is interpreted from left
" to right. Put the excluded character after the range where it is
" included. To include '^' itself use it as the last character of the
" option or the end of a range. Example:
" "^a-z,#,^" (exclude 'a' to 'z', include '#' and '^')
" If the character is '@', all characters where isalpha() returns TRUE
" are included. Normally these are the characters a to z and A to Z,
" plus accented characters. To include '@' itself use "@-@". Examples:
" "@,^a-z" All alphabetic characters, excluding lower
" case ASCII letters.
" "a-z,A-Z,@-@" All letters plus the '@' character.
" A comma can be included by using it where a character number is
" expected. Example:
" "48-57,,,_" Digits, comma and underscore.
" A comma can be excluded by prepending a '^'. Example:
" " -~,^,,9" All characters from space to '~', excluding
" comma, plus <Tab>.
" See |option-backslash| about including spaces and backslashes.
function ParseIsFnameChars(value) abort
const parts = split(a:value, ',', v:true)
const num_parts = len(parts)
let result = []
let index = 0
let last_caret = v:false
for part in parts
let part_to_parse = substitute(part, '^\^', '', '')
let part_ends = split(part_to_parse, '-')
let num_ends = len(part_ends)
if part_to_parse is# '-' " => num_ends is# 0
" just -
let parsed = ['-']
elseif empty(part_to_parse) " => num_ends is# 0
let parsed = [',']
elseif num_ends is# 1
" single items
let parsed = [part_to_parse =~# '\d\+'
\ ? str2nr(part_to_parse)->nr2char()
\ : part_to_parse is# '@'
\ ? 'isalpha()'
\ : part_to_parse]
elseif num_ends is# 2 && part is# '@-@'
let parsed = ['@']
elseif num_ends is# 2 " && part isnot# '@-@'
let left_end_parsed = part_ends[0] =~# '\d\+'
\ ? str2nr(part_ends[0])
\ : char2nr(part_ends[0])
let right_end_parsed = part_ends[1] =~# '\d\+'
\ ? str2nr(part_ends[1])
\ : char2nr(part_ends[1])
let parsed = range(left_end_parsed, right_end_parsed)->map({_, v -> nr2char(v)})
else
throw "ParseIsFnameChars: ".part." is not formatted correctly: wrong number of subparts to -"
endif
if part =~# '^\^$' && index is# num_parts-1
" include a '^'
let result += ['^']
elseif part =~# '^\^$' " && index isnot# num_parts-1
" preceding a comma
let last_caret = v:true
elseif last_caret
if empty(part)
call filter(result, {_, v -> v isnot# ','})
let result += ['^,']
else
throw "ParseIsFnameChars: ".part." is not formatted correctly: non-comma after non-ending ^"
endif
let last_caret = v:false
elseif part =~# '^\^'
" exclude
call filter(result, {_, v -> index(parsed, v) >= 0})
let result += map(parsed, '"^".v:val')
else
" include
let to_remove = mapnew(parsed, '"^".v:val')
call filter(result, {_, v -> index(to_remove, v) < 0})
let result += parsed
endif
let index += 1
endfor
return result->sort()->uniq()
endfunction
Martin and I had the following conversation in chat that I thought was worth preserving:
Martin Tournoij
@D.BenKnoble Re: https://vi.stackexchange.com/a/31564/51
Good grief ...
That's all I have to say about that 🙃
Actually, I do have some more to say: something like a keyword()
function wouldn't be a bad idea; shouldn't be too hard to add to Vim
That's what I would have done anyway, usually patches like this should get accepted fairly quickly
And seems like a useful function to have
e.g. keyword()
→ returns all keyword characters as an array
Actually seems less effort than that Vim function heh, and the Vim codebase actually isn't as bad as some people make it out to be; some parts are hairy, but a new function is something even a bad C programmer like me can do :-)
as long as you stay away from the whole screen update stuff and such
D. Ben Knoble
@MartinTournoij Honestly the function is pretty inefficient because of all the list stuff. Worse though is that it’s not enough to just include all the keyword characters. For one, there’s @ for isalpha; for two, you can exclude characters. I think it would need to turn more information (possibly a dict?). The good news is the vim codebase should already have some logic for parsing iskeyword. (I also may have spotted a bug in my code lol)
Martin Tournoij
It's init_chartab()
in charset.c
, but right now it just sets the values on the buffer directly rather than parsing it in to a struct or some such and using that to set the buffer values, so that would need some rewriting
I thought I might cook up a quick patch, but this is too much work 🙃 Especially since I don't really have a use case for this myself
D. Ben Knoble
For anyone else: https://github.com/vim/vim/blob/4781d6fd8670af415c3b78f00d70036af85bd286/src/charset.c#L150