scriptencoding utf-8

let s:has_nvim_ghost_text = has('nvim-0.8')
let s:vim_minimum_version = '9.0.0185'
let s:has_vim_ghost_text = has('patch-' . s:vim_minimum_version) && has('textprop')
let s:has_ghost_text = s:has_nvim_ghost_text || s:has_vim_ghost_text

let s:hlgroup = 'CopilotSuggestion'
let s:annot_hlgroup = 'CopilotAnnotation'

if s:has_vim_ghost_text && empty(prop_type_get(s:hlgroup))
  call prop_type_add(s:hlgroup, {'highlight': s:hlgroup})
endif
if s:has_vim_ghost_text && empty(prop_type_get(s:annot_hlgroup))
  call prop_type_add(s:annot_hlgroup, {'highlight': s:annot_hlgroup})
endif

function! s:Echo(msg) abort
  if has('nvim') && &cmdheight == 0
    call v:lua.vim.notify(a:msg, v:null, {'title': 'GitHub Copilot'})
  else
    echo a:msg
  endif
endfunction

function! copilot#Init(...) abort
  call copilot#util#Defer({ -> exists('s:client') || s:Start() })
endfunction

function! s:Running() abort
  return exists('s:client.job') || exists('s:client.client_id')
endfunction

function! s:Start() abort
  if s:Running() || exists('s:client.startup_error')
    return
  endif
  let s:client = copilot#client#New()
endfunction

function! s:Stop() abort
  if exists('s:client')
    let client = remove(s:, 'client')
    call client.Close()
  endif
endfunction

function! copilot#Client() abort
  call s:Start()
  return s:client
endfunction

function! copilot#RunningClient() abort
  if s:Running()
    return s:client
  else
    return v:null
  endif
endfunction

if has('nvim-0.8') && !has(luaeval('vim.version().api_prerelease') ? 'nvim-0.9.1' : 'nvim-0.9.0')
  let s:editor_warning = 'Neovim 0.8 support is deprecated and will be dropped in a future release of copilot.vim.'
endif
if has('vim_starting') && exists('s:editor_warning')
  call copilot#logger#Warn(s:editor_warning)
endif
function! s:EditorVersionWarning() abort
  if exists('s:editor_warning')
    echohl WarningMsg
    echo 'Warning: ' . s:editor_warning
    echohl None
  endif
endfunction

function! copilot#Request(method, params, ...) abort
  let client = copilot#Client()
  return call(client.Request, [a:method, a:params] + a:000)
endfunction

function! copilot#Call(method, params, ...) abort
  let client = copilot#Client()
  return call(client.Call, [a:method, a:params] + a:000)
endfunction

function! copilot#Notify(method, params, ...) abort
  let client = copilot#Client()
  return call(client.Notify, [a:method, a:params] + a:000)
endfunction

function! copilot#NvimNs() abort
  return nvim_create_namespace('github-copilot')
endfunction

function! copilot#Clear() abort
  if exists('g:_copilot_timer')
    call timer_stop(remove(g:, '_copilot_timer'))
  endif
  if exists('b:_copilot')
    call copilot#client#Cancel(get(b:_copilot, 'first', {}))
    call copilot#client#Cancel(get(b:_copilot, 'cycling', {}))
  endif
  call s:UpdatePreview()
  unlet! b:_copilot
  return ''
endfunction

function! copilot#Dismiss() abort
  call copilot#Clear()
  call s:UpdatePreview()
  return ''
endfunction

let s:filetype_defaults = {
      \ 'gitcommit': 0,
      \ 'gitrebase': 0,
      \ 'hgcommit': 0,
      \ 'svn': 0,
      \ 'cvs': 0,
      \ '.': 0}

function! s:BufferDisabled() abort
  if &buftype =~# '^\%(help\|prompt\|quickfix\|terminal\)$'
    return 5
  endif
  if exists('b:copilot_disabled')
    return empty(b:copilot_disabled) ? 0 : 3
  endif
  if exists('b:copilot_enabled')
    return empty(b:copilot_enabled) ? 4 : 0
  endif
  let short = empty(&l:filetype) ? '.' : split(&l:filetype, '\.', 1)[0]
  let config = {}
  if type(get(g:, 'copilot_filetypes')) == v:t_dict
    let config = g:copilot_filetypes
  endif
  if has_key(config, &l:filetype)
    return empty(config[&l:filetype])
  elseif has_key(config, short)
    return empty(config[short])
  elseif has_key(config, '*')
    return empty(config['*'])
  else
    return get(s:filetype_defaults, short, 1) == 0 ? 2 : 0
  endif
