"============================================================================ " FILE: autoload/tsuquyomi/es6import.vim " AUTHOR: Quramy "============================================================================ scriptencoding utf-8 let s:save_cpo = &cpo set cpo&vim let s:V = vital#of('tsuquyomi') let s:Filepath = s:V.import('System.Filepath') let s:JSON = s:V.import('Web.JSON') function! s:normalizePath(path) return substitute(a:path, '\\', '/', 'g') endfunction function! s:is_valid_identifier(symbol_str) return a:symbol_str =~ '^[A-Za-z_\$][A-Za-z_\$0-9]*$' endfunction function! s:get_keyword_under_cursor() let l:line_str = getline('.') let l:line = line('.') let l:offset = col('.') " search backwards for start of identifier (iskeyword pattern) let l:start = l:offset let l:end = l:offset while l:start > 0 && l:line_str[l:start-2] =~ "\\k" let l:start -= 1 endwhile while l:end <= strlen(l:line_str) && l:line_str[l:end] =~ "\\k" let l:end += 1 endwhile return { \ 'text': l:line_str[l:start-1:l:end-1], \ 'start': { 'offset': l:start, 'line': l:line }, \ 'end': { 'offset': l:end, 'line': l:line } \ } endfunction function! s:relativePath(from, to) let l:from_parts = s:Filepath.split(s:Filepath.dirname(a:from)) let l:to_parts = s:Filepath.split(a:to) let l:count_node_modules = len(filter(copy(l:to_parts), 'v:val==#"node_modules"')) if l:count_node_modules > 1 return ['', 0] elseif l:count_node_modules == 1 return [substitute(a:to, '^.*\/node_modules\/', '', ''), 1] endif let l:idx = 0 while idx < min([len(l:from_parts), len(l:to_parts)]) && l:from_parts[l:idx] ==# l:to_parts[l:idx] let l:idx += 1 endwhile call remove(l:from_parts, 0, l:idx - 1) call remove(l:to_parts, 0, l:idx - 1) if len(l:from_parts) return [join(map(l:from_parts, '"../"'), '').join(l:to_parts, '/'), 1] else return ['./'.join(l:to_parts, '/'), 1] endif endfunction let s:external_module_cache_dict = {} function! tsuquyomi#es6import#checkExternalModule(name, file, no_use_cache) let l:cache = s:external_module_cache_dict if a:no_use_cache || !has_key(l:cache, a:file) || !has_key(l:cache[a:file], a:name) if !has_key(l:cache, a:file) let l:cache[a:file] = {} endif let l:result = tsuquyomi#tsClient#tsNavBar(a:file) let l:modules = map(filter(l:result, 'v:val.kind==#"module"'), 'v:val.text') let l:cache[a:file][a:name] = 0 for module_name in l:modules if module_name[0] ==# '"' || module_name[0] ==# "'" if module_name[1:-2] ==# a:name let l:cache[a:file][a:name] = 1 break endif endif endfor endif return l:cache[a:file][a:name] endfunction function! tsuquyomi#es6import#createImportBlock(text) let l:identifier = a:text if !s:is_valid_identifier(l:identifier) return [] endif let [l:nav_list, l:hit] = tsuquyomi#navto(l:identifier, 'export', 2) if !l:hit || !len(l:nav_list) return [] endif let l:from = s:normalizePath(expand('%:p')) let l:result_list = [] for nav in l:nav_list if has_key(nav, 'containerKind') && nav.containerKind ==# 'module' if tsuquyomi#es6import#checkExternalModule(nav.containerName, nav.file, 0) let l:importDict = { \ 'identifier': nav.name, \ 'path': nav.containerName, \ 'nav': nav \ } call add(l:result_list, l:importDict) endif else let l:to = s:normalizePath(nav.file) let [l:relative_path, l:result] = s:relativePath(l:from, l:to) if !l:result return [] endif let l:relative_path = s:removeTSExtensions(l:relative_path) if g:tsuquyomi_shortest_import_path == 1 let l:path = s:getShortestImportPath(l:to, l:identifier, l:relative_path) elseif g:tsuquyomi_baseurl_import_path == 1 let l:base_url_import_path = s:getBaseUrlImportPath(nav.file) let l:path = l:base_url_import_path != '' ? l:base_url_import_path : l:relative_path else let l:path = l:relative_path endif let l:importDict = { \ 'identifier': nav.name, \ 'path': l:path, \ 'nav': nav \ } call add(l:result_list, l:importDict) endif endfor if g:tsuquyomi_case_sensitive_imports == 1 call filter(l:result_list, 'v:val.identifier ==# l:identifier') endif " Make the possible imports list unique per path let dictionary = {} for i in l:result_list let dictionary[i.path] = i endfor let l:unique_result_list = [] if (exists('a:1')) let l:unique_result_list = sort(values(dictionary), a:1) else let l:unique_result_list = sort(values(dictionary)) endif return l:unique_result_list endfunction function! s:removeTSExtensions(path) let l:path = a:path let l:path = substitute(l:path, '\.d\.ts$', '', '') let l:path = substitute(l:path, '\.ts$', '', '') let l:path = substitute(l:path, '\.tsx$', '', '') let l:path = substitute(l:path, '^@types/', '', '') let l:path = substitute(l:path, '/index$', '', '') return l:path endfunction function! s:getShortestImportPath(absolute_path, module_identifier, relative_path) let l:splitted_relative_path = split(a:relative_path, '/') if l:splitted_relative_path[0] == '..' let l:paths_to_visit = substitute(a:relative_path, '\.\.\/', '', 'g') let l:path_moves_to_do = len(split(l:paths_to_visit, '/')) else let l:path_moves_to_do = len(l:splitted_relative_path) - 1 endif let l:shortened_path = l:splitted_relative_path[len(l:splitted_relative_path) - 1] let l:path_move_count = 0 let l:splitted_absolute_path = split(a:absolute_path, '/') while l:path_move_count != l:path_moves_to_do let l:splitted_absolute_path = l:splitted_absolute_path[0:len(splitted_absolute_path) - 2] let l:shortened_path = s:getShortenedPath(l:splitted_absolute_path, l:shortened_path, a:module_identifier) let l:path_move_count += 1 endwhile let l:shortened_path = substitute(l:shortened_path, '\[\/\]\*\[\index\]\*', '', 'g') if l:splitted_relative_path[0] == '.' return './' . s:getPathWithSkippedRoot(l:shortened_path) elseif l:splitted_relative_path[0] == '..' let l:count = 0 let l:current = '..' let l:prefix = '' while l:current == '..' || l:count == len(l:splitted_relative_path) - 1 let l:current = l:splitted_relative_path[l:count] if l:current == '..' let l:prefix = l:prefix . l:current . '/' endif let l:count += 1 endwhile return l:prefix . s:getPathWithSkippedRoot(l:shortened_path) endif return l:shortened_path endfunction function! s:getPathWithSkippedRoot(path) return join(split(a:path, '/')[1:len(a:path) -1], '/') endfunction function! s:getShortenedPath(splitted_absolute_path, previous_shortened_path, module_identifier) let l:shortened_path = a:previous_shortened_path let l:absolute_path_to_search_in = '/' . join(a:splitted_absolute_path, '/') . '/' let l:found_module_reference = s:findExportingFileForModule(a:module_identifier, l:shortened_path, l:absolute_path_to_search_in) let l:current_directory_name = a:splitted_absolute_path[len(a:splitted_absolute_path) -1] let l:path_separator = '/' while l:found_module_reference != '' if l:found_module_reference == 'index' let l:found_module_reference = '[index]*' let l:path_separator = '[/]*' else let l:path_separator = '/' endif let l:shortened_path = l:found_module_reference let l:found_module_reference = s:findExportingFileForModule(a:module_identifier, l:found_module_reference, l:absolute_path_to_search_in) if l:found_module_reference != '' let l:shortened_path = l:found_module_reference endif endwhile return l:current_directory_name . l:path_separator . l:shortened_path endfunction function! s:getBaseUrlImportPath(module_absolute_path) let [l:tsconfig, l:tsconfig_file_path] = s:getTsconfig(a:module_absolute_path) if empty(l:tsconfig) || l:tsconfig_file_path == '' return '' endif let l:project_root_path = fnamemodify(l:tsconfig_file_path, ':h').'/' " We assume that baseUrl is a path relative to tsconfig.json path. let l:base_url_config = has_key(l:tsconfig.compilerOptions, 'baseUrl') ? l:tsconfig.compilerOptions.baseUrl : '.' let l:base_url_path = simplify(l:project_root_path.l:base_url_config) return s:removeTSExtensions(substitute(a:module_absolute_path, l:base_url_path, '', '')) endfunction let s:tsconfig = {} let s:tsconfig_file_path = '' function! s:getTsconfig(module_absolute_path) if empty(s:tsconfig) let l:project_info = tsuquyomi#tsClient#tsProjectInfo(a:module_absolute_path, 0) if has_key(l:project_info, 'configFileName') let s:tsconfig_file_path = l:project_info.configFileName else echom '[Tsuquyomi] Cannot find project’s tsconfig.json to compute baseUrl import path.' endif let l:json = join(readfile(s:tsconfig_file_path),'') try let s:tsconfig = s:JSON.decode(l:json) catch echom '[Tsuquyomi] Cannot parse project’s tsconfig.json. Does it have comments?' endtry endif return [s:tsconfig, s:tsconfig_file_path] endfunction function! s:findExportingFileForModule(module, current_module_file, module_directory_path) execute \"silent! noautocmd vimgrep /export\\s*\\({.*\\(\\s\\|,\\)" \. a:module \."\\(\\s\\|,\\)*.*}\\|\\*\\)\\s\\+from\\s\\+\\(\\'\\|\\\"\\)\\.\\\/" \. substitute(a:current_module_file, '\/', '\\/', '') \."[\\/]*\\(\\'\\|\\\"\\)[;]*/j " \. a:module_directory_path \. "*.ts" redir => l:grep_result silent! clist redir END if l:grep_result =~ 'No Errors' return '' endif let l:raw_result = split(l:grep_result, ' ')[2] let l:raw_result = split(l:raw_result, ':')[0] let l:raw_result_parts = split(l:raw_result, '/') let l:extracted_file_name = l:raw_result_parts[len(l:raw_result_parts) -1 ] let l:extracted_file_name = s:removeTSExtensions(l:extracted_file_name) return l:extracted_file_name endfunction function! s:comp_alias(alias1, alias2) return a:alias2.spans[0].end.line - a:alias1.spans[0].end.line endfunction function! tsuquyomi#es6import#createImportPosition(nav_bar_list) if !len(a:nav_bar_list) return {} endif if len(a:nav_bar_list) == 1 if a:nav_bar_list[0].kind ==# 'module' if !len(filter(copy(a:nav_bar_list[0].childItems), 'v:val.kind ==#"alias"')) let l:start_line = a:nav_bar_list[0].spans[0].start.line - 1 let l:end_line = l:start_line else let l:start_line = a:nav_bar_list[0].spans[0].start.line let l:end_line = a:nav_bar_list[0].spans[0].end.line endif else let l:start_line = a:nav_bar_list[0].spans[0].start.line - 1 let l:end_line = l:start_line endif elseif len(a:nav_bar_list) > 1 let l:start_line = a:nav_bar_list[0].spans[0].start.line let l:end_line = a:nav_bar_list[1].spans[0].start.line - 1 endif return { 'start': { 'line': l:start_line }, 'end': { 'line': l:end_line } } endfunction function! tsuquyomi#es6import#getImportDeclarations(fileName, content_list) let l:nav_bar_list = tsuquyomi#tsClient#tsNavBar(a:fileName) if !len(l:nav_bar_list) return [[], {}, 'no_nav_bar'] endif let l:position = tsuquyomi#es6import#createImportPosition(l:nav_bar_list) let l:module_infos = filter(copy(l:nav_bar_list), 'v:val.kind ==# "module"') if !len(l:module_infos) return [[], l:position, 'no_module_info'] endif let l:result_list = [] let l:alias_list = filter(l:module_infos[0].childItems, 'v:val.kind ==# "alias"') let l:end_line = position.end.line let l:last_module_end_line = 0 for alias in sort(l:alias_list, "s:comp_alias") let l:hit = 0 let [l:has_brace, l:brace] = [0, {}] let [l:has_from, l:from] = [0, { 'start': {}, 'end': {} }] let [l:has_module, l:module] = [0, { 'name': '', 'start': {}, 'end': {} }] let l:line = alias.spans[0].start.line while !l:hit && l:line <= l:end_line if !len(a:content_list) let l:line_str = getline(l:line) else let l:line_str = a:content_list[l:line - 1] endif let l:brace_end_offset = match(l:line_str, "}") let l:from_offset = match(l:line_str, 'from') if l:brace_end_offset + 1 && !l:has_brace && !l:has_from let l:has_brace = 1 let l:brace = { \ 'end': { 'offset': l:brace_end_offset + 1, 'line': l:line } \ } endif if l:from_offset + 1 let l:has_from = 1 let l:from = { \ 'start': { 'offset': l:from_offset + 1, 'line': l:line }, \ 'end': { 'offset': l:from_offset + 4, 'line': l:line } \ } endif if l:has_from let l:module_name_sq = matchstr(l:line_str, "\\m'\\zs.*\\ze'") if l:module_name_sq !=# '' let l:has_module = 1 let l:module_name = l:module_name_sq else let l:module_name_dq = matchstr(l:line_str, '\m"\zs.*\ze"') if l:module_name_dq !=# '' let l:has_module = 1 let l:module_name = l:module_name_dq endif endif endif if l:has_module let [l:hit, l:end_line] = [1, l:line] let l:module = { \ 'name': l:module_name, \ 'start': { 'line': l:line }, \ 'end': { 'line': l:line }, \ } if !l:last_module_end_line let l:last_module_end_line = l:line endif else let l:line += 1 endif endwhile if l:hit let l:info = { \ 'module': l:module, \ 'has_from': l:has_from, \ 'from_span': l:from, \ 'has_brace': l:has_brace, \ 'brace': l:brace, \ 'alias_info': alias, \ 'is_oneliner': alias.spans[0].start.line == l:module.end.line \ } call add(l:result_list, l:info) endif endfor if l:last_module_end_line let l:position.end.line = l:last_module_end_line endif return [l:result_list, l:position, ''] endfunction let s:importable_module_list = [] function! tsuquyomi#es6import#moduleComplete(arg_lead, cmd_line, cursor_pos) return join(s:importable_module_list, "\n") endfunction function! tsuquyomi#es6import#selectModule() echohl String let l:selected_module = input("[Tsuquyomi] You can import from 2 or more modules.\n" . join(s:importable_module_list, "\n") . "\nSelect one: ", '', 'custom,tsuquyomi#es6import#moduleComplete') echohl none echo ' ' if len(filter(copy(s:importable_module_list), 'v:val==#l:selected_module')) return [l:selected_module, 1] else echohl Error echom '[Tsuquyomi] Invalid module path.' echohl none return ['', 0] endif endfunction function! tsuquyomi#es6import#complete() if !tsuquyomi#bufManager#isOpened(expand('%:p')) return end call tsuquyomi#flush() let l:identifier_info = s:get_keyword_under_cursor() let l:list = tsuquyomi#es6import#createImportBlock(l:identifier_info.text) if len(l:list) > 1 let s:importable_module_list = map(copy(l:list), 'v:val.path') let [l:selected_module, l:code] = tsuquyomi#es6import#selectModule() if !l:code echohl Error echom '[Tsuquyomi] No search result.' echohl none return endif let l:block = filter(l:list, 'v:val.path==#l:selected_module')[0] elseif len(l:list) == 1 let l:block = l:list[0] else return endif let [l:import_list, l:dec_position, l:reason] = tsuquyomi#es6import#getImportDeclarations(expand('%:p'), []) let l:module_end_line = has_key(l:dec_position, 'end') ? l:dec_position.end.line : 0 let l:same_path_import_list = filter(l:import_list, 'v:val.has_brace && v:val.module.name ==# l:block.path') if len(l:same_path_import_list) && len(filter(copy(l:same_path_import_list), 'v:val.alias_info.text ==# l:block.identifier')) echohl Error echom '[Tsuquyomi] '.l:block.identifier.' is already imported.' echohl none return endif "Replace search keyword to hit result identifer let l:line = getline(l:identifier_info.start.line) let l:new_line = l:block.identifier if l:identifier_info.start.offset > 1 let l:new_line = l:line[0:l:identifier_info.start.offset - 2].l:new_line endif let l:new_line = l:new_line.l:line[l:identifier_info.end.offset: -1] call setline(l:identifier_info.start.line, l:new_line) if g:tsuquyomi_import_curly_spacing == 0 let l:curly_spacing = '' else let l:curly_spacing = ' ' end "Add import declaration if !len(l:same_path_import_list) if g:tsuquyomi_semicolon_import let l:semicolon = ';' else let l:semicolon = '' endif if g:tsuquyomi_single_quote_import let l:expression = "import {".l:curly_spacing.l:block.identifier.l:curly_spacing."} from '".l:block.path."'".l:semicolon else let l:expression = 'import {'.l:curly_spacing.l:block.identifier.l:curly_spacing.'} from "'.l:block.path.'"'.l:semicolon endif call append(l:module_end_line, l:expression) else let l:target_import = l:same_path_import_list[0] if l:target_import.is_oneliner let l:line = getline(l:target_import.brace.end.line) let l:injection_position = target_import.brace.end.offset - 2 - strlen(l:curly_spacing) let l:expression = l:line[0:l:injection_position].', '.l:block.identifier.l:curly_spacing.l:line[l:target_import.brace.end.offset - 1: -1] call setline(l:target_import.brace.end.line, l:expression) else let l:before_line = getline(l:target_import.brace.end.line - 1) let l:indent = matchstr(l:before_line, '\m^\s*') let l:before_has_trailing_comma = matchstr(l:before_line, ',\s*$') if l:before_has_trailing_comma !=# '' let l:prev_trailing_comma = '' let l:new_trailing_comma = ',' else let l:prev_trailing_comma = ',' let l:new_trailing_comma = '' endif call setline(l:target_import.brace.end.line - 1, l:before_line.l:prev_trailing_comma) call append(l:target_import.brace.end.line - 1, l:indent.l:block.identifier.l:new_trailing_comma) endif endif endfunction let &cpo = s:save_cpo unlet s:save_cpo