Skip to content

Instantly share code, notes, and snippets.

@romainl
Last active June 2, 2020 09:20
Show Gist options
  • Save romainl/a50b49408308c45cc2f9f877dfe4df0c to your computer and use it in GitHub Desktop.
Save romainl/a50b49408308c45cc2f9f877dfe4df0c to your computer and use it in GitHub Desktop.
TypeScript ftplugin

TypeScript ftplugin

Attention! All this is a work in progress.

Include search

:help 'include' is set so that Vim recognizes both:

import Foo from 'foo';

and:

const Foo = require('foo');

This is instrumental in enabling the following features:

  • printing the first line found in the inclusion tree that contains the given keyword

    :help :isearch, :help [i, :help ]i

  • printing a list of lines found in the inclusion tree that contain the given keyword

    :help :ilist, :help [I, :help ]I

  • jumping to the first line found in the inclusion tree that contains the given keyword

    :help :ijump, :help [_ctrl-i, :help ]_ctrl-i

  • completing keywords from included files

    :help i_ctrl-x_ctrl-i, i in :help 'complete'

Definition search

:help 'define' is set so that Vim recognizes a wide variety of "definitions":

const foo = [...];
export default class Foo {...};
etc.

This is instrumental in enabling the following features:

  • printing a definition of the given keyword found in the inclusion tree

    :help :dsearch, :help [d, :help ]d

  • printing a list of definitions of the given keyword found in the inclusion tree

    :help :dlist, :help [D, :help ]D

  • jumping to the first definition of the given keyword found in the inclusion tree

    :help :djump, :help [_ctrl-d, :help ]_ctrl-d

  • completing definitions from included files

    :help i_ctrl-x_ctrl-d, d in :help 'complete'

Go to file

:help 'isfname' is set so that Vim recognizes filenames containing an @, a very common Webpack/TypeScript/VSCode alias. This should make use of b:ts_config_paths.

:help 'suffixesadd' is set so that Vim can try different suffixes when faced with a suffix-less filename.

:help 'includeexpr' is set to a rather involved function so that Vim can resolve as many inclusion patterns as possible: @/foo/barsrc/components/foo/bar.ts, ../baz../baz/index.ts, etc. This has effect on include search and definition search as well and the function is also used to implement a custom gf because the original gf can't be forced to resolve ./foo/index.ts when given ./foo.

Due to the recursive nature of include search and definition search, looking through node_modules/ would be too time consuming so the function deliberately ignores node_modules/ imports when it is used for include/definition search. gf works differently so that limitation doesn't apply.

Linting

:help 'makeprg' and :help 'errorformat' are properly set to use tslint as linter for TypeScript files. This will have to change by the end of 2019, when tslint will be officially deprecated in favor of eslint.

An autocommand is added to run :make on write. See this gist for the rationale and additional tricks.

$PATH

The current project's node_modules/.bin/ is prepended to $PATH so that we can use local commands managed by npm like eslint, tslint, prettier, or webpack.


My Vim-related gists.

