mirror of
https://github.com/amix/vimrc
synced 2025-06-16 09:35:01 +08:00
Updated plugins
This commit is contained in:
@ -25,7 +25,8 @@ function! go#def#Jump(mode) abort
|
||||
let $GOPATH = old_gopath
|
||||
return
|
||||
endif
|
||||
let command = printf("%s -f=%s -o=%s -t", bin_path, fname, go#util#OffsetCursor())
|
||||
let command = printf("%s -f=%s -o=%s -t", go#util#Shellescape(bin_path),
|
||||
\ go#util#Shellescape(fname), go#util#OffsetCursor())
|
||||
let out = go#util#System(command)
|
||||
if exists("l:tmpname")
|
||||
call delete(l:tmpname)
|
||||
@ -96,6 +97,7 @@ function! s:jump_to_declaration_cb(mode, bin_name, job, exit_status, data) abort
|
||||
endif
|
||||
|
||||
call go#def#jump_to_declaration(a:data[0], a:mode, a:bin_name)
|
||||
call go#util#EchoSuccess(fnamemodify(a:data[0], ":t"))
|
||||
endfunction
|
||||
|
||||
function! go#def#jump_to_declaration(out, mode, bin_name) abort
|
||||
@ -153,9 +155,11 @@ function! go#def#jump_to_declaration(out, mode, bin_name) abort
|
||||
endif
|
||||
|
||||
if a:mode == "tab"
|
||||
let &switchbuf = "usetab"
|
||||
let &switchbuf = "useopen,usetab,newtab"
|
||||
if bufloaded(filename) == 0
|
||||
tab split
|
||||
else
|
||||
let cmd = 'sbuf'
|
||||
endif
|
||||
elseif a:mode == "split"
|
||||
split
|
||||
@ -164,7 +168,7 @@ function! go#def#jump_to_declaration(out, mode, bin_name) abort
|
||||
endif
|
||||
|
||||
" open the file and jump to line and column
|
||||
exec cmd filename
|
||||
exec cmd fnameescape(filename)
|
||||
endif
|
||||
endif
|
||||
call cursor(line, col)
|
||||
|
@ -58,17 +58,21 @@ function! go#doc#OpenBrowser(...) abort
|
||||
endfunction
|
||||
|
||||
function! go#doc#Open(newmode, mode, ...) abort
|
||||
" With argument: run "godoc [arg]".
|
||||
if len(a:000)
|
||||
" check if we have 'godoc' and use it automatically
|
||||
let bin_path = go#path#CheckBinPath('godoc')
|
||||
if empty(bin_path)
|
||||
return
|
||||
endif
|
||||
|
||||
let command = printf("%s %s", bin_path, join(a:000, ' '))
|
||||
let command = printf("%s %s", go#util#Shellescape(bin_path), join(a:000, ' '))
|
||||
let out = go#util#System(command)
|
||||
" Without argument: run gogetdoc on cursor position.
|
||||
else
|
||||
let out = s:gogetdoc(0)
|
||||
if out == -1
|
||||
return
|
||||
endif
|
||||
endif
|
||||
|
||||
if go#util#ShellError() != 0
|
||||
@ -137,7 +141,7 @@ function! s:gogetdoc(json) abort
|
||||
return -1
|
||||
endif
|
||||
|
||||
let cmd = [bin_path]
|
||||
let cmd = [go#util#Shellescape(bin_path)]
|
||||
|
||||
let offset = go#util#OffsetCursor()
|
||||
let fname = expand("%:p:gs!\\!/!")
|
||||
|
@ -69,7 +69,10 @@ function! go#fmt#Format(withGoimport) abort
|
||||
let bin_name = "goimports"
|
||||
endif
|
||||
|
||||
let current_col = col('.')
|
||||
let out = go#fmt#run(bin_name, l:tmpname, expand('%'))
|
||||
let diff_offset = len(readfile(l:tmpname)) - line('$')
|
||||
|
||||
if go#util#ShellError() == 0
|
||||
call go#fmt#update_file(l:tmpname, expand('%'))
|
||||
elseif g:go_fmt_fail_silently == 0
|
||||
@ -95,6 +98,9 @@ function! go#fmt#Format(withGoimport) abort
|
||||
" Restore our cursor/windows positions.
|
||||
call winrestview(l:curw)
|
||||
endif
|
||||
|
||||
" be smart and jump to the line the new statement was added/removed
|
||||
call cursor(line('.') + diff_offset, current_col)
|
||||
endfunction
|
||||
|
||||
" update_file updates the target file with the given formatted source
|
||||
@ -116,15 +122,25 @@ function! go#fmt#update_file(source, target)
|
||||
endif
|
||||
|
||||
" reload buffer to reflect latest changes
|
||||
silent! edit!
|
||||
silent edit!
|
||||
|
||||
let &fileformat = old_fileformat
|
||||
let &syntax = &syntax
|
||||
|
||||
|
||||
" the title information was introduced with 7.4-2200
|
||||
" https://github.com/vim/vim/commit/d823fa910cca43fec3c31c030ee908a14c272640
|
||||
if !has('patch-7.4-2200')
|
||||
return
|
||||
endif
|
||||
|
||||
" clean up previous location list
|
||||
let l:listtype = "locationlist"
|
||||
call go#list#Clean(l:listtype)
|
||||
call go#list#Window(l:listtype)
|
||||
let l:list_title = getqflist({'title': 1})
|
||||
if has_key(l:list_title, "title") && l:list_title['title'] == "Format"
|
||||
let l:listtype = go#list#Type("quickfix")
|
||||
call go#list#Clean(l:listtype)
|
||||
call go#list#Window(l:listtype)
|
||||
endif
|
||||
endfunction
|
||||
|
||||
" run runs the gofmt/goimport command for the given source file and returns
|
||||
@ -166,9 +182,10 @@ function! s:fmt_cmd(bin_name, source, target)
|
||||
endif
|
||||
|
||||
" start constructing the command
|
||||
let bin_path = go#util#Shellescape(bin_path)
|
||||
let cmd = [bin_path]
|
||||
call add(cmd, "-w")
|
||||
|
||||
|
||||
" add the options for binary (if any). go_fmt_options was by default of type
|
||||
" string, however to allow customization it's now a dictionary of binary
|
||||
" name mapping to options.
|
||||
@ -228,7 +245,7 @@ endfunction
|
||||
" show_errors opens a location list and shows the given errors. If the given
|
||||
" errors is empty, it closes the the location list
|
||||
function! s:show_errors(errors) abort
|
||||
let l:listtype = go#list#Type("locationlist")
|
||||
let l:listtype = go#list#Type("quickfix")
|
||||
if !empty(a:errors)
|
||||
call go#list#Populate(l:listtype, a:errors, 'Format')
|
||||
echohl Error | echomsg "Gofmt returned error" | echohl None
|
||||
|
@ -33,7 +33,7 @@ function! go#impl#Impl(...) abort
|
||||
return
|
||||
endif
|
||||
|
||||
let result = go#util#System(printf("%s '%s' '%s'", binpath, recv, iface))
|
||||
let result = go#util#System(join(go#util#Shelllist([binpath, recv, iface], ' ')))
|
||||
if go#util#ShellError() != 0
|
||||
call go#util#EchoError(result)
|
||||
return
|
||||
|
@ -31,10 +31,6 @@ function go#job#Spawn(args)
|
||||
endfunction
|
||||
|
||||
function cbs.exit_cb(job, exitval) dict
|
||||
if has_key(self, 'custom_cb')
|
||||
call self.custom_cb(a:job, a:exitval, self.messages)
|
||||
endif
|
||||
|
||||
if has_key(self, 'error_info_cb')
|
||||
call self.error_info_cb(a:job, a:exitval, self.messages)
|
||||
endif
|
||||
@ -47,6 +43,10 @@ function go#job#Spawn(args)
|
||||
endif
|
||||
endif
|
||||
|
||||
if has_key(self, 'custom_cb')
|
||||
call self.custom_cb(a:job, a:exitval, self.messages)
|
||||
endif
|
||||
|
||||
let l:listtype = go#list#Type("quickfix")
|
||||
if a:exitval == 0
|
||||
call go#list#Clean(l:listtype)
|
||||
|
@ -10,7 +10,8 @@ function! go#keyify#Keyify()
|
||||
endif
|
||||
|
||||
" Get result of command as json, that contains `start`, `end` and `replacement`
|
||||
let command = printf("%s -json %s:#%s", bin_path, fname, go#util#OffsetCursor())
|
||||
let command = printf("%s -json %s:#%s", go#util#Shellescape(bin_path),
|
||||
\ go#util#Shellescape(fname), go#util#OffsetCursor())
|
||||
let output = go#util#System(command)
|
||||
silent! let result = json_decode(output)
|
||||
|
||||
|
@ -121,9 +121,10 @@ function! go#lint#Golint(...) abort
|
||||
if empty(bin_path)
|
||||
return
|
||||
endif
|
||||
let bin_path = go#util#Shellescape(bin_path)
|
||||
|
||||
if a:0 == 0
|
||||
let out = go#util#System(bin_path)
|
||||
let out = go#util#System(bin_path . " " . go#util#Shellescape(go#package#ImportPath()))
|
||||
else
|
||||
let out = go#util#System(bin_path . " " . go#util#Shelljoin(a:000))
|
||||
endif
|
||||
@ -146,9 +147,9 @@ function! go#lint#Vet(bang, ...) abort
|
||||
call go#cmd#autowrite()
|
||||
echon "vim-go: " | echohl Identifier | echon "calling vet..." | echohl None
|
||||
if a:0 == 0
|
||||
let out = go#tool#ExecuteInDir('go vet')
|
||||
let out = go#util#System('go vet ' . go#util#Shellescape(go#package#ImportPath()))
|
||||
else
|
||||
let out = go#tool#ExecuteInDir('go tool vet ' . go#util#Shelljoin(a:000))
|
||||
let out = go#util#System('go tool vet ' . go#util#Shelljoin(a:000))
|
||||
endif
|
||||
|
||||
let l:listtype = "quickfix"
|
||||
@ -188,7 +189,7 @@ function! go#lint#Errcheck(...) abort
|
||||
echon "vim-go: " | echohl Identifier | echon "errcheck analysing ..." | echohl None
|
||||
redraw
|
||||
|
||||
let command = bin_path . ' -abspath ' . import_path
|
||||
let command = go#util#Shellescape(bin_path) . ' -abspath ' . import_path
|
||||
let out = go#tool#ExecuteInDir(command)
|
||||
|
||||
let l:listtype = "quickfix"
|
||||
|
@ -3,8 +3,10 @@ if !exists("g:go_list_type")
|
||||
endif
|
||||
|
||||
" Window opens the list with the given height up to 10 lines maximum.
|
||||
" Otherwise g:go_loclist_height is used. If no or zero height is given it
|
||||
" closes the window
|
||||
" Otherwise g:go_loclist_height is used.
|
||||
"
|
||||
" If no or zero height is given it closes the window by default.
|
||||
" To prevent this, set g:go_list_autoclose = 0
|
||||
function! go#list#Window(listtype, ...) abort
|
||||
let l:listtype = go#list#Type(a:listtype)
|
||||
" we don't use lwindow to close the location list as we need also the
|
||||
@ -13,10 +15,13 @@ function! go#list#Window(listtype, ...) abort
|
||||
" location list increases/decreases, cwindow will not resize when a new
|
||||
" updated height is passed. lopen in the other hand resizes the screen.
|
||||
if !a:0 || a:1 == 0
|
||||
if l:listtype == "locationlist"
|
||||
lclose
|
||||
else
|
||||
cclose
|
||||
let autoclose_window = get(g:, 'go_list_autoclose', 1)
|
||||
if autoclose_window
|
||||
if l:listtype == "locationlist"
|
||||
lclose
|
||||
else
|
||||
cclose
|
||||
endif
|
||||
endif
|
||||
return
|
||||
endif
|
||||
|
@ -83,8 +83,16 @@ function! go#path#Detect() abort
|
||||
" fetched from a customizable list. The user should define any new package
|
||||
" management tool by it's own.
|
||||
|
||||
" src folder outside $GOPATH
|
||||
let src_root = finddir("src", current_dir .";")
|
||||
" src folders outside $GOPATH
|
||||
let src_roots = finddir("src", current_dir .";", -1)
|
||||
|
||||
" for cases like GOPATH/src/foo/src/bar, pick up GOPATH/src instead of
|
||||
" GOPATH/src/foo/src
|
||||
let src_root = ""
|
||||
if len(src_roots) > 0
|
||||
let src_root = src_roots[-1]
|
||||
endif
|
||||
|
||||
if !empty(src_root)
|
||||
let src_path = fnamemodify(src_root, ':p:h:h') . go#util#PathSep()
|
||||
|
||||
@ -129,6 +137,9 @@ function! go#path#BinPath() abort
|
||||
let bin_path = $GOBIN
|
||||
else
|
||||
let go_paths = split(go#path#Default(), go#util#PathListSep())
|
||||
if len(go_paths) == 0
|
||||
return "" "nothing found
|
||||
endif
|
||||
let bin_path = expand(go_paths[0] . "/bin/")
|
||||
endif
|
||||
|
||||
@ -157,6 +168,11 @@ function! go#path#CheckBinPath(binpath) abort
|
||||
let binpath = exepath(binpath)
|
||||
endif
|
||||
let $PATH = old_path
|
||||
|
||||
if go#util#IsUsingCygwinShell() == 1
|
||||
return go#path#CygwinPath(binpath)
|
||||
endif
|
||||
|
||||
return binpath
|
||||
endif
|
||||
|
||||
@ -173,18 +189,15 @@ function! go#path#CheckBinPath(binpath) abort
|
||||
|
||||
let $PATH = old_path
|
||||
|
||||
" When you are using:
|
||||
" 1) Windows system
|
||||
" 2) Has cygpath executable
|
||||
" 3) Use *sh* as 'shell'
|
||||
"
|
||||
" This converts your <path> to $(cygpath '<path>') to make cygwin working in
|
||||
" shell of cygwin way
|
||||
if go#util#IsWin() && executable('cygpath') && &shell !~ '.*sh.*'
|
||||
return printf("$(cygpath '%s')", a:bin_path)
|
||||
endif
|
||||
if go#util#IsUsingCygwinShell() == 1
|
||||
return go#path#CygwinPath(a:binpath)
|
||||
endif
|
||||
|
||||
return go_bin_path . go#util#PathSep() . basename
|
||||
endfunction
|
||||
|
||||
function! go#path#CygwinPath(path)
|
||||
return substitute(a:path, '\\', '/', "g")
|
||||
endfunction
|
||||
|
||||
" vim: sw=2 ts=2 et
|
||||
|
@ -1,12 +1,6 @@
|
||||
" mapped to :GoAddTags
|
||||
function! go#tags#Add(start, end, count, ...) abort
|
||||
let fname = fnamemodify(expand("%"), ':p:gs?\\?/?')
|
||||
if &modified
|
||||
" Write current unsaved buffer to a temp file and use the modified content
|
||||
let l:tmpname = tempname()
|
||||
call writefile(getline(1, '$'), l:tmpname)
|
||||
let fname = l:tmpname
|
||||
endif
|
||||
|
||||
let offset = 0
|
||||
if a:count == -1
|
||||
let offset = go#util#OffsetCursor()
|
||||
@ -14,22 +8,11 @@ function! go#tags#Add(start, end, count, ...) abort
|
||||
|
||||
let test_mode = 0
|
||||
call call("go#tags#run", [a:start, a:end, offset, "add", fname, test_mode] + a:000)
|
||||
|
||||
" if exists, delete it as we don't need it anymore
|
||||
if exists("l:tmpname")
|
||||
call delete(l:tmpname)
|
||||
endif
|
||||
endfunction
|
||||
|
||||
" mapped to :GoRemoveTags
|
||||
function! go#tags#Remove(start, end, count, ...) abort
|
||||
let fname = fnamemodify(expand("%"), ':p:gs?\\?/?')
|
||||
if &modified
|
||||
" Write current unsaved buffer to a temp file and use the modified content
|
||||
let l:tmpname = tempname()
|
||||
call writefile(getline(1, '$'), l:tmpname)
|
||||
let fname = l:tmpname
|
||||
endif
|
||||
|
||||
let offset = 0
|
||||
if a:count == -1
|
||||
let offset = go#util#OffsetCursor()
|
||||
@ -37,11 +20,6 @@ function! go#tags#Remove(start, end, count, ...) abort
|
||||
|
||||
let test_mode = 0
|
||||
call call("go#tags#run", [a:start, a:end, offset, "remove", fname, test_mode] + a:000)
|
||||
|
||||
" if exists, delete it as we don't need it anymore
|
||||
if exists("l:tmpname")
|
||||
call delete(l:tmpname)
|
||||
endif
|
||||
endfunction
|
||||
|
||||
" run runs gomodifytag. This is an internal test so we can test it
|
||||
@ -49,6 +27,10 @@ function! go#tags#run(start, end, offset, mode, fname, test_mode, ...) abort
|
||||
" do not split this into multiple lines, somehow tests fail in that case
|
||||
let args = {'mode': a:mode,'start': a:start,'end': a:end,'offset': a:offset,'fname': a:fname,'cmd_args': a:000}
|
||||
|
||||
if &modified
|
||||
let args["modified"] = 1
|
||||
endif
|
||||
|
||||
let result = s:create_cmd(args)
|
||||
if has_key(result, 'err')
|
||||
call go#util#EchoError(result.err)
|
||||
@ -57,8 +39,15 @@ function! go#tags#run(start, end, offset, mode, fname, test_mode, ...) abort
|
||||
|
||||
let command = join(result.cmd, " ")
|
||||
|
||||
call go#cmd#autowrite()
|
||||
let out = go#util#System(command)
|
||||
if &modified
|
||||
let filename = expand("%:p:gs!\\!/!")
|
||||
let content = join(go#util#GetLines(), "\n")
|
||||
let in = filename . "\n" . strlen(content) . "\n" . content
|
||||
let out = go#util#System(command, in)
|
||||
else
|
||||
let out = go#util#System(command)
|
||||
endif
|
||||
|
||||
if go#util#ShellError() != 0
|
||||
call go#util#EchoError(out)
|
||||
return
|
||||
@ -103,6 +92,16 @@ func s:write_out(out) abort
|
||||
call setline(line, lines[index])
|
||||
let index += 1
|
||||
endfor
|
||||
|
||||
if has_key(result, 'errors')
|
||||
let l:winnr = winnr()
|
||||
let l:listtype = go#list#Type("quickfix")
|
||||
call go#list#ParseFormat(l:listtype, "%f:%l:%c:%m", result['errors'], "gomodifytags")
|
||||
call go#list#Window(l:listtype, len(result['errors']))
|
||||
|
||||
"prevent jumping to quickfix list
|
||||
exe l:winnr . "wincmd w"
|
||||
endif
|
||||
endfunc
|
||||
|
||||
|
||||
@ -116,6 +115,7 @@ func s:create_cmd(args) abort
|
||||
if empty(bin_path)
|
||||
return {'err': "gomodifytags does not exist"}
|
||||
endif
|
||||
let bin_path = go#util#Shellescape(bin_path)
|
||||
|
||||
let l:start = a:args.start
|
||||
let l:end = a:args.end
|
||||
@ -127,9 +127,13 @@ func s:create_cmd(args) abort
|
||||
" start constructing the command
|
||||
let cmd = [bin_path]
|
||||
call extend(cmd, ["-format", "json"])
|
||||
call extend(cmd, ["-file", a:args.fname])
|
||||
call extend(cmd, ["-file", go#util#Shellescape(a:args.fname)])
|
||||
call extend(cmd, ["-transform", l:modifytags_transform])
|
||||
|
||||
if has_key(a:args, "modified")
|
||||
call add(cmd, "-modified")
|
||||
endif
|
||||
|
||||
if l:offset != 0
|
||||
call extend(cmd, ["-offset", l:offset])
|
||||
else
|
||||
|
@ -21,7 +21,7 @@ function! go#template#create() abort
|
||||
let l:template_file = get(g:, 'go_template_file', "hello_world.go")
|
||||
endif
|
||||
let l:template_path = go#util#Join(l:root_dir, "templates", l:template_file)
|
||||
exe '0r ' . fnameescape(l:template_path)
|
||||
silent exe '0r ' . fnameescape(l:template_path)
|
||||
elseif l:package_name == -1 && l:go_template_use_pkg == 1
|
||||
" cwd is now the dir of the package
|
||||
let l:path = fnamemodify(getcwd(), ':t')
|
||||
@ -33,9 +33,6 @@ function! go#template#create() abort
|
||||
endif
|
||||
$delete _
|
||||
|
||||
" Remove the '... [New File]' message line from the command line
|
||||
echon
|
||||
|
||||
execute cd . fnameescape(dir)
|
||||
endfunction
|
||||
|
||||
|
@ -1,11 +1,42 @@
|
||||
function! go#tool#Files() abort
|
||||
if go#util#IsWin()
|
||||
let format = '{{range $f := .GoFiles}}{{$.Dir}}\{{$f}}{{printf \"\n\"}}{{end}}{{range $f := .CgoFiles}}{{$.Dir}}\{{$f}}{{printf \"\n\"}}{{end}}'
|
||||
else
|
||||
let format = "{{range $f := .GoFiles}}{{$.Dir}}/{{$f}}{{printf \"\\n\"}}{{end}}{{range $f := .CgoFiles}}{{$.Dir}}/{{$f}}{{printf \"\\n\"}}{{end}}"
|
||||
" From "go list -h".
|
||||
function! go#tool#ValidFiles(...)
|
||||
let l:list = ["GoFiles", "CgoFiles", "IgnoredGoFiles", "CFiles", "CXXFiles",
|
||||
\ "MFiles", "HFiles", "FFiles", "SFiles", "SwigFiles", "SwigCXXFiles",
|
||||
\ "SysoFiles", "TestGoFiles", "XTestGoFiles"]
|
||||
|
||||
" Used as completion
|
||||
if len(a:000) > 0
|
||||
let l:list = filter(l:list, 'strpart(v:val, 0, len(a:1)) == a:1')
|
||||
endif
|
||||
let command = 'go list -f '.shellescape(format)
|
||||
let out = go#tool#ExecuteInDir(command)
|
||||
|
||||
return l:list
|
||||
endfunction
|
||||
|
||||
function! go#tool#Files(...) abort
|
||||
if len(a:000) > 0
|
||||
let source_files = a:000
|
||||
else
|
||||
let source_files = ['GoFiles']
|
||||
endif
|
||||
|
||||
let combined = ''
|
||||
for sf in source_files
|
||||
" Strip dot in case people used ":GoFiles .GoFiles".
|
||||
let sf = substitute(sf, '^\.', '', '')
|
||||
|
||||
" Make sure the passed options are valid.
|
||||
if index(go#tool#ValidFiles(), sf) == -1
|
||||
echoerr "unknown source file variable: " . sf
|
||||
endif
|
||||
|
||||
if go#util#IsWin()
|
||||
let combined .= '{{range $f := .' . sf . '}}{{$.Dir}}\{{$f}}{{printf \"\n\"}}{{end}}{{range $f := .CgoFiles}}{{$.Dir}}\{{$f}}{{printf \"\n\"}}{{end}}'
|
||||
else
|
||||
let combined .= "{{range $f := ." . sf . "}}{{$.Dir}}/{{$f}}{{printf \"\\n\"}}{{end}}{{range $f := .CgoFiles}}{{$.Dir}}/{{$f}}{{printf \"\\n\"}}{{end}}"
|
||||
endif
|
||||
endfor
|
||||
|
||||
let out = go#tool#ExecuteInDir('go list -f ' . shellescape(combined))
|
||||
return split(out, '\n')
|
||||
endfunction
|
||||
|
||||
|
@ -43,6 +43,14 @@ function! go#util#IsWin() abort
|
||||
return 0
|
||||
endfunction
|
||||
|
||||
" Checks if using:
|
||||
" 1) Windows system,
|
||||
" 2) And has cygpath executable,
|
||||
" 3) And uses *sh* as 'shell'
|
||||
function! go#util#IsUsingCygwinShell()
|
||||
return go#util#IsWin() && executable('cygpath') && &shell =~ '.*sh.*'
|
||||
endfunction
|
||||
|
||||
function! go#util#has_job() abort
|
||||
" job was introduced in 7.4.xxx however there are multiple bug fixes and one
|
||||
" of the latest is 8.0.0087 which is required for a stable async API.
|
||||
@ -102,7 +110,7 @@ function! go#util#osarch() abort
|
||||
return go#util#env("goos") . '_' . go#util#env("goarch")
|
||||
endfunction
|
||||
|
||||
" System runs a shell command. If possible, it will temporary set
|
||||
" System runs a shell command. If possible, it will temporary set
|
||||
" the shell to /bin/sh for Unix-like systems providing a Bourne
|
||||
" POSIX like environment.
|
||||
function! go#util#System(str, ...) abort
|
||||
|
Reference in New Issue
Block a user