endfunction

function! copilot#Enabled() abort
  return get(g:, 'copilot_enabled', 1)
        \ && empty(s:BufferDisabled())
endfunction

let s:inline_invoked = 1
let s:inline_automatic = 2

function! copilot#Complete(...) abort
  if exists('g:_copilot_timer')
    call timer_stop(remove(g:, '_copilot_timer'))
  endif
  let target = [bufnr(''), getbufvar('', 'changedtick'), line('.'), col('.')]
  if !exists('b:_copilot.target') || b:_copilot.target !=# target
    if exists('b:_copilot.first')
      call copilot#client#Cancel(b:_copilot.first)
    endif
    if exists('b:_copilot.cycling')
      call copilot#client#Cancel(b:_copilot.cycling)
    endif
    let params = {
          \ 'textDocument': {'uri': bufnr('')},
          \ 'position': copilot#util#AppendPosition(),
          \ 'formattingOptions': {'insertSpaces': &expandtab ? v:true : v:false, 'tabSize': shiftwidth()},
          \ 'context': {'triggerKind': s:inline_automatic}}
    let b:_copilot = {
          \ 'target': target,
          \ 'params': params,
          \ 'first': copilot#Request('textDocument/inlineCompletion', params)}
    let g:_copilot_last = b:_copilot
  endif
  let completion = b:_copilot.first
  if !a:0
    return completion.Await()
  else
    call copilot#client#Result(completion, function(a:1, [b:_copilot]))
    if a:0 > 1
      call copilot#client#Error(completion, function(a:2, [b:_copilot]))
    endif
  endif
endfunction

function! s:HideDuringCompletion() abort
  return get(g:, 'copilot_hide_during_completion', 1)
endfunction

function! s:SuggestionTextWithAdjustments() abort
  let empty = ['', 0, '', {}]
  try
    if mode() !~# '^[iR]' || (s:HideDuringCompletion() && pumvisible()) || !exists('b:_copilot.suggestions')
      return empty
    endif
    let choice = get(b:_copilot.suggestions, b:_copilot.choice, {})
    if !has_key(choice, 'range') || choice.range.start.line != line('.') - 1 || type(choice.insertText) !=# v:t_string
      return empty
    endif
    let line = getline('.')
    let offset = col('.') - 1
    let byte_offset = copilot#util#UTF16ToByteIdx(line, choice.range.start.character)
    let choice_text = strpart(line, 0, byte_offset) .
          \ substitute(substitute(choice.insertText, '\r\n\=', '\n', 'g'), '\n*$', '', '')
    let typed = strpart(line, 0, offset)
    let end_offset = copilot#util#UTF16ToByteIdx(line, choice.range.end.character)
    if end_offset < 0
      let end_offset = len(line)
    endif
    let delete = strpart(line, offset, end_offset - offset)
    if typed =~# '^\s*$'
      let leading = strpart(matchstr(choice_text, '^\s\+'), 0, len(typed))
      let unindented = strpart(choice_text, len(leading))
      if strpart(typed, 0, len(leading)) ==# leading && unindented !=# delete
        return [unindented, len(typed) - len(leading), delete, choice]
      endif
    elseif typed ==# strpart(choice_text, 0, offset)
      return [strpart(choice_text, offset), 0, delete, choice]
    endif
  catch
    call copilot#logger#Exception()
  endtry
  return empty
endfunction


function! s:Advance(count, context, ...) abort
  if a:context isnot# get(b:, '_copilot', {})
    return
  endif
  let a:context.choice += a:count
  if a:context.choice < 0
    let a:context.choice += len(a:context.suggestions)
  endif
  let a:context.choice %= len(a:context.suggestions)
  call s:UpdatePreview()
endfunction

function! s:GetSuggestionsCyclingCallback(context, result) abort
  let callbacks = remove(a:context, 'cycling_callbacks')
  let seen = {}
  for suggestion in a:context.suggestions
    let seen[suggestion.insertText] = 1
  endfor
  for suggestion in get(a:result, 'items', [])
    if !has_key(seen, suggestion.insertText)
      call add(a:context.suggestions, suggestion)
      let seen[suggestion.insertText] = 1
    endif
  endfor
  for Callback in callbacks
    call Callback(a:context)
  endfor
endfunction

function! s:GetSuggestionsCycling(callback) abort
  if exists('b:_copilot.cycling_callbacks')
    call add(b:_copilot.cycling_callbacks, a:callback)
  elseif exists('b:_copilot.cycling')
    call a:callback(b:_copilot)
  elseif exists('b:_copilot.suggestions')
    let params = deepcopy(b:_copilot.first.params)
    let params.context.triggerKind = s:inline_invoked
    let b:_copilot.cycling_callbacks = [a:callback]
    let b:_copilot.cycling = copilot#Request('textDocument/inlineCompletion',
          \ params,
          \ function('s:GetSuggestionsCyclingCallback', [b:_copilot]),
          \ function('s:GetSuggestionsCyclingCallback', [b:_copilot]),
          \ )
    call s:UpdatePreview()
  endif
  return ''
endfunction

function! copilot#Next() abort
  return s:GetSuggestionsCycling(function('s:Advance', [1]))
endfunction

function! copilot#Previous() abort
  return s:GetSuggestionsCycling(function('s:Advance', [-1]))
endfunction

function! copilot#GetDisplayedSuggestion() abort
  let [text, outdent, delete, item] = s:SuggestionTextWithAdjustments()

  return {
        \ 'item': item,
        \ 'text': text,
        \ 'outdentSize': outdent,
        \ 'deleteSize': strchars(delete),
        \ 'deleteChars': delete}
endfunction

function! s:ClearPreview() abort
  if s:has_nvim_ghost_text
    call nvim_buf_del_extmark(0, copilot#NvimNs(), 1)
  elseif s:has_vim_ghost_text
    call prop_remove({'type': s:hlgroup, 'all': v:true})
    call prop_remove({'type': s:annot_hlgroup, 'all': v:true})
  endif
endfunction

function! s:UpdatePreview() abort
  try
    let [text, outdent, delete_chars, item] = s:SuggestionTextWithAdjustments()
    let delete = strchars(delete_chars)
    let text = split(text, "\r\n\\=\\|\n", 1)
    if empty(text[-1])
      call remove(text, -1)
    endif
    if empty(text) || !s:has_ghost_text
      return s:ClearPreview()
    endif
    if exists('b:_copilot.cycling_callbacks')
      let annot = '(1/…)'
    elseif exists('b:_copilot.cycling')
      let annot = '(' . (b:_copilot.choice + 1) . '/' . len(b:_copilot.suggestions) . ')'
    else
      let annot = ''
    endif
    call s:ClearPreview()
    if s:has_nvim_ghost_text
      let data = {'id': 1}
      let data.virt_text_pos = 'overlay'
      let append = strpart(getline('.'), col('.') - 1 + delete)
      let data.virt_text = [[text[0] . append . repeat(' ', delete - len(text[0])), s:hlgroup]]
      if len(text) > 1
        let data.virt_lines = map(text[1:-1], { _, l -> [[l, s:hlgroup]] })
        if !empty(annot)
          let data.virt_lines[-1] += [[' '], [annot, s:annot_hlgroup]]
        endif
      elseif len(annot)
        let data.virt_text += [[' '], [annot, s:annot_hlgroup]]
      endif
      let data.hl_mode = 'combine'
      call nvim_buf_set_extmark(0, copilot#NvimNs(), line('.')-1, col('.')-1, data)
    elseif s:has_vim_ghost_text
      let new_suffix = text[0]
      let current_suffix = getline('.')[col('.') - 1 :]
      let inset = ''
      while delete > 0 && !empty(new_suffix)
        let last_char = matchstr(new_suffix, '.$')
        let new_suffix = matchstr(new_suffix, '^.\{-\}\ze.$')
        if last_char ==# matchstr(current_suffix, '.$')
          if !empty(inset)
            call prop_add(line('.'), col('.') + len(current_suffix), {'type': s:hlgroup, 'text': inset})
            let inset = ''
          endif
          let current_suffix = matchstr(current_suffix, '^.\{-\}\ze.$')
          let delete -= 1
        else
          let inset = last_char . inset
        endif
      endwhile
      if !empty(new_suffix . inset)
        call prop_add(line('.'), col('.'), {'type': s:hlgroup, 'text': new_suffix . inset})
      endif
      for line in text[1:]
        call prop_add(line('.'), 0, {'type': s:hlgroup, 'text_align': 'below', 'text': line})
      endfor
      if !empty(annot)
        call prop_add(line('.'), col('$'), {'type': s:annot_hlgroup, 'text': ' ' . annot})
      endif
    endif
    call copilot#Notify('textDocument/didShowCompletion', {'item': item})
  catch
    return copilot#logger#Exception()
  endtry
endfunction

function! s:HandleTriggerResult(state, result) abort
  let a:state.suggestions = type(a:result) == type([]) ? a:result : get(empty(a:result) ? {} : a:result, 'items', [])
  let a:state.choice = 0
  if get(b:, '_copilot') is# a:state
    call s:UpdatePreview()
  endif
endfunction

function! s:HandleTriggerError(state, result) abort
  let a:state.suggestions = []
  let a:state.choice = 0
  let a:state.error = a:result
  if get(b:, '_copilot') is# a:state
    call s:UpdatePreview()
  endif
endfunction

function! copilot#Suggest() abort
  if !s:Running()
    return ''
  endif
  try
    call copilot#Complete(function('s:HandleTriggerResult'), function('s:HandleTriggerError'))
  catch
    call copilot#logger#Exception()
  endtry
  return ''
endfunction

function! s:Trigger(bufnr, timer) abort
  let timer = get(g:, '_copilot_timer', -1)
  if a:bufnr !=# bufnr('') || a:timer isnot# timer || mode() !=# 'i'
    return
  endif
  unlet! g:_copilot_timer
  return copilot#Suggest()
endfunction

function! copilot#Schedule() abort
  if !s:has_ghost_text || !s:Running() || !copilot#Enabled()
    call copilot#Clear()
    return
  endif
  call s:UpdatePreview()
  let delay = get(g:, 'copilot_idle_delay', 45)
  call timer_stop(get(g:, '_copilot_timer', -1))
  let g:_copilot_timer = timer_start(delay, function('s:Trigger', [bufnr('')]))
endfunction

function! s:Attach(bufnr, ...) abort
  try
    return copilot#Client().Attach(a:bufnr)
  catch
    call copilot#logger#Exception()
  endtry
endfunction

function! copilot#OnFileType() abort
  if empty(s:BufferDisabled()) && &l:modifiable && &l:buflisted
    call copilot#util#Defer(function('s:Attach'), bufnr(''))
  endif
endfunction

function! s:Focus(bufnr, ...) abort
  if s:Running() && copilot#Client().IsAttached(a:bufnr)
    call copilot#Client().Notify('textDocument/didFocus', {'textDocument': {'uri': copilot#Client().Attach(a:bufnr).uri}})
  endif
endfunction

function! copilot#OnBufEnter() abort
  let bufnr = bufnr('')
  call copilot#util#Defer(function('s:Focus'), bufnr)
endfunction

function! copilot#OnInsertLeavePre() abort
  call copilot#Clear()
  call s:ClearPreview()
endfunction

function! copilot#OnInsertEnter() abort
  return copilot#Schedule()
endfunction

function! copilot#OnCompleteChanged() abort
  if s:HideDuringCompletion()
    return copilot#Clear()
  else
    return copilot#Schedule()
  endif
endfunction

function! copilot#OnCursorMovedI() abort
  return copilot#Schedule()
endfunction

function! copilot#OnBufUnload() abort
endfunction

function! copilot#OnVimLeavePre() abort
endfunction

function! copilot#TextQueuedForInsertion() abort
  try
    return remove(s:, 'suggestion_text')
  catch
    return ''
  endtry
endfunction

function! copilot#Accept(...) abort
  let s = copilot#GetDisplayedSuggestion()
  if !empty(s.text)
    unlet! b:_copilot
    let text = ''
    if a:0 > 1
      let text = substitute(matchstr(s.text, "\n*" . '\%(' . a:2 .'\)'), "\n*$", '', '')
    endif
    if empty(text)
      let text = s.text
    endif
    let delete_chars = s.deleteChars
    let leftover = strpart(s.text, strlen(text))
    let idx = strridx(leftover, matchstr(delete_chars, '.$'))
    while !empty(delete_chars) && idx != -1
      let delete_chars = substitute(delete_chars, '.$', '', '')
      let idx = strridx(leftover, matchstr(delete_chars, '.$'), idx - 1)
    endwhile
    if text ==# s.text && has_key(s.item, 'command')
      call copilot#Request('workspace/executeCommand', s.item.command)
    else
      let line_text = strpart(getline('.'), 0, col('.') - 1) . text
      call copilot#Notify('textDocument/didPartiallyAcceptCompletion', {
            \ 'item': s.item,
            \ 'acceptedLength': copilot#util#UTF16Width(line_text) - s.item.range.start.character})
    endif
    call s:ClearPreview()
    let s:suggestion_text = text
    let recall = text =~# "\n" ? "\<C-R>\<C-O>=" : "\<C-R>\<C-R>="
    return repeat("\<Left>\<Del>", s.outdentSize) . repeat("\<Del>", strchars(delete_chars)) .
            \ recall . "copilot#TextQueuedForInsertion()\<CR>" . (a:0 > 1 ? '' : "\<End>")
  endif
  let default = get(g:, 'copilot_tab_fallback', pumvisible() ? "\<C-N>" : "\t")
  if !a:0
    return default
  elseif type(a:1) == v:t_string
    return a:1
  elseif type(a:1) == v:t_func
    try
      return call(a:1, [])
    catch
      return default
    endtry
  else
    return default
  endif
endfunction

function! copilot#AcceptWord(...) abort
  return copilot#Accept(a:0 ? a:1 : '', '\%(\k\@!.\)*\k*')
endfunction

function! copilot#AcceptLine(...) abort
  return copilot#Accept(a:0 ? a:1 : "\r", "[^\n]\\+")
endfunction

function! copilot#Browser() abort
  if type(get(g:, 'copilot_browser')) == v:t_list
    let cmd = copy(g:copilot_browser)
  elseif type(get(g:, 'open_command')) == v:t_list
    let cmd = copy(g:open_command)
  elseif has('win32')
    let cmd = ['rundll32', 'url.dll,FileProtocolHandler']
  elseif has('mac')
    let cmd = ['open']
  elseif executable('wslview')
    return ['wslview']
  elseif executable('xdg-open')
    return ['xdg-open']
  else
    return []
  endif
  if executable(get(cmd, 0, ''))
    return cmd
  else
    return []
  endif
endfunction

let s:commands = {}

function! s:EnabledStatusMessage() abort
  let buf_disabled = s:BufferDisabled()
  if !s:has_ghost_text
    if has('nvim')
      return "Neovim 0.6 required to support ghost text"
    else
      return "Vim " . s:vim_minimum_version . " required to support ghost text"
    endif
  elseif !get(g:, 'copilot_enabled', 1)
    return 'Disabled globally by :Copilot disable'
  elseif buf_disabled is# 5
    return 'Disabled for current buffer by buftype=' . &buftype
  elseif buf_disabled is# 4
    return 'Disabled for current buffer by b:copilot_enabled'
  elseif buf_disabled is# 3
    return 'Disabled for current buffer by b:copilot_disabled'
  elseif buf_disabled is# 2
    return 'Disabled for filetype=' . &filetype . ' by internal default'
  elseif buf_disabled
    return 'Disabled for filetype=' . &filetype . ' by g:copilot_filetypes'
  elseif !copilot#Enabled()
    return 'BUG: Something is wrong with enabling/disabling'
  else
    return ''
  endif
endfunction

function! s:VerifySetup() abort
  let error = copilot#Client().StartupError()
  if !empty(error)
    echo 'Copilot: ' . error
    return
  endif

  if exists('s:client.status.kind') && s:client.status.kind ==# 'Error'
    echo 'Copilot: Error: ' . get(s:client.status, 'message', 'unknown')
    return
  endif

  return 1
endfunction

function! s:commands.status(opts) abort
  if !s:VerifySetup()
    return
  endif

  if exists('s:client.status.kind') && s:client.status.kind ==# 'Warning'
    echo 'Copilot: Warning: ' . get(s:client.status, 'message', 'unknown')
    return
  endif

  let status = s:EnabledStatusMessage()
  if !empty(status)
    echo 'Copilot: ' . status
    return
  endif

  echo 'Copilot: Ready'
  call s:EditorVersionWarning()
endfunction

function! s:commands.signout(opts) abort
  echo 'Copilot: Signed out'
  call copilot#Call('signOut', {})
endfunction

function! s:commands.setup(opts) abort
  let startup_error = copilot#Client().StartupError()
  if !empty(startup_error)
      echo 'Copilot: ' . startup_error
      return
  endif

  let data = copilot#Call('signIn', {})

  if has_key(data, 'verificationUri')
    let uri = data.verificationUri
    if has('clipboard')
      try
        let @+ = data.userCode
      catch
      endtry
      try
        let @* = data.userCode
      catch
      endtry
    endif
    let codemsg = "First copy your one-time code: " . data.userCode . "\n"
    try
      if len(&mouse)
        let mouse = &mouse
        set mouse=
      endif
      if get(a:opts, 'bang')
        call s:Echo(codemsg . "In your browser, visit " . uri)
        let request = copilot#Request('signInConfirm', {})
      else
        call input(codemsg . "Press ENTER to open GitHub in your browser\n")
        let request = copilot#Request('workspace/executeCommand', data.command)
      endif
      call s:Echo("Waiting for " . data.userCode . " at " . uri . " (could take up to 5 seconds)")
      call request.Wait()
    finally
      if exists('mouse')
        let &mouse = mouse
      endif
    endtry
    if request.status ==# 'error'
      return 'echoerr ' . string('Copilot: Authentication failure: ' . request.error.message)
    else
      let data = request.result
    endif
  elseif get(data, 'status', '') isnot# 'AlreadySignedIn'
    return 'echoerr ' . string('Copilot: Something went wrong')
  endif

  let user = get(data, 'user', '<unknown>')

  echo 'Copilot: Authenticated as GitHub user ' . user
endfunction

let s:commands.auth = s:commands.setup
let s:commands.signin = s:commands.setup

function! s:commands.help(opts) abort
  return a:opts.mods . ' help ' . (len(a:opts.arg) ? ':Copilot_' . a:opts.arg : 'copilot')
endfunction

function! s:commands.version(opts) abort
  echo 'copilot.vim ' .copilot#client#EditorPluginInfo().version
  let editorInfo = copilot#client#EditorInfo()
  echo editorInfo.name . ' ' . editorInfo.version
  if s:Running()
    let versions = s:client.Request('getVersion', {})
    if exists('s:client.serverInfo.version')
      echo s:client.serverInfo.name . ' ' . s:client.serverInfo.version
    else
      echo 'GitHub Copilot Language Server ' . versions.Await().version
    endif
    if exists('s:client.node_version')
      echo 'Node.js ' . s:client.node_version
    else
      echo 'Node.js ' . substitute(get(versions.Await(), 'runtimeVersion', '?'), '^node/', '', 'g')
    endif
  else
    echo 'Not running'
    if exists('s:client.node_version')
      echo 'Node.js ' . s:client.node_version
    endif
  endif
  if has('win32')
    echo 'Windows'
  elseif has('macunix')
    echo 'macOS'
  elseif !has('unix')
    echo 'Unknown OS'
  elseif isdirectory('/sys/kernel')
    echo 'Linux'
  else
    echo 'UNIX'
  endif
  call s:EditorVersionWarning()
endfunction

function! s:commands.restart(opts) abort
  call s:Stop()
  echo 'Copilot: Restarting language server'
  call s:Start()
endfunction

function! s:AfterUpgrade(old_version, client) abort
  if exists('a:client.serverInfo.version')
    call s:Echo('Copilot: Upgraded language server to ' . a:client.serverInfo.version)
    let g:copilot_version = '^' . a:client.serverInfo.version
  else
    call s:Echo('Copilot: Failed to upgrade language server. Check log for details')
    let g:copilot_version = a:old_version
    if a:old_version is v:null
      unlet g:copilot_version
    endif
    call s:Start()
  endif
endfunction

function! s:commands.upgrade(opts) abort
  if exists('s:client.serverInfo.version')
    echo 'Copilot: Upgrading language server from version ' . s:client.serverInfo.version
  else
    echo 'Copilot: Upgrading language server'
  endif
  let old_version = get(g:, 'copilot_version', v:null)
  let g:copilot_version = 'latest'
  call s:Stop()
  call s:Start()
  call s:client.AfterInitialized(function('s:AfterUpgrade', [old_version]))
endfunction

function! s:commands.disable(opts) abort
  let g:copilot_enabled = 0
endfunction

function! s:commands.enable(opts) abort
  let g:copilot_enabled = 1
endfunction

function! s:commands.panel(opts) abort
  if s:VerifySetup()
    return copilot#panel#Open(a:opts)
  endif
endfunction

function! s:FmtModel(model) abort
  return a:model.modelName . ' (' . a:model.id . ')'
endfunction

function! s:commands.model(opts) abort
  if !s:VerifySetup()
    return
  endif
  let client = copilot#Client()
  let response = client.Request('copilot/models', {}).Wait()
  if response.status ==# 'error'
    return 'echoerr ' . string('Copilot: Error retrieving completions models: ' . response.error.message)
  endif
  let models = filter(response.result, { _, m -> index(m.scopes, 'completion') >= 0 })
  if len(models) == 0
    echo 'Copilot: Could not retrieve completions models'
  elseif len(models) == 1
    echo 'Copilot: Current/only completions model is ' . s:FmtModel(models[0])
  else
    let choices = map(copy(models), { i, m -> (i + 1) . '. ' . s:FmtModel(m) })
    let choice = inputlist(['Select a completions model:'] + choices)
    if choice < 1 || choice > len(models)
      return
    endif
    let model = models[choice - 1]
    if type(get(g:, 'copilot_settings')) != v:t_dict
      let g:copilot_settings = {}
    endif
    let g:copilot_settings.selectedCompletionModel = model.id
    redraw
    echo 'Copilot: Set completions model to ' . s:FmtModel(model)
    call client.DidChangeConfiguration()
  endif
endfunction

function! s:commands.log(opts) abort
  return a:opts.mods . ' split +$ copilot:///log'
endfunction

function! copilot#CommandComplete(arg, lead, pos) abort
  let args = matchstr(strpart(a:lead, 0, a:pos), 'C\%[opilot][! ] *\zs.*')
  if args !~# ' '
    return sort(filter(map(keys(s:commands), { k, v -> tr(v, '_', '-') }),
          \ { k, v -> strpart(v, 0, len(a:arg)) ==# a:arg }))
  else
    return []
  endif
endfunction

function! copilot#Command(line1, line2, range, bang, mods, arg) abort
  let cmd = matchstr(a:arg, '^\%(\\.\|\S\)\+')
  let arg = matchstr(a:arg, '\s\zs\S.*')
  if !empty(cmd) && !has_key(s:commands, tr(cmd, '-', '_'))
    return 'echoerr ' . string('Copilot: unknown command ' . string(cmd))
  endif
  try
    if empty(cmd)
      if !s:Running()
        let cmd = 'restart'
      else
        try
          let opts = copilot#Call('checkStatus', {'options': {'localChecksOnly': v:true}})
          if opts.status !=# 'OK' && opts.status !=# 'MaybeOK'
            let cmd = 'setup'
          else
            let cmd = 'status'
          endif
        catch
          call copilot#logger#Exception()
          let cmd = 'log'
        endtry
      endif
    endif
    let opts = {'line1': a:line1, 'line2': a:line2, 'range': a:range, 'bang': a:bang, 'mods': a:mods, 'arg': arg}
    let retval = s:commands[tr(cmd, '-', '_')](opts)
    if type(retval) == v:t_string
      return retval
    else
      return ''
    endif
  catch /^Copilot:/
    return 'echoerr ' . string(v:exception)
  endtry
endfunction