if !exists('b:did_typescript_setup')
" node_modules
let node_modules = finddir('node_modules', '.;', -1)
if len(node_modules)
let b:ts_node_modules = map(node_modules, { idx, val -> substitute(fnamemodify(val, ':p'), '/$', '', '') })
unlet node_modules
endif
" $PATH
if exists('b:ts_node_modules')
if $PATH !~ b:ts_node_modules[0]
let $PATH = b:ts_node_modules[0] . ':' . $PATH
endif
endif
" aliases
let tsconfig_file = findfile('tsconfig.json', '.;')
if len(tsconfig_file)
let tsconfig_data = json_decode(join(readfile(tsconfig_file)))
let paths = values(map(tsconfig_data.compilerOptions.paths, {key, val -> [
\ glob2regpat(key),
\ substitute(val[0], '\/\*$', '', '')]
\ }))
for path in paths
let path[1] = finddir(path[1], '.;')
endfor
let b:ts_config_paths = paths
unlet tsconfig_file
unlet tsconfig_data
unlet paths
endif
" lint file on write
if executable('tslint')
let &l:errorformat = '%EERROR: %f:%l:%c - %m,'
\ . '%WWARNING: %f:%l:%c - %m,'
\ . '%E%f:%l:%c - %m,'
\ . '%-G%.%#'
let &l:makeprg = 'tslint --format prose'
augroup TS
autocmd!
autocmd BufWritePost <buffer> silent make! <afile> | silent redraw!
augroup END
endif
" matchit
let b:match_words = '\<function\>:\<return\>,'
\ . '\<do\>:\<while\>,'
\ . '\<switch\>:\<case\>:\<default\>,'
\ . '\<if\>:\<else\>,'
\ . '\<try\>:\<catch\>:\<finally\>'
let b:did_typescript_setup = 1
endif
if !exists("*TypeScriptIncludeExpression")
function TypeScriptIncludeExpression(fname, gf) abort
" BUILT-IN NODE MODULES
" =====================
" they aren't natively accessible but we can use @types/node if available
if index([ 'assert', 'async_hooks',
\ 'base', 'buffer',
\ 'child_process', 'cluster', 'console', 'constants', 'crypto',
\ 'dgram', 'dns', 'domain',
\ 'events',
\ 'fs',
\ 'globals',
\ 'http', 'http2', 'https',
\ 'inspector',
\ 'net',
\ 'os',
\ 'path', 'perf_hooks', 'process', 'punycode',
\ 'querystring',
\ 'readline', 'repl',
\ 'stream', 'string_decoder',
\ 'timers', 'tls', 'trace_events', 'tty',
\ 'url', 'util',
\ 'v8', 'vm',
\ 'worker_threads',
\ 'zlib' ], a:fname) != -1
let found_definition = b:ts_node_modules[0] . '/@types/node/' . a:fname . '.d.ts'
if filereadable(found_definition)
return found_definition
endif
return 0
endif
" LOCAL IMPORTS
" =============
" they are everywhere so we must get them right
if a:fname =~ '^\.'
" ./
if a:fname =~ '^\./$'
return './index.ts'
endif
" ../
if a:fname =~ '\.\./$'
return a:fname . 'index.ts'
endif
" ./foo
" ./foo/bar
" ../foo
" ../foo/bar
" simplify module name to find it more easily
return substitute(a:fname, '^\W*', '', '')
endif
" ALIASED IMPORTS
" ===============
" https://code.visualstudio.com/docs/languages/jsconfig
" https://webpack.js.org/configuration/resolve/#resolve-alias
if !empty(get(b:, 'ts_config_paths', []))
for path in b:ts_config_paths
if a:fname =~ path[0]
let base_name = substitute(a:fname, path[0], path[1] . '/', '')
if isdirectory(base_name)
return base_name . '/index'
endif
return base_name
endif
endfor
endif
" this is where we stop for include-search/definition-search
if !a:gf
if filereadable(a:fname)
return a:fname
endif
return 0
endif
" NODE IMPORTS
" ============
" give up if there's no node_modules
if empty(get(b:, 'ts_node_modules', []))
if filereadable(a:fname)
return a:fname
endif
return 0
endif
" split the filename in meaningful parts:
" - a package name, used to search for the package in node_modules/
" - a subpath if applicable, used to reach the right module
"
" example:
" import bar from 'coolcat/foo/bar';
" - package_name = coolcat
" - sub_path = foo/bar
"
" special case:
" import something from '@scope/something/else';
" - package_name = @scope/something
" - sub_path = else
let parts = split(a:fname, '/')
if parts[0] =~ '^@'
let package_name = join(parts[0:1], '/')
let sub_path = join(parts[2:-1], '/')
else
let package_name = parts[0]
let sub_path = join(parts[1:-1], '/')
endif
" find the package.json for that package
let package_json = b:ts_node_modules[-1] . '/' . package_name . '/package.json'
" give up if there's no package.json
if !filereadable(package_json)
if filereadable(a:fname)
return a:fname
endif
return 0
endif
if len(sub_path) == 0
" grab data from the package.json
if !has_key(b:ts_packages, a:fname)
let package = json_decode(join(readfile(package_json)))
let b:ts_packages[a:fname] = {
\ "pack": fnamemodify(package_json, ':p:h'),
\ "entry": substitute(get(package, "typings", get(package, "main", "index.js")), '^\.\{1,2}\/', '', '')
\ }
endif
" build path from 'typings' key
" fall back to 'main' key
" fall back to 'index.js'
return b:ts_packages[a:fname].pack . "/" . b:ts_packages[a:fname].entry
else
" build the path to the module
let common_path = fnamemodify(package_json, ':p:h') . '/' . sub_path
" first, try with .ts and .js
let found_ext = glob(common_path . '.[jt]s', 1)
if len(found_ext)
return found_ext
endif
" second, try with /index.ts and /index.js
let found_index = glob(common_path . '/index.[jt]s', 1)
if len(found_index)
return found_index
endif
" give up
if filereadable(a:fname)
return a:fname
endif
return 0
endif
" give up
if filereadable(a:fname)
return a:fname
endif
return a:fname . '.d.ts'
endfunction
endif
setlocal include=^\\s*[^\/]\\+\\(from\\\|require(\\)\\s*['\"\.]
let &l:define = '^\s*\('
\ . '\(export\s\)*\(default\s\)*\(var\|const\|let\|function\|class\|interface\)\s'
\ . '\|\(public\|private\|protected\|readonly\|static\)\s'
\ . '\|\(get\s\|set\s\)'
\ . '\|\(export\sdefault\s\|abstract\sclass\s\)'
\ . '\|\(async\s\)'
\ . '\|\(\ze\i\+([^)]*).*{$\)'
\ . '\)'
setlocal includeexpr=TypeScriptIncludeExpression(v:fname,0)
setlocal suffixesadd+=.ts,.tsx,.d.ts
setlocal isfname+=@-@
" more helpful gf
nnoremap <silent> <buffer> gf :call <SID>GF(expand('<cfile>'), 'find')<CR>
xnoremap <silent> <buffer> gf <Esc>:<C-u>call <SID>GF(visual#GetSelection(), 'find')<CR>
nnoremap <silent> <buffer> <C-w><C-f> :call <SID>GF(expand('<cfile>'), 'sfind')<CR>
xnoremap <silent> <buffer> <C-w><C-f> <Esc>:<C-u>call <SID>GF(visual#GetSelection(), 'sfind')<CR>
if !exists("*s:GF")
function s:GF(text, cmd)
let include_expression = TypeScriptIncludeExpression(a:text, 1)
if len(include_expression) > 1
execute a:cmd . " " . include_expression
else
echohl WarningMsg
echo "Can't find file " . a:text
echohl None
endif
endfunction
endif
@jssee
Copy link

jssee commented Nov 21, 2019

This is excellent! thanks for creating it. I quickly put together this eslint-formatter trying to emulate the output of tslint --format prose in hopes that it could work interchangeably here for linting. The only thing is that you either have to install the formatter on a per project basis, or point the --format option to the global installation.

I'd be curious to know if you have any pointers to make it better.

@romainl
Copy link
Author

romainl commented Nov 21, 2019

This is excellent! thanks for creating it. I quickly put together this eslint-formatter trying to emulate the output of tslint --format prose in hopes that it could work interchangeably here for linting. The only thing is that you either have to install the formatter on a per project basis, or point the --format option to the global installation.

I'd be curious to know if you have any pointers to make it better.

Thanks. Here is the eslint setup I use for JavaScript, which should work for TypeScript as well:

setlocal errorformat=%f:\ line\ %l\\,\ col\ %c\\,\ %m,%-G%.%#
setlocal makeprg=eslint\ --format\ compact

We have just started transitioning our linter from tslint to eslint so it's a bit too early for me to validate my hypothesis and update this gist but I will certainly do when I'm done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment