diff --git a/go.mod b/go.mod index ad27295..aceee61 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,9 @@ module github.com/bcmi-labs/arduino-language-server go 1.12 require ( - github.com/gorilla/websocket v1.4.0 // indirect - github.com/pkg/errors v0.8.1 - github.com/sourcegraph/go-lsp v0.0.0-20181119182933-0c7d621186c1 - github.com/sourcegraph/jsonrpc2 v0.0.0-20190106185902-35a74f039c6a + github.com/arduino/arduino-cli v0.0.0-20201215104024-6a177ebf56f2 + github.com/arduino/go-paths-helper v1.4.0 + github.com/pkg/errors v0.9.1 + github.com/sourcegraph/jsonrpc2 v0.0.0-20200429184054-15c2290dcb37 + github.com/stretchr/testify v1.6.1 ) diff --git a/go.sum b/go.sum index 386ebdb..2eacd34 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,390 @@ -github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/arduino/arduino-cli v0.0.0-20201215104024-6a177ebf56f2 h1:J+EUtAwSXK7AFSkK0Tbw85rLbcGf1ykuerssDUD9LxY= +github.com/arduino/arduino-cli v0.0.0-20201215104024-6a177ebf56f2/go.mod h1:RZsAJUrAIHFaSj71SNJ/hSRUqNrjDw+3WFT2xw9NnRM= +github.com/arduino/board-discovery v0.0.0-20180823133458-1ba29327fb0c h1:agh2JT96G8egU7FEb13L4dq3fnCN7lxXhJ86t69+W7s= +github.com/arduino/board-discovery v0.0.0-20180823133458-1ba29327fb0c/go.mod h1:HK7SpkEax/3P+0w78iRQx1sz1vCDYYw9RXwHjQTB5i8= +github.com/arduino/go-paths-helper v1.0.1 h1:utYXLM2RfFlc9qp/MJTIYp3t6ux/xM6mWjeEb/WLK4Q= +github.com/arduino/go-paths-helper v1.0.1/go.mod h1:HpxtKph+g238EJHq4geEPv9p+gl3v5YYu35Yb+w31Ck= +github.com/arduino/go-paths-helper v1.2.0/go.mod h1:HpxtKph+g238EJHq4geEPv9p+gl3v5YYu35Yb+w31Ck= +github.com/arduino/go-paths-helper v1.4.0 h1:ilnseAdxmN1bFnLxxXHRtcdmt9jBf3O4jtYfWfqule4= +github.com/arduino/go-paths-helper v1.4.0/go.mod h1:V82BWgAAp4IbmlybxQdk9Bpkz8M4Qyx+RAFKaG9NuvU= +github.com/arduino/go-properties-orderedmap v1.3.0 h1:4No/vQopB36e7WUIk6H6TxiSEJPiMrVOCZylYmua39o= +github.com/arduino/go-properties-orderedmap v1.3.0/go.mod h1:DKjD2VXY/NZmlingh4lSFMEYCVubfeArCsGPGDwb2yk= +github.com/arduino/go-timeutils v0.0.0-20171220113728-d1dd9e313b1b h1:9hDi4F2st6dbLC3y4i02zFT5quS4X6iioWifGlVwfy4= +github.com/arduino/go-timeutils v0.0.0-20171220113728-d1dd9e313b1b/go.mod h1:uwGy5PpN4lqW97FiLnbcx+xx8jly5YuPMJWfVwwjJiQ= +github.com/arduino/go-win32-utils v0.0.0-20180330194947-ed041402e83b h1:3PjgYG5gVPA7cipp7vIR2lF96KkEJIFBJ+ANnuv6J20= +github.com/arduino/go-win32-utils v0.0.0-20180330194947-ed041402e83b/go.mod h1:iIPnclBMYm1g32Q5kXoqng4jLhMStReIP7ZxaoUC2y8= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cmaglie/go.rice v1.0.3 h1:ZBLmBdQp6ejc+n8eMNH0uuRSKkg6kKe6ORjXKnyHBYw= +github.com/cmaglie/go.rice v1.0.3/go.mod h1:AF3bOWkvdOpp8/S3UL8qbQ4N7DiISIbJtj54GWFPAsc= +github.com/cmaglie/pb v1.0.27 h1:ynGj8vBXR+dtj4B7Q/W/qGt31771Ux5iFfRQBnwdQiA= +github.com/cmaglie/pb v1.0.27/go.mod h1:GilkKZMXYjBA4NxItWFfO+lwkp59PLHQ+IOW/b/kmZI= +github.com/codeclysm/cc v1.2.2 h1:1ChS4EvWTjw6bH2sd6QiMcmih0itVVrWdh9MmOliX/I= +github.com/codeclysm/cc v1.2.2/go.mod h1:XtW4ArCNgQwFphcRGG9+sPX5WM1J6/u0gMy5ZdV3obA= +github.com/codeclysm/extract/v3 v3.0.2 h1:sB4LcE3Php7LkhZwN0n2p8GCwZe92PEQutdbGURf5xc= +github.com/codeclysm/extract/v3 v3.0.2/go.mod h1:NKsw+hqua9H+Rlwy/w/3Qgt9jDonYEgB6wJu+25eOKw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/goselect v0.1.1 h1:tiSSgKE1eJtxs1h/VgGQWuXUP0YS4CDIFMp6vaI1ls0= +github.com/creack/goselect v0.1.1/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/daaku/go.zipexe v1.0.0 h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY= +github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/facchinm/gohex v0.0.0-20201008150446-be2a6be25790 h1:GTx/F+6TdhQAMtaDrpGDgnAzxYJhTVuTdSKpMsUbMNA= +github.com/facchinm/gohex v0.0.0-20201008150446-be2a6be25790/go.mod h1:sErAiirjQXs3P13DBW7oPmJ6Q0WsFjYXVAhz7Xt59UQ= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fluxio/iohelpers v0.0.0-20160419043813-3a4dd67a94d2 h1:C6sOwknxwWfLBEQ91zhmptlfxf7pVEs5s6wOnDxNpS4= +github.com/fluxio/iohelpers v0.0.0-20160419043813-3a4dd67a94d2/go.mod h1:c7sGIpDbBo0JZZ1tKyC1p5smWf8QcUjK4bFtZjHAecg= +github.com/fluxio/multierror v0.0.0-20160419044231-9c68d39025e5 h1:R8jFW6G/bjoXjWPFrEfw9G5YQDlYhwV4AC+Eonu6wmk= +github.com/fluxio/multierror v0.0.0-20160419044231-9c68d39025e5/go.mod h1:BEUDl7FG1cc76sM0J0x8dqr6RhiL4uqvk6oFkwuNyuM= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/h2non/filetype v1.0.6/go.mod h1:isekKqOuhMj+s/7r3rIeTErIRy4Rub5uBWHfvMusLMU= +github.com/h2non/filetype v1.0.8 h1:le8gpf+FQA0/DlDABbtisA1KiTS0Xi+YSC/E8yY3Y14= +github.com/h2non/filetype v1.0.8/go.mod h1:isekKqOuhMj+s/7r3rIeTErIRy4Rub5uBWHfvMusLMU= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/imjasonmiller/godice v0.1.2 h1:T1/sW/HoDzFeuwzOOuQjmeMELz9CzZ53I2CnD+08zD4= +github.com/imjasonmiller/godice v0.1.2/go.mod h1:8cTkdnVI+NglU2d6sv+ilYcNaJ5VSTBwvMbFULJd/QQ= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/juju/clock v0.0.0-20180524022203-d293bb356ca4/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA= +github.com/juju/errors v0.0.0-20150916125642-1b5e39b83d18/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= +github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5 h1:rhqTjzJlm7EbkELJDKMTU7udov+Se0xZkWmugr6zGok= +github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= +github.com/juju/loggo v0.0.0-20170605014607-8232ab8918d9/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= +github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 h1:UUHMLvzt/31azWTN/ifGWef4WUqvXk0iRqdhdy/2uzI= +github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= +github.com/juju/retry v0.0.0-20160928201858-1998d01ba1c3/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4= +github.com/juju/testing v0.0.0-20200510222523-6c8c298c77a0 h1:+WWUkhnTjV6RNOxkcwk79qrjeyHEHvBzlneueBsatX4= +github.com/juju/testing v0.0.0-20200510222523-6c8c298c77a0/go.mod h1:hpGvhGHPVbNBraRLZEhoQwFLMrjK8PSlO4D3nDjKYXo= +github.com/juju/utils v0.0.0-20180808125547-9dfc6dbfb02b/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= +github.com/juju/version v0.0.0-20161031051906-1f41e27e54f2/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leonelquinteros/gotext v1.4.0 h1:2NHPCto5IoMXbrT0bldPrxj0qM5asOCwtb1aUQZ1tys= +github.com/leonelquinteros/gotext v1.4.0/go.mod h1:yZGXREmoGTtBvZHNcc+Yfug49G/2spuF/i/Qlsvz1Us= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mdlayher/genetlink v0.0.0-20190313224034-60417448a851/go.mod h1:EsbsAEUEs15qC1cosAwxgCWV0Qhd8TmkxnA9Kw1Vhl4= +github.com/mdlayher/netlink v0.0.0-20190313131330-258ea9dff42c/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= +github.com/mdlayher/taskstats v0.0.0-20190313225729-7cbba52ee072/go.mod h1:sGdS7A6CAETR53zkdjGkgoFlh1vSm7MtX+i8XfEsTMA= +github.com/miekg/dns v1.0.5 h1:MQBGf2JEJDu0rg9WOpQZzeO+zW8UKwgkvP3R1dUU1Yw= +github.com/miekg/dns v1.0.5/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/oleksandr/bonjour v0.0.0-20160508152359-5dcf00d8b228 h1:Cvfd2dOlXIPTeEkOT/h8PyK4phBngOM4at9/jlgy7d4= +github.com/oleksandr/bonjour v0.0.0-20160508152359-5dcf00d8b228/go.mod h1:MGuVJ1+5TX1SCoO2Sx0eAnjpdRytYla2uC1YIZfkC9c= +github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/sourcegraph/go-lsp v0.0.0-20181119182933-0c7d621186c1 h1:O1d7nVzpGmP5pGAZBSlp9TSpjNwwI0xThxhPd9oVJuU= -github.com/sourcegraph/go-lsp v0.0.0-20181119182933-0c7d621186c1/go.mod h1:tpps84QRlOVVLYk5QpKYX8Tr289D1v/UTWDLqeguiqM= -github.com/sourcegraph/jsonrpc2 v0.0.0-20190106185902-35a74f039c6a h1:jTZwOlrDhmk4Ez2vhWh7kA0eKUahp1lCO2uyM4fi/Qk= -github.com/sourcegraph/jsonrpc2 v0.0.0-20190106185902-35a74f039c6a/go.mod h1:eESpbCslcLDs8j2D7IEdGVgul7xuk9odqDTaor30IUU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmylund/sortutil v0.0.0-20120526081524-abeda66eb583 h1:ogHi8YLNeIxABOaH6UgtbwkODheuAK+ErP8gWXYQVj0= +github.com/pmylund/sortutil v0.0.0-20120526081524-abeda66eb583/go.mod h1:sFPiU/UgDcsQVu3vkqpZLCXWFwUoQRpHGu9ATihPAl0= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo= +github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= +github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= +github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e h1:uO75wNGioszjmIzcY/tvdDYKRLVvzggtAmmJkn9j4GQ= +github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e/go.mod h1:tm/wZFQ8e24NYaBGIlnO2WGCAi67re4HHuOm0sftE/M= +github.com/segmentio/objconv v1.0.1/go.mod h1:auayaH5k3137Cl4SoXTgrzQcuQDmvuVtZgS0fb1Ahys= +github.com/segmentio/stats/v4 v4.5.3 h1:Y/DSUWZ4c8ICgqJ9rQohzKvGqGWbLPWad5zmxVoKN+Y= +github.com/segmentio/stats/v4 v4.5.3/go.mod h1:LsaahUJR7iiSs8mnkvQvdQ/RLHAS5adGLxuntg0ydGo= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sourcegraph/jsonrpc2 v0.0.0-20200429184054-15c2290dcb37 h1:marA1XQDC7N870zmSFIoHZpIUduK80USeY0Rkuflgp4= +github.com/sourcegraph/jsonrpc2 v0.0.0-20200429184054-15c2290dcb37/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.0.1-0.20200710201246-675ae5f5a98c h1:/dP/1GnfVIlWnB0YDImenSmneUCw3wjyq2RMgAG1e2o= +github.com/spf13/cobra v1.0.1-0.20200710201246-675ae5f5a98c/go.mod h1:aeNIJzz/GSSVlS+gpCpQWZ83BKbsoW57mr90+YthtkQ= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E= +github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= +github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= +github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= +github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.bug.st/cleanup v1.0.0 h1:XVj1HZxkBXeq3gMT7ijWUpHyIC1j8XAoNSyQ06CskgA= +go.bug.st/cleanup v1.0.0/go.mod h1:EqVmTg2IBk4znLbPD28xne3abjsJftMdqqJEjhn70bk= +go.bug.st/downloader/v2 v2.1.0 h1:VqGOrJrjgz8c0c8ExvF9dvvcpcrbo2IrI+rOoXKD6nQ= +go.bug.st/downloader/v2 v2.1.0/go.mod h1:VZW2V1iGKV8rJL2ZEGIDzzBeKowYv34AedJz13RzVII= +go.bug.st/relaxed-semver v0.0.0-20190922224835-391e10178d18 h1:F1qxtaFuewctYc/SsHRn+Q7Dtwi+yJGPgVq8YLtQz98= +go.bug.st/relaxed-semver v0.0.0-20190922224835-391e10178d18/go.mod h1:Cx1VqMtEhE9pIkEyUj3LVVVPkv89dgW8aCKrRPDR/uE= +go.bug.st/serial v1.1.1 h1:5J1DpaIaSIruBi7jVnKXnhRS+YQ9+2PLJMtIZKoIgnc= +go.bug.st/serial v1.1.1/go.mod h1:VmYBeyJWp5BnJ0tw2NUJHZdJTGl2ecBGABHlzRK1knY= +go.bug.st/serial.v1 v0.0.0-20180827123349-5f7892a7bb45 h1:mACY1anK6HNCZtm/DK2Rf2ZPHggVqeB0+7rY9Gl6wyI= +go.bug.st/serial.v1 v0.0.0-20180827123349-5f7892a7bb45/go.mod h1:dRSl/CVCTf56CkXgJMDOdSwNfo2g1orOGE/gBGdvjZw= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180214000028-650f4a345ab4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200406173513-056763e48d71 h1:DOmugCavvUtnUD114C1Wh+UgTgQZ4pMLzXxi1pSt+/Y= +golang.org/x/crypto v0.0.0-20200406173513-056763e48d71/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180406214816-61147c48b25b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 h1:W0lCpv29Hv0UaM1LXb9QlBHLNP8UFfcKjblhVCWftOM= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/mgo.v2 v2.0.0-20160818015218-f2b6f6c918c4/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= +gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= +gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= +gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= +gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170712054546-1be3d31502d6/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/handler/builder.go b/handler/builder.go index 170bbd2..db03d53 100644 --- a/handler/builder.go +++ b/handler/builder.go @@ -1,362 +1,123 @@ package handler import ( - "bufio" "bytes" - "io/ioutil" + "encoding/json" "log" - "os" - "os/exec" - "path/filepath" "strings" + "time" + "github.com/arduino/arduino-cli/arduino/libraries" + "github.com/arduino/arduino-cli/executils" + "github.com/arduino/go-paths-helper" + "github.com/bcmi-labs/arduino-language-server/streams" "github.com/pkg/errors" ) -func generateCpp(inoCode []byte, sourcePath, fqbn string) (cppPath string, cppCode []byte, err error) { - // The CLI expects the `theSketchName.ino` file to be in `some/path/theSketchName` folder. - // Expected folder structure: `/path/to/temp/ino2cpp-${random}/theSketchName/theSketchName.ino`. - rawRootTempDir, err := ioutil.TempDir("", "ino2cpp-") - if err != nil { - err = errors.Wrap(err, "Error while creating temporary directory.") - return - } - rootTempDir, err := filepath.EvalSymlinks(rawRootTempDir) - if err != nil { - err = errors.Wrap(err, "Error while resolving symbolic links of temporary directory.") - return - } +func (handler *InoHandler) scheduleRebuildEnvironment() { + handler.rebuildSketchDeadlineMutex.Lock() + defer handler.rebuildSketchDeadlineMutex.Unlock() + d := time.Now().Add(time.Second) + handler.rebuildSketchDeadline = &d +} - sketchName := filepath.Base(sourcePath) - if strings.HasSuffix(sketchName, ".ino") { - sketchName = sketchName[:len(sketchName)-len(".ino")] - } - sketchTempPath := filepath.Join(rootTempDir, sketchName) - createDirIfNotExist(sketchTempPath) +func (handler *InoHandler) rebuildEnvironmentLoop() { + defer streams.CatchAndLogPanic() - // Write source file to temp dir - sketchFileName := sketchName + ".ino" - inoPath := filepath.Join(sketchTempPath, sketchFileName) - err = ioutil.WriteFile(inoPath, inoCode, 0600) - if err != nil { - err = errors.Wrap(err, "Error while writing source file to temporary directory.") - return - } - if enableLogging { - log.Println("Source file written to", inoPath) - } + grabDeadline := func() *time.Time { + handler.rebuildSketchDeadlineMutex.Lock() + defer handler.rebuildSketchDeadlineMutex.Unlock() - // Copy all header files to temp dir - err = copyHeaderFiles(filepath.Dir(sourcePath), rootTempDir) - if err != nil { - return + res := handler.rebuildSketchDeadline + handler.rebuildSketchDeadline = nil + return res } - // Generate compile_flags.txt - cppPath = filepath.Join(sketchTempPath, sketchFileName+".cpp") - flagsPath, err := generateCompileFlags(sketchTempPath, inoPath, sourcePath, fqbn) - if err != nil { - return - } - if enableLogging { - log.Println("Compile flags written to", flagsPath) - } - - // Generate target file - cppCode, err = generateTargetFile(sketchTempPath, inoPath, cppPath, fqbn) - return -} - -func createDirIfNotExist(dir string) { - if _, err := os.Stat(dir); os.IsNotExist(err) { - err = os.MkdirAll(dir, os.ModePerm) - if err != nil { - panic(err) + for { + // Wait for someone to schedule a preprocessing... + time.Sleep(100 * time.Millisecond) + deadline := grabDeadline() + if deadline == nil { + continue } - } -} -func copyHeaderFiles(sourceDir string, destDir string) error { - fileInfos, err := ioutil.ReadDir(sourceDir) - if err != nil { - return err - } - for _, fileInfo := range fileInfos { - if !fileInfo.IsDir() && strings.HasSuffix(fileInfo.Name(), ".h") { - input, err := ioutil.ReadFile(filepath.Join(sourceDir, fileInfo.Name())) - if err != nil { - return err - } + for time.Now().Before(*deadline) { + time.Sleep(100 * time.Millisecond) - err = ioutil.WriteFile(filepath.Join(destDir, fileInfo.Name()), input, 0644) - if err != nil { - return err + if d := grabDeadline(); d != nil { + deadline = d } } + + // Regenerate preprocessed sketch! + handler.synchronizer.DataMux.Lock() + handler.initializeWorkbench(nil) + handler.synchronizer.DataMux.Unlock() } - return nil } -func updateCpp(inoCode []byte, sourcePath, fqbn string, fqbnChanged bool, cppPath string) (cppCode []byte, err error) { - tempDir := filepath.Dir(cppPath) - inoPath := strings.TrimSuffix(cppPath, ".cpp") - if inoCode != nil { - // Write source file to temp dir - err = ioutil.WriteFile(inoPath, inoCode, 0600) - if err != nil { - err = errors.Wrap(err, "Error while writing source file to temporary directory.") - return - } - if enableLogging { - log.Println("Source file written to", inoPath) - } - } +func (handler *InoHandler) generateBuildEnvironment() (*paths.Path, error) { + sketchDir := handler.sketchRoot + fqbn := handler.config.SelectedBoard.Fqbn - if fqbnChanged { - // Generate compile_flags.txt - var flagsPath string - flagsPath, err = generateCompileFlags(tempDir, inoPath, sourcePath, fqbn) + // Export temporary files + type overridesFile struct { + Overrides map[string]string `json:"overrides"` + } + data := overridesFile{Overrides: map[string]string{}} + for uri, trackedFile := range handler.docs { + rel, err := uri.AsPath().RelFrom(handler.sketchRoot) if err != nil { - return - } - if enableLogging { - log.Println("Compile flags written to", flagsPath) + return nil, errors.WithMessage(err, "dumping tracked files") } + data.Overrides[rel.String()] = trackedFile.Text } - - // Generate target file - cppCode, err = generateTargetFile(tempDir, inoPath, cppPath, fqbn) - return -} - -func generateCompileFlags(tempDir, inoPath, sourcePath, fqbn string) (string, error) { - var cliArgs []string - if len(fqbn) > 0 { - cliArgs = []string{"compile", "--fqbn", fqbn, "--show-properties", inoPath} + var overridesJSON string + if jsonBytes, err := json.MarshalIndent(data, "", " "); err != nil { + return nil, errors.WithMessage(err, "dumping tracked files") + } else if tmpFile, err := paths.WriteToTempFile(jsonBytes, nil, ""); err != nil { + return nil, errors.WithMessage(err, "dumping tracked files") } else { - cliArgs = []string{"compile", "--show-properties", inoPath} - } - propertiesCmd := exec.Command(globalCliPath, cliArgs...) - output, err := propertiesCmd.Output() - if err != nil { - err = logCommandErr(globalCliPath, output, err, errMsgFilter(tempDir)) - return "", err + overridesJSON = tmpFile.String() } - properties, err := readProperties(bytes.NewReader(output)) - if err != nil { - return "", errors.Wrap(err, "Error while reading build properties.") - } - flagsPath := filepath.Join(tempDir, "compile_flags.txt") - outFile, err := os.OpenFile(flagsPath, os.O_WRONLY|os.O_CREATE, 0600) - if err != nil { - return flagsPath, errors.Wrap(err, "Error while creating output file for compile flags.") - } - defer outFile.Close() - - printer := Printer{Writer: bufio.NewWriter(outFile)} - printCompileFlags(properties, &printer, fqbn) - printLibraryPaths(sourcePath, &printer) - printer.Flush() - return flagsPath, printer.Err -} -func generateTargetFile(tempDir, inoPath, cppPath, fqbn string) (cppCode []byte, err error) { - var cliArgs []string - if len(fqbn) > 0 { - cliArgs = []string{"compile", "--fqbn", fqbn, "--preprocess", inoPath} - } else { - cliArgs = []string{"compile", "--preprocess", inoPath} + // XXX: do this from IDE or via gRPC + args := []string{globalCliPath, + "compile", + "--fqbn", fqbn, + "--only-compilation-database", + "--clean", + "--source-override", overridesJSON, + "--format", "json", + sketchDir.String(), } - preprocessCmd := exec.Command(globalCliPath, cliArgs...) - cppCode, err = preprocessCmd.Output() + cmd, err := executils.NewProcess(args...) if err != nil { - err = logCommandErr(globalCliPath, cppCode, err, errMsgFilter(tempDir)) - return + return nil, errors.Errorf("running %s: %s", strings.Join(args, " "), err) } - - // Filter lines beginning with ERROR or WARNING - cppCode = []byte(filterErrorsAndWarnings(cppCode)) - - err = ioutil.WriteFile(cppPath, cppCode, 0600) - if err != nil { - err = errors.Wrap(err, "Error while writing target file to temporary directory.") - } else if enableLogging { - log.Println("Target file written to", cppPath) + cmdOutput := &bytes.Buffer{} + cmd.RedirectStdoutTo(cmdOutput) + cmd.SetDirFromPath(sketchDir) + log.Println("running: ", strings.Join(args, " ")) + if err := cmd.Run(); err != nil { + return nil, errors.Errorf("running %s: %s", strings.Join(args, " "), err) } - return -} -func filterErrorsAndWarnings(cppCode []byte) string { - var sb strings.Builder - scanner := bufio.NewScanner(bytes.NewReader(cppCode)) - for scanner.Scan() { - lineStr := scanner.Text() - if !(strings.HasPrefix(lineStr, "ERROR:") || strings.HasPrefix(lineStr, "WARNING:")) { - sb.WriteString(lineStr) - sb.WriteRune('\n') - } - } - return sb.String() -} - -func copyIno2Cpp(inoCode string, cppPath string) (cppCode []byte, err error) { - inoPath := strings.TrimSuffix(cppPath, ".cpp") - filePrefix := "#include \n#line 1 \"" + inoPath + "\"\n" - cppCode = []byte(filePrefix + inoCode) - err = ioutil.WriteFile(cppPath, cppCode, 0600) - if err != nil { - err = errors.Wrap(err, "Error while writing target file to temporary directory.") - return - } - if enableLogging { - log.Println("Target file written to", cppPath) - } - return -} - -func printCompileFlags(properties map[string]string, printer *Printer, fqbn string) { - if strings.Contains(fqbn, ":avr:") { - printer.Println("--target=avr") - } else if strings.Contains(fqbn, ":sam:") { - printer.Println("--target=arm-none-eabi") - } - cppFlags := expandProperty(properties, "compiler.cpp.flags") - printer.Println(splitFlags(cppFlags)) - mcu := expandProperty(properties, "build.mcu") - if strings.Contains(fqbn, ":avr:") { - printer.Println("-mmcu=", mcu) - } else if strings.Contains(fqbn, ":sam:") { - printer.Println("-mcpu=", mcu) - } - fcpu := expandProperty(properties, "build.f_cpu") - printer.Println("-DF_CPU=", fcpu) - ideVersion := expandProperty(properties, "runtime.ide.version") - printer.Println("-DARDUINO=", ideVersion) - board := expandProperty(properties, "build.board") - printer.Println("-DARDUINO_", board) - arch := expandProperty(properties, "build.arch") - printer.Println("-DARDUINO_ARCH_", arch) - if strings.Contains(fqbn, ":sam:") { - libSamFlags := expandProperty(properties, "compiler.libsam.c.flags") - printer.Println(splitFlags(libSamFlags)) - } - extraFlags := expandProperty(properties, "build.extra_flags") - printer.Println(splitFlags(extraFlags)) - corePath := expandProperty(properties, "build.core.path") - printer.Println("-I", corePath) - variantPath := expandProperty(properties, "build.variant.path") - printer.Println("-I", variantPath) - if strings.Contains(fqbn, ":avr:") { - avrgccPath := expandProperty(properties, "runtime.tools.avr-gcc.path") - printer.Println("-I", filepath.Join(avrgccPath, "avr", "include")) - } - - printLibraryPaths(corePath, printer) -} - -func printLibraryPaths(basePath string, printer *Printer) { - parentDir := filepath.Dir(basePath) - if strings.HasSuffix(parentDir, string(filepath.Separator)) || strings.HasSuffix(parentDir, ".") { - return - } - libsDir := filepath.Join(parentDir, "libraries") - if libraries, err := ioutil.ReadDir(libsDir); err == nil { - for _, libInfo := range libraries { - if libInfo.IsDir() { - srcDir := filepath.Join(libsDir, libInfo.Name(), "src") - if srcInfo, err := os.Stat(srcDir); err == nil && srcInfo.IsDir() { - printer.Println("-I", srcDir) - } else { - printer.Println("-I", filepath.Join(libsDir, libInfo.Name())) - } - } - } - } - printLibraryPaths(parentDir, printer) -} - -// Printer prints to a Writer and stores the first error. -type Printer struct { - Writer *bufio.Writer - Err error -} - -// Println prints the given strings followed by a line break. -func (printer *Printer) Println(text ...string) { - totalLen := 0 - for i := range text { - if len(text[i]) > 0 { - _, err := printer.Writer.WriteString(text[i]) - if err != nil && printer.Err == nil { - printer.Err = err - } - totalLen += len(text[i]) - } - } - if totalLen > 0 { - _, err := printer.Writer.WriteString("\n") - if err != nil && printer.Err == nil { - printer.Err = err - } - } -} - -// Flush flushes the underlying writer. -func (printer *Printer) Flush() { - err := printer.Writer.Flush() - if err != nil && printer.Err == nil { - printer.Err = err - } -} - -func splitFlags(flags string) string { - flagsBytes := []byte(flags) - result := make([]byte, len(flagsBytes)) - inSingleQuotes := false - inDoubleQuotes := false - for i, b := range flagsBytes { - if b == '\'' && !inDoubleQuotes { - inSingleQuotes = !inSingleQuotes - } - if b == '"' && !inSingleQuotes { - inDoubleQuotes = !inDoubleQuotes - } - if b == ' ' && !inSingleQuotes && !inDoubleQuotes { - result[i] = '\n' - } else { - result[i] = b - } + type cmdBuilderRes struct { + BuildPath *paths.Path `json:"build_path"` + UsedLibraries []*libraries.Library } - return string(result) -} - -func logCommandErr(command string, stdout []byte, err error, filter func(string) string) error { - message := "" - log.Println("Command error:", command, err) - if len(stdout) > 0 { - stdoutStr := string(stdout) - log.Println("------------------------------BEGIN STDOUT\n", stdoutStr, "------------------------------END STDOUT") - message += filter(stdoutStr) + type cmdRes struct { + CompilerOut string `json:"compiler_out"` + CompilerErr string `json:"compiler_err"` + BuilderResult cmdBuilderRes `json:"builder_result"` } - if exitErr, ok := err.(*exec.ExitError); ok { - stderr := exitErr.Stderr - if len(stderr) > 0 { - stderrStr := string(stderr) - log.Println("------------------------------BEGIN STDERR\n", stderrStr, "------------------------------END STDERR") - message += filter(stderrStr) - } - } - if len(message) == 0 { - return err + var res cmdRes + if err := json.Unmarshal(cmdOutput.Bytes(), &res); err != nil { + return nil, errors.Errorf("parsing arduino-cli output: %s", err) } - return errors.New(message) -} -func errMsgFilter(tempDir string) func(string) string { - if !strings.HasSuffix(tempDir, string(filepath.Separator)) { - tempDir += string(filepath.Separator) - } - return func(s string) string { - return strings.ReplaceAll(s, tempDir, "") - } + // Return only the build path + log.Println("arduino-cli output:", cmdOutput) + return res.BuilderResult.BuildPath, nil } diff --git a/handler/handler.go b/handler/handler.go index 4141da6..293ddd3 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -1,30 +1,38 @@ package handler import ( - "bytes" "context" "encoding/json" "fmt" "io" - "io/ioutil" "log" "os" "regexp" + "strconv" "strings" + "sync" "time" + "github.com/arduino/arduino-cli/arduino/builder" + "github.com/arduino/arduino-cli/executils" + "github.com/arduino/go-paths-helper" + "github.com/bcmi-labs/arduino-language-server/handler/sourcemapper" + "github.com/bcmi-labs/arduino-language-server/handler/textutils" + "github.com/bcmi-labs/arduino-language-server/lsp" + "github.com/bcmi-labs/arduino-language-server/streams" "github.com/pkg/errors" - lsp "github.com/sourcegraph/go-lsp" "github.com/sourcegraph/jsonrpc2" ) var globalCliPath string +var globalClangdPath string var enableLogging bool var asyncProcessing bool // Setup initializes global variables. -func Setup(cliPath string, _enableLogging bool, _asyncProcessing bool) { +func Setup(cliPath string, clangdPath string, _enableLogging bool, _asyncProcessing bool) { globalCliPath = cliPath + globalClangdPath = clangdPath enableLogging = _enableLogging asyncProcessing = _asyncProcessing } @@ -32,21 +40,43 @@ func Setup(cliPath string, _enableLogging bool, _asyncProcessing bool) { // CLangdStarter starts clangd and returns its stdin/out/err type CLangdStarter func() (stdin io.WriteCloser, stdout io.ReadCloser, stderr io.ReadCloser) +// InoHandler is a JSON-RPC handler that delegates messages to clangd. +type InoHandler struct { + StdioConn *jsonrpc2.Conn + ClangdConn *jsonrpc2.Conn + lspInitializeParams *lsp.InitializeParams + buildPath *paths.Path + buildSketchRoot *paths.Path + buildSketchCpp *paths.Path + buildSketchCppVersion int + buildSketchSymbols []lsp.DocumentSymbol + buildSketchSymbolsLoad bool + buildSketchSymbolsCheck bool + rebuildSketchDeadline *time.Time + rebuildSketchDeadlineMutex sync.Mutex + sketchRoot *paths.Path + sketchName string + sketchMapper *sourcemapper.InoMapper + sketchTrackedFilesCount int + docs map[lsp.DocumentURI]*lsp.TextDocumentItem + inoDocsWithDiagnostics map[lsp.DocumentURI]bool + + config lsp.BoardConfig + synchronizer Synchronizer +} + // NewInoHandler creates and configures an InoHandler. -func NewInoHandler(stdin io.ReadCloser, stdout io.WriteCloser, logStreams *StreamLogger, startClangd CLangdStarter, board Board) *InoHandler { +func NewInoHandler(stdio io.ReadWriteCloser, board lsp.Board) *InoHandler { handler := &InoHandler{ - clangdProc: ClangdProc{ - Start: startClangd, - Logs: logStreams, - }, - data: make(map[lsp.DocumentURI]*FileData), - config: BoardConfig{ + docs: map[lsp.DocumentURI]*lsp.TextDocumentItem{}, + inoDocsWithDiagnostics: map[lsp.DocumentURI]bool{}, + config: lsp.BoardConfig{ SelectedBoard: board, }, } - handler.startClangd() - stdStream := jsonrpc2.NewBufferedStream(logStreams.AttachStdInOut(stdin, stdout), jsonrpc2.VSCodeObjectCodec{}) - var stdHandler jsonrpc2.Handler = jsonrpc2.HandlerWithError(handler.FromStdio) + + stdStream := jsonrpc2.NewBufferedStream(stdio, jsonrpc2.VSCodeObjectCodec{}) + var stdHandler jsonrpc2.Handler = jsonrpc2.HandlerWithError(handler.HandleMessageFromIDE) if asyncProcessing { stdHandler = AsyncHandler{ handler: stdHandler, @@ -57,47 +87,18 @@ func NewInoHandler(stdin io.ReadCloser, stdout io.WriteCloser, logStreams *Strea if enableLogging { log.Println("Initial board configuration:", board) } - return handler -} -// InoHandler is a JSON-RPC handler that delegates messages to clangd. -type InoHandler struct { - StdioConn *jsonrpc2.Conn - ClangdConn *jsonrpc2.Conn - clangdProc ClangdProc - data map[lsp.DocumentURI]*FileData - config BoardConfig - synchronizer Synchronizer -} - -// ClangdProc contains the process input / output streams for clangd. -type ClangdProc struct { - Start func() (io.WriteCloser, io.ReadCloser, io.ReadCloser) - Logs *StreamLogger + go handler.rebuildEnvironmentLoop() + return handler } // FileData gathers information on a .ino source file. type FileData struct { - sourceText string - sourceURI lsp.DocumentURI - targetURI lsp.DocumentURI - sourceLineMap map[int]int - targetLineMap map[int]int - version int -} - -// StartClangd starts the clangd process and connects its input / output streams. -func (handler *InoHandler) startClangd() { - clangdWrite, clangdRead, clangdErr := handler.clangdProc.Start() - if enableLogging { - go io.Copy(handler.clangdProc.Logs.ClangdErr, clangdErr) - } else { - go io.Copy(ioutil.Discard, clangdErr) - } - srw := handler.clangdProc.Logs.AttachClangdInOut(clangdRead, clangdWrite) - clangdStream := jsonrpc2.NewBufferedStream(srw, jsonrpc2.VSCodeObjectCodec{}) - clangdHandler := jsonrpc2.AsyncHandler(jsonrpc2.HandlerWithError(handler.FromClangd)) - handler.ClangdConn = jsonrpc2.NewConn(context.Background(), clangdStream, clangdHandler) + sourceText string + sourceURI lsp.DocumentURI + targetURI lsp.DocumentURI + sourceMap *sourcemapper.InoMapper + version int } // StopClangd closes the connection to the clangd process. @@ -106,35 +107,208 @@ func (handler *InoHandler) StopClangd() { handler.ClangdConn = nil } -// FromStdio handles a message received from the client (via stdio). -func (handler *InoHandler) FromStdio(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) (result interface{}, err error) { - params, err := readParams(req.Method, req.Params) - if err != nil { - return +// HandleMessageFromIDE handles a message received from the IDE client (via stdio). +func (handler *InoHandler) HandleMessageFromIDE(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) (interface{}, error) { + defer streams.CatchAndLogPanic() + + needsWriteLock := map[string]bool{ + "initialize": true, + "textDocument/didOpen": true, + "textDocument/didChange": true, + "textDocument/didClose": true, + } + if needsWriteLock[req.Method] { + handler.synchronizer.DataMux.Lock() + defer handler.synchronizer.DataMux.Unlock() + } else { + handler.synchronizer.DataMux.RLock() + defer handler.synchronizer.DataMux.RUnlock() } // Handle LSP methods: transform parameters and send to clangd var uri lsp.DocumentURI + + params, err := lsp.ReadParams(req.Method, req.Params) + if err != nil { + return nil, err + } if params == nil { params = req.Params - } else { - uri, err = handler.transformParamsToClangd(ctx, req.Method, params) + } + switch p := params.(type) { + case *lsp.InitializeParams: + // method "initialize" + err = handler.initializeWorkbench(p) + + case *lsp.InitializedParams: + // method "initialized" + log.Println("--> initialized") + + case *lsp.DidOpenTextDocumentParams: + // method "textDocument/didOpen" + uri = p.TextDocument.URI + log.Printf("--> didOpen(%s@%d as '%s')", p.TextDocument.URI, p.TextDocument.Version, p.TextDocument.LanguageID) + + res, err := handler.didOpen(ctx, p) + + if res == nil { + log.Println(" --X notification is not propagated to clangd") + return nil, err // do not propagate to clangd + } + + log.Printf(" --> didOpen(%s@%d as '%s')", res.TextDocument.URI, res.TextDocument.Version, p.TextDocument.LanguageID) + params = res + + case *lsp.DidChangeTextDocumentParams: + // notification "textDocument/didChange" + uri = p.TextDocument.URI + log.Printf("--> didChange(%s@%d)", p.TextDocument.URI, p.TextDocument.Version) + for _, change := range p.ContentChanges { + log.Printf(" > %s -> %s", change.Range, strconv.Quote(change.Text)) + } + + if res, err := handler.didChange(ctx, p); err != nil { + log.Printf(" --E error: %s", err) + return nil, err + } else if res == nil { + log.Println(" --X notification is not propagated to clangd") + return nil, err // do not propagate to clangd + } else { + p = res + } + + log.Printf(" --> didChange(%s@%d)", p.TextDocument.URI, p.TextDocument.Version) + for _, change := range p.ContentChanges { + log.Printf(" > %s -> %s", change.Range, strconv.Quote(change.Text)) + } + err = handler.ClangdConn.Notify(ctx, req.Method, p) + return nil, err + + case *lsp.CompletionParams: + // method: "textDocument/completion" + uri = p.TextDocument.URI + log.Printf("--> completion(%s:%d:%d)\n", p.TextDocument.URI, p.Position.Line, p.Position.Character) + + err = handler.ino2cppTextDocumentPositionParams(&p.TextDocumentPositionParams) + log.Printf(" --> completion(%s:%d:%d)\n", p.TextDocument.URI, p.Position.Line, p.Position.Character) + + case *lsp.CodeActionParams: + // method "textDocument/codeAction" + uri = p.TextDocument.URI + log.Printf("--> codeAction(%s:%s)", p.TextDocument.URI, p.Range.Start) + + p.TextDocument, err = handler.ino2cppTextDocumentIdentifier(p.TextDocument) + if err != nil { + break + } + if p.TextDocument.URI.AsPath().EquivalentTo(handler.buildSketchCpp) { + p.Range = handler.sketchMapper.InoToCppLSPRange(uri, p.Range) + for index := range p.Context.Diagnostics { + r := &p.Context.Diagnostics[index].Range + *r = handler.sketchMapper.InoToCppLSPRange(uri, *r) + } + } + log.Printf(" --> codeAction(%s:%s)", p.TextDocument.URI, p.Range.Start) + + case *lsp.HoverParams: + // method: "textDocument/hover" + uri = p.TextDocument.URI + doc := &p.TextDocumentPositionParams + log.Printf("--> hover(%s:%d:%d)\n", doc.TextDocument.URI, doc.Position.Line, doc.Position.Character) + + err = handler.ino2cppTextDocumentPositionParams(doc) + log.Printf(" --> hover(%s:%d:%d)\n", doc.TextDocument.URI, doc.Position.Line, doc.Position.Character) + + case *lsp.DocumentSymbolParams: + // method "textDocument/documentSymbol" + uri = p.TextDocument.URI + log.Printf("--> documentSymbol(%s)", p.TextDocument.URI) + + p.TextDocument, err = handler.ino2cppTextDocumentIdentifier(p.TextDocument) + log.Printf(" --> documentSymbol(%s)", p.TextDocument.URI) + + case *lsp.DidSaveTextDocumentParams: // "textDocument/didSave": + log.Printf("--X " + req.Method) + return nil, nil + uri = p.TextDocument.URI + p.TextDocument, err = handler.ino2cppTextDocumentIdentifier(p.TextDocument) + case *lsp.DidCloseTextDocumentParams: // "textDocument/didClose": + log.Printf("--X " + req.Method) + return nil, nil + // uri = p.TextDocument.URI + // err = handler.sketchToBuildPathTextDocumentIdentifier(&p.TextDocument) + // handler.deleteFileData(uri) + // case "textDocument/signatureHelp": + // fallthrough + // case "textDocument/definition": + // fallthrough + // case "textDocument/typeDefinition": + // fallthrough + // case "textDocument/implementation": + // fallthrough + case *lsp.TextDocumentPositionParams: // "textDocument/documentHighlight": + log.Printf("--X " + req.Method) + return nil, nil + uri = p.TextDocument.URI + err = handler.ino2cppTextDocumentPositionParams(p) + case *lsp.ReferenceParams: // "textDocument/references": + log.Printf("--X " + req.Method) + return nil, nil + uri = p.TextDocument.URI + err = handler.ino2cppTextDocumentPositionParams(&p.TextDocumentPositionParams) + case *lsp.DocumentFormattingParams: // "textDocument/formatting": + log.Printf("--X " + req.Method) + return nil, nil + uri = p.TextDocument.URI + p.TextDocument, err = handler.ino2cppTextDocumentIdentifier(p.TextDocument) + case *lsp.DocumentRangeFormattingParams: // "textDocument/rangeFormatting": + log.Printf("--X " + req.Method) + return nil, nil + uri = p.TextDocument.URI + err = handler.ino2cppDocumentRangeFormattingParams(p) + case *lsp.DocumentOnTypeFormattingParams: // "textDocument/onTypeFormatting": + log.Printf("--X " + req.Method) + return nil, nil + uri = p.TextDocument.URI + err = handler.ino2cppDocumentOnTypeFormattingParams(p) + case *lsp.RenameParams: // "textDocument/rename": + log.Printf("--X " + req.Method) + return nil, nil + uri = p.TextDocument.URI + err = handler.ino2cppRenameParams(p) + case *lsp.DidChangeWatchedFilesParams: // "workspace/didChangeWatchedFiles": + log.Printf("--X " + req.Method) + return nil, nil + err = handler.ino2cppDidChangeWatchedFilesParams(p) + case *lsp.ExecuteCommandParams: // "workspace/executeCommand": + log.Printf("--X " + req.Method) + return nil, nil + err = handler.ino2cppExecuteCommand(p) } if err != nil { - return + log.Printf(" --E %s", err) + return nil, err } + + var result interface{} if req.Notif { err = handler.ClangdConn.Notify(ctx, req.Method, params) - if enableLogging { - log.Println("From stdio:", req.Method) - } + // log.Println(" sent", req.Method, "notification to clangd") } else { ctx, cancel := context.WithTimeout(ctx, 800*time.Millisecond) defer cancel() - result, err = sendRequest(ctx, handler.ClangdConn, req.Method, params) - if enableLogging { - log.Println("From stdio:", req.Method, "id", req.ID) - } + result, err = lsp.SendRequest(ctx, handler.ClangdConn, req.Method, params) + // log.Println(" sent", req.Method, "request id", req.ID, " to clangd") + } + if err == nil && handler.buildSketchSymbolsLoad { + handler.buildSketchSymbolsLoad = false + log.Println("--! Resfreshing document symbols") + err = handler.refreshCppDocumentSymbols() + } + if err == nil && handler.buildSketchSymbolsCheck { + handler.buildSketchSymbolsCheck = false + log.Println("--! Resfreshing document symbols") + err = handler.checkCppDocumentSymbols() } if err != nil { // Exit the process and trigger a restart by the client in case of a severe error @@ -146,14 +320,13 @@ func (handler *InoHandler) FromStdio(ctx context.Context, conn *jsonrpc2.Conn, r log.Println("The clangd process has lost track of the open document.") handler.exit() } - return } // Transform and return the result if result != nil { result = handler.transformClangdResult(req.Method, uri, result) } - return + return result, err } func (handler *InoHandler) exit() { @@ -162,181 +335,280 @@ func (handler *InoHandler) exit() { os.Exit(1) } -func (handler *InoHandler) transformParamsToClangd(ctx context.Context, method string, params interface{}) (uri lsp.DocumentURI, err error) { - needsWriteLock := method == "textDocument/didOpen" || method == "textDocument/didChange" || method == "textDocument/didClose" - if needsWriteLock { - handler.synchronizer.DataMux.Lock() - defer handler.synchronizer.DataMux.Unlock() +func (handler *InoHandler) initializeWorkbench(params *lsp.InitializeParams) error { + currCppTextVersion := 0 + if params != nil { + log.Printf("--> initialize(%s)\n", params.RootURI) + handler.lspInitializeParams = params + handler.sketchRoot = params.RootURI.AsPath() + handler.sketchName = handler.sketchRoot.Base() } else { - handler.synchronizer.DataMux.RLock() - defer handler.synchronizer.DataMux.RUnlock() + currCppTextVersion = handler.sketchMapper.CppText.Version + log.Printf("--> RE-initialize()\n") } - switch method { - case "textDocument/didOpen": - p := params.(*lsp.DidOpenTextDocumentParams) - uri = p.TextDocument.URI - err = handler.ino2cppTextDocumentItem(ctx, &p.TextDocument) - case "textDocument/didChange": - p := params.(*lsp.DidChangeTextDocumentParams) - uri = p.TextDocument.URI - err = handler.ino2cppDidChangeTextDocumentParams(ctx, p) - case "textDocument/didSave": - p := params.(*lsp.DidSaveTextDocumentParams) - uri = p.TextDocument.URI - err = handler.ino2cppTextDocumentIdentifier(&p.TextDocument) - case "textDocument/didClose": - p := params.(*lsp.DidCloseTextDocumentParams) - uri = p.TextDocument.URI - err = handler.ino2cppTextDocumentIdentifier(&p.TextDocument) - handler.deleteFileData(uri) - case "textDocument/completion": - p := params.(*lsp.CompletionParams) - uri = p.TextDocument.URI - err = handler.ino2cppTextDocumentPositionParams(&p.TextDocumentPositionParams) - case "textDocument/codeAction": - p := params.(*lsp.CodeActionParams) - uri = p.TextDocument.URI - err = handler.ino2cppCodeActionParams(p) - case "textDocument/signatureHelp": - fallthrough - case "textDocument/hover": - fallthrough - case "textDocument/definition": - fallthrough - case "textDocument/typeDefinition": - fallthrough - case "textDocument/implementation": - fallthrough - case "textDocument/documentHighlight": - p := params.(*lsp.TextDocumentPositionParams) - uri = p.TextDocument.URI - err = handler.ino2cppTextDocumentPositionParams(p) - case "textDocument/references": - p := params.(*lsp.ReferenceParams) - uri = p.TextDocument.URI - err = handler.ino2cppTextDocumentPositionParams(&p.TextDocumentPositionParams) - case "textDocument/formatting": - p := params.(*lsp.DocumentFormattingParams) - uri = p.TextDocument.URI - err = handler.ino2cppTextDocumentIdentifier(&p.TextDocument) - case "textDocument/rangeFormatting": - p := params.(*lsp.DocumentRangeFormattingParams) - uri = p.TextDocument.URI - err = handler.ino2cppDocumentRangeFormattingParams(p) - case "textDocument/onTypeFormatting": - p := params.(*lsp.DocumentOnTypeFormattingParams) - uri = p.TextDocument.URI - err = handler.ino2cppDocumentOnTypeFormattingParams(p) - case "textDocument/documentSymbol": - p := params.(*lsp.DocumentSymbolParams) - uri = p.TextDocument.URI - err = handler.ino2cppTextDocumentIdentifier(&p.TextDocument) - case "textDocument/rename": - p := params.(*lsp.RenameParams) - uri = p.TextDocument.URI - err = handler.ino2cppRenameParams(p) - case "workspace/didChangeWatchedFiles": - p := params.(*lsp.DidChangeWatchedFilesParams) - err = handler.ino2cppDidChangeWatchedFilesParams(p) - case "workspace/executeCommand": - p := params.(*lsp.ExecuteCommandParams) - err = handler.ino2cppExecuteCommand(p) + if buildPath, err := handler.generateBuildEnvironment(); err == nil { + handler.buildPath = buildPath + handler.buildSketchRoot = buildPath.Join("sketch") + } else { + return err + } + handler.buildSketchCpp = handler.buildSketchRoot.Join(handler.sketchName + ".ino.cpp") + handler.buildSketchCppVersion = 1 + handler.lspInitializeParams.RootPath = handler.buildSketchRoot.String() + handler.lspInitializeParams.RootURI = lsp.NewDocumentURIFromPath(handler.buildSketchRoot) + + if cppContent, err := handler.buildSketchCpp.ReadFile(); err == nil { + handler.sketchMapper = sourcemapper.CreateInoMapper(cppContent) + handler.sketchMapper.CppText.Version = currCppTextVersion + 1 + } else { + return errors.WithMessage(err, "reading generated cpp file from sketch") + } + + if params == nil { + // If we are restarting re-synchronize clangd + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + cppURI := lsp.NewDocumentURIFromPath(handler.buildSketchCpp) + cppTextDocumentIdentifier := lsp.TextDocumentIdentifier{URI: cppURI} + + syncEvent := &lsp.DidChangeTextDocumentParams{ + TextDocument: lsp.VersionedTextDocumentIdentifier{ + TextDocumentIdentifier: cppTextDocumentIdentifier, + Version: handler.sketchMapper.CppText.Version, + }, + ContentChanges: []lsp.TextDocumentContentChangeEvent{ + {Text: handler.sketchMapper.CppText.Text}, // Full text change + }, + } + + if err := handler.ClangdConn.Notify(ctx, "textDocument/didChange", syncEvent); err != nil { + log.Println(" error reinitilizing clangd:", err) + return err + } + } else { + // Otherwise start clangd! + clangdStdout, clangdStdin, clangdStderr := startClangd(handler.buildPath, handler.buildSketchCpp) + clangdStdio := streams.NewReadWriteCloser(clangdStdin, clangdStdout) + if enableLogging { + clangdStdio = streams.LogReadWriteCloserAs(clangdStdio, "inols-clangd.log") + go io.Copy(streams.OpenLogFileAs("inols-clangd-err.log"), clangdStderr) + } else { + go io.Copy(os.Stderr, clangdStderr) + } + + clangdStream := jsonrpc2.NewBufferedStream(clangdStdio, jsonrpc2.VSCodeObjectCodec{}) + clangdHandler := jsonrpc2.AsyncHandler(jsonrpc2.HandlerWithError(handler.FromClangd)) + handler.ClangdConn = jsonrpc2.NewConn(context.Background(), clangdStream, clangdHandler) } - return + + return nil } -func (handler *InoHandler) createFileData(ctx context.Context, sourceURI lsp.DocumentURI, sourceText string, version int) (*FileData, []byte, error) { - sourcePath := uriToPath(sourceURI) - targetPath, targetBytes, err := generateCpp([]byte(sourceText), sourcePath, handler.config.SelectedBoard.Fqbn) +func (handler *InoHandler) refreshCppDocumentSymbols() error { + // Query source code symbols + cppURI := lsp.NewDocumentURIFromPath(handler.buildSketchCpp) + log.Printf(" --> documentSymbol(%s)", cppURI) + result, err := lsp.SendRequest(context.Background(), handler.ClangdConn, "textDocument/documentSymbol", &lsp.DocumentSymbolParams{ + TextDocument: lsp.TextDocumentIdentifier{URI: cppURI}, + }) if err != nil { - err = handler.handleError(ctx, err) - if len(targetPath) == 0 { - return nil, nil, err + return errors.WithMessage(err, "quering source code symbols") + } + result = handler.transformClangdResult("textDocument/documentSymbol", cppURI, result) + if symbols, ok := result.([]lsp.DocumentSymbol); !ok { + return errors.WithMessage(err, "quering source code symbols (2)") + } else { + // Filter non-functions symbols + i := 0 + for _, symbol := range symbols { + if symbol.Kind != lsp.SKFunction { + continue + } + symbols[i] = symbol + i++ } - // Fallback: use the source text unchanged - targetBytes, err = copyIno2Cpp(sourceText, targetPath) - if err != nil { - return nil, nil, err + symbols = symbols[:i] + for _, symbol := range symbols { + log.Printf(" symbol: %s %s", symbol.Kind, symbol.Name) } + handler.buildSketchSymbols = symbols } + return nil +} - targetURI := pathToURI(targetPath) - sourceLineMap, targetLineMap := createSourceMaps(bytes.NewReader(targetBytes)) - data := &FileData{ - sourceText, - sourceURI, - targetURI, - sourceLineMap, - targetLineMap, - version, +func (handler *InoHandler) checkCppDocumentSymbols() error { + oldSymbols := handler.buildSketchSymbols + if err := handler.refreshCppDocumentSymbols(); err != nil { + return err + } + if len(oldSymbols) != len(handler.buildSketchSymbols) { + log.Println("--! new symbols detected, triggering sketch rebuild!") + handler.scheduleRebuildEnvironment() + return nil + } + for i, old := range oldSymbols { + if newName := handler.buildSketchSymbols[i].Name; old.Name != newName { + log.Printf("--! symbols changed, triggering sketch rebuild: '%s' -> '%s'", old.Name, newName) + handler.scheduleRebuildEnvironment() + return nil + } } - handler.data[sourceURI] = data - handler.data[targetURI] = data - return data, targetBytes, nil + return nil } -func (handler *InoHandler) updateFileData(ctx context.Context, data *FileData, change *lsp.TextDocumentContentChangeEvent) (err error) { - rang := change.Range - if rang == nil || rang.Start.Line != rang.End.Line { - // Update the source text and regenerate the cpp code - var newSourceText string - if rang == nil { - newSourceText = change.Text - } else { - newSourceText, err = applyTextChange(data.sourceText, *rang, change.Text) - if err != nil { - return err - } +func startClangd(compileCommandsDir, sketchCpp *paths.Path) (io.WriteCloser, io.ReadCloser, io.ReadCloser) { + // Open compile_commands.json and find the main cross-compiler executable + compileCommands, err := builder.LoadCompilationDatabase(compileCommandsDir.Join("compile_commands.json")) + if err != nil { + panic("could not find compile_commands.json") + } + compilers := map[string]bool{} + for _, cmd := range compileCommands.Contents { + if len(cmd.Arguments) == 0 { + panic("invalid empty argument field in compile_commands.json") } - targetBytes, err := updateCpp([]byte(newSourceText), uriToPath(data.sourceURI), handler.config.SelectedBoard.Fqbn, false, uriToPath(data.targetURI)) - if err != nil { - if rang == nil { - // Fallback: use the source text unchanged - targetBytes, err = copyIno2Cpp(newSourceText, uriToPath(data.targetURI)) - if err != nil { - return err - } - } else { - // Fallback: try to apply a multi-line update - targetStartLine := data.targetLineMap[rang.Start.Line] - targetEndLine := data.targetLineMap[rang.End.Line] - data.sourceText = newSourceText - updateSourceMaps(data.sourceLineMap, data.targetLineMap, rang.End.Line-rang.Start.Line, rang.Start.Line, change.Text) - rang.Start.Line = targetStartLine - rang.End.Line = targetEndLine - return nil - } + compilers[cmd.Arguments[0]] = true + } + if len(compilers) == 0 { + panic("main compiler not found") + } + + // Start clangd + args := []string{ + globalClangdPath, + "-log=verbose", + fmt.Sprintf(`--compile-commands-dir=%s`, compileCommandsDir), + } + for compiler := range compilers { + args = append(args, fmt.Sprintf("-query-driver=%s", compiler)) + } + if enableLogging { + log.Println(" Starting clangd:", strings.Join(args, " ")) + } + if clangdCmd, err := executils.NewProcess(args...); err != nil { + panic("starting clangd: " + err.Error()) + } else if clangdIn, err := clangdCmd.StdinPipe(); err != nil { + panic("getting clangd stdin: " + err.Error()) + } else if clangdOut, err := clangdCmd.StdoutPipe(); err != nil { + panic("getting clangd stdout: " + err.Error()) + } else if clangdErr, err := clangdCmd.StderrPipe(); err != nil { + panic("getting clangd stderr: " + err.Error()) + } else if err := clangdCmd.Start(); err != nil { + panic("running clangd: " + err.Error()) + } else { + return clangdIn, clangdOut, clangdErr + } +} + +func (handler *InoHandler) didOpen(ctx context.Context, inoDidOpen *lsp.DidOpenTextDocumentParams) (*lsp.DidOpenTextDocumentParams, error) { + // Add the TextDocumentItem in the tracked files list + inoItem := inoDidOpen.TextDocument + handler.docs[inoItem.URI] = &inoItem + + // If we are tracking a .ino... + if inoItem.URI.Ext() == ".ino" { + handler.sketchTrackedFilesCount++ + log.Printf(" increasing .ino tracked files count: %d", handler.sketchTrackedFilesCount) + + // notify clang that sketchCpp has been opened only once + if handler.sketchTrackedFilesCount != 1 { + return nil, nil } - sourceLineMap, targetLineMap := createSourceMaps(bytes.NewReader(targetBytes)) - data.sourceText = newSourceText - data.sourceLineMap = sourceLineMap - data.targetLineMap = targetLineMap + // trigger a documentSymbol load + handler.buildSketchSymbolsLoad = true + } + + cppItem, err := handler.ino2cppTextDocumentItem(inoItem) + return &lsp.DidOpenTextDocumentParams{ + TextDocument: cppItem, + }, err +} + +func (handler *InoHandler) ino2cppTextDocumentItem(inoItem lsp.TextDocumentItem) (cppItem lsp.TextDocumentItem, err error) { + cppURI, err := handler.ino2cppDocumentURI(inoItem.URI) + if err != nil { + return cppItem, err + } + cppItem.URI = cppURI - change.Text = string(targetBytes) - change.Range = nil - change.RangeLength = 0 + if cppURI.AsPath().EquivalentTo(handler.buildSketchCpp) { + cppItem.LanguageID = "cpp" + cppItem.Text = handler.sketchMapper.CppText.Text + cppItem.Version = handler.sketchMapper.CppText.Version } else { - // Apply an update to a single line both to the source and the target text - targetLine := data.targetLineMap[rang.Start.Line] - data.sourceText, err = applyTextChange(data.sourceText, *rang, change.Text) - if err != nil { - return err + cppItem.Text = handler.docs[inoItem.URI].Text + cppItem.Version = handler.docs[inoItem.URI].Version + } + + return cppItem, nil +} + +func (handler *InoHandler) didChange(ctx context.Context, req *lsp.DidChangeTextDocumentParams) (*lsp.DidChangeTextDocumentParams, error) { + doc := req.TextDocument + + trackedDoc, ok := handler.docs[doc.URI] + if !ok { + return nil, unknownURI(doc.URI) + } + textutils.ApplyLSPTextDocumentContentChangeEvent(trackedDoc, req.ContentChanges, doc.Version) + + // If changes are applied to a .ino file we increment the global .ino.cpp versioning + // for each increment of the single .ino file. + if doc.URI.Ext() == ".ino" { + + cppChanges := []lsp.TextDocumentContentChangeEvent{} + for _, inoChange := range req.ContentChanges { + dirty := handler.sketchMapper.ApplyTextChange(doc.URI, inoChange) + if dirty { + // TODO: Detect changes in critical lines (for example function definitions) + // and trigger arduino-preprocessing + clangd restart. + + log.Println("--! DIRTY CHANGE, force sketch rebuild!") + handler.scheduleRebuildEnvironment() + } + + // log.Println("New version:----------") + // log.Println(handler.sketchMapper.CppText.Text) + // log.Println("----------------------") + + cppRange, ok := handler.sketchMapper.InoToCppLSPRangeOk(doc.URI, *inoChange.Range) + if !ok { + return nil, errors.Errorf("invalid change range %s:%s", doc.URI, *inoChange.Range) + } + cppChange := lsp.TextDocumentContentChangeEvent{ + Range: &cppRange, + RangeLength: inoChange.RangeLength, + Text: inoChange.Text, + } + cppChanges = append(cppChanges, cppChange) } - updateSourceMaps(data.sourceLineMap, data.targetLineMap, 0, rang.Start.Line, change.Text) - rang.Start.Line = targetLine - rang.End.Line = targetLine + // build a cpp equivalent didChange request + cppReq := &lsp.DidChangeTextDocumentParams{ + ContentChanges: cppChanges, + TextDocument: lsp.VersionedTextDocumentIdentifier{ + TextDocumentIdentifier: lsp.TextDocumentIdentifier{ + URI: lsp.NewDocumentURIFromPath(handler.buildSketchCpp), + }, + Version: handler.sketchMapper.CppText.Version, + }, + } + return cppReq, nil } - return nil -} -func (handler *InoHandler) deleteFileData(sourceURI lsp.DocumentURI) { - if data, ok := handler.data[sourceURI]; ok { - delete(handler.data, data.sourceURI) - delete(handler.data, data.targetURI) + // If changes are applied to other files pass them by converting just the URI + cppDoc, err := handler.ino2cppVersionedTextDocumentIdentifier(req.TextDocument) + if err != nil { + return nil, err } + cppReq := &lsp.DidChangeTextDocumentParams{ + TextDocument: cppDoc, + ContentChanges: req.ContentChanges, + } + return cppReq, err } func (handler *InoHandler) handleError(ctx context.Context, err error) error { @@ -374,218 +646,263 @@ func (handler *InoHandler) handleError(ctx context.Context, err error) error { return errors.New(message) } -func (handler *InoHandler) ino2cppTextDocumentIdentifier(doc *lsp.TextDocumentIdentifier) error { - if data, ok := handler.data[doc.URI]; ok { - doc.URI = data.targetURI - return nil - } - return unknownURI(doc.URI) +func (handler *InoHandler) ino2cppVersionedTextDocumentIdentifier(doc lsp.VersionedTextDocumentIdentifier) (lsp.VersionedTextDocumentIdentifier, error) { + cppURI, err := handler.ino2cppDocumentURI(doc.URI) + res := doc + res.URI = cppURI + return res, err } -func (handler *InoHandler) ino2cppTextDocumentItem(ctx context.Context, doc *lsp.TextDocumentItem) error { - if strings.HasSuffix(string(doc.URI), ".ino") { - data, targetBytes, err := handler.createFileData(ctx, doc.URI, doc.Text, doc.Version) - if err != nil { - return err - } - doc.LanguageID = "cpp" - doc.URI = data.targetURI - doc.Text = string(targetBytes) - } - return nil +func (handler *InoHandler) ino2cppTextDocumentIdentifier(doc lsp.TextDocumentIdentifier) (lsp.TextDocumentIdentifier, error) { + cppURI, err := handler.ino2cppDocumentURI(doc.URI) + res := doc + res.URI = cppURI + return res, err } -func (handler *InoHandler) ino2cppDidChangeTextDocumentParams(ctx context.Context, params *lsp.DidChangeTextDocumentParams) error { - handler.ino2cppTextDocumentIdentifier(¶ms.TextDocument.TextDocumentIdentifier) - if data, ok := handler.data[params.TextDocument.URI]; ok { - for index := range params.ContentChanges { - err := handler.updateFileData(ctx, data, ¶ms.ContentChanges[index]) - if err != nil { - return err - } - } - data.version = params.TextDocument.Version - return nil +func (handler *InoHandler) ino2cppDocumentURI(inoURI lsp.DocumentURI) (lsp.DocumentURI, error) { + // Sketchbook/Sketch/Sketch.ino -> build-path/sketch/Sketch.ino.cpp + // Sketchbook/Sketch/AnotherTab.ino -> build-path/sketch/Sketch.ino.cpp (different section from above) + // Sketchbook/Sketch/AnotherFile.cpp -> build-path/sketch/AnotherFile.cpp (1:1) + // another/path/source.cpp -> unchanged + + // Convert sketch path to build path + inoPath := inoURI.AsPath() + if inoPath.Ext() == ".ino" { + return lsp.NewDocumentURIFromPath(handler.buildSketchCpp), nil } - return unknownURI(params.TextDocument.URI) + + inside, err := inoPath.IsInsideDir(handler.sketchRoot) + if err != nil { + log.Printf(" could not determine if '%s' is inside '%s'", inoPath, handler.sketchRoot) + return "", unknownURI(inoURI) + } + if !inside { + log.Printf(" passing doc identifier to '%s' as-is", inoPath) + return inoURI, nil + } + + rel, err := handler.sketchRoot.RelTo(inoPath) + if err == nil { + cppPath := handler.buildSketchRoot.JoinPath(rel) + log.Printf(" URI: '%s' -> '%s'", inoPath, cppPath) + return lsp.NewDocumentURIFromPath(cppPath), nil + } + + log.Printf(" could not determine rel-path of '%s' in '%s': %s", inoPath, handler.sketchRoot, err) + return "", err } -func (handler *InoHandler) ino2cppTextDocumentPositionParams(params *lsp.TextDocumentPositionParams) error { - handler.ino2cppTextDocumentIdentifier(¶ms.TextDocument) - if data, ok := handler.data[params.TextDocument.URI]; ok { - targetLine := data.targetLineMap[params.Position.Line] - params.Position.Line = targetLine - return nil +func (handler *InoHandler) cpp2inoDocumentURI(cppURI lsp.DocumentURI, cppRange lsp.Range) (lsp.DocumentURI, lsp.Range, error) { + // Sketchbook/Sketch/Sketch.ino <- build-path/sketch/Sketch.ino.cpp + // Sketchbook/Sketch/AnotherTab.ino <- build-path/sketch/Sketch.ino.cpp (different section from above) + // Sketchbook/Sketch/AnotherFile.cpp <- build-path/sketch/AnotherFile.cpp (1:1) + // another/path/source.cpp <- unchanged + + // Convert build path to sketch path + cppPath := cppURI.AsPath() + if cppPath.EquivalentTo(handler.buildSketchCpp) { + inoPath, inoRange := handler.sketchMapper.CppToInoRange(cppRange) + return lsp.NewDocumentURI(inoPath), inoRange, nil } - return unknownURI(params.TextDocument.URI) + + inside, err := cppPath.IsInsideDir(handler.buildSketchRoot) + if err != nil { + log.Printf(" could not determine if '%s' is inside '%s'", cppPath, handler.buildSketchRoot) + return "", lsp.Range{}, err + } + if !inside { + log.Printf(" keep doc identifier to '%s' as-is", cppPath) + return cppURI, cppRange, nil + } + + rel, err := handler.buildSketchRoot.RelTo(cppPath) + if err == nil { + inoPath := handler.sketchRoot.JoinPath(rel) + log.Printf(" URI: '%s' -> '%s'", cppPath, inoPath) + return lsp.NewDocumentURIFromPath(inoPath), cppRange, nil + } + + log.Printf(" could not determine rel-path of '%s' in '%s': %s", cppPath, handler.buildSketchRoot, err) + return "", lsp.Range{}, err } -func (handler *InoHandler) ino2cppCodeActionParams(params *lsp.CodeActionParams) error { - handler.ino2cppTextDocumentIdentifier(¶ms.TextDocument) - if data, ok := handler.data[params.TextDocument.URI]; ok { - params.Range.Start.Line = data.targetLineMap[params.Range.Start.Line] - params.Range.End.Line = data.targetLineMap[params.Range.End.Line] - for index := range params.Context.Diagnostics { - r := ¶ms.Context.Diagnostics[index].Range - r.Start.Line = data.targetLineMap[r.Start.Line] - r.End.Line = data.targetLineMap[r.End.Line] +func (handler *InoHandler) ino2cppTextDocumentPositionParams(params *lsp.TextDocumentPositionParams) error { + sourceURI := params.TextDocument.URI + if strings.HasSuffix(string(sourceURI), ".ino") { + line, ok := handler.sketchMapper.InoToCppLineOk(sourceURI, params.Position.Line) + if !ok { + log.Printf(" invalid line requested: %s:%d", sourceURI, params.Position.Line) + return unknownURI(params.TextDocument.URI) } - return nil + params.Position.Line = line } - return unknownURI(params.TextDocument.URI) + cppDoc, err := handler.ino2cppTextDocumentIdentifier(params.TextDocument) + if err != nil { + return err + } + params.TextDocument = cppDoc + return nil } func (handler *InoHandler) ino2cppDocumentRangeFormattingParams(params *lsp.DocumentRangeFormattingParams) error { - handler.ino2cppTextDocumentIdentifier(¶ms.TextDocument) - if data, ok := handler.data[params.TextDocument.URI]; ok { - params.Range.Start.Line = data.targetLineMap[params.Range.Start.Line] - params.Range.End.Line = data.targetLineMap[params.Range.End.Line] - return nil - } + panic("not implemented") + // handler.sketchToBuildPathTextDocumentIdentifier(¶ms.TextDocument) + // if data, ok := handler.data[params.TextDocument.URI]; ok { + // params.Range = data.sourceMap.InoToCppLSPRange(data.sourceURI, params.Range) + // return nil + // } return unknownURI(params.TextDocument.URI) } func (handler *InoHandler) ino2cppDocumentOnTypeFormattingParams(params *lsp.DocumentOnTypeFormattingParams) error { - handler.ino2cppTextDocumentIdentifier(¶ms.TextDocument) - if data, ok := handler.data[params.TextDocument.URI]; ok { - params.Position.Line = data.targetLineMap[params.Position.Line] - return nil - } + panic("not implemented") + // handler.sketchToBuildPathTextDocumentIdentifier(¶ms.TextDocument) + // if data, ok := handler.data[params.TextDocument.URI]; ok { + // params.Position.Line = data.sourceMap.InoToCppLine(data.sourceURI, params.Position.Line) + // return nil + // } return unknownURI(params.TextDocument.URI) } func (handler *InoHandler) ino2cppRenameParams(params *lsp.RenameParams) error { - handler.ino2cppTextDocumentIdentifier(¶ms.TextDocument) - if data, ok := handler.data[params.TextDocument.URI]; ok { - params.Position.Line = data.targetLineMap[params.Position.Line] - return nil - } + panic("not implemented") + // handler.sketchToBuildPathTextDocumentIdentifier(¶ms.TextDocument) + // if data, ok := handler.data[params.TextDocument.URI]; ok { + // params.Position.Line = data.sourceMap.InoToCppLine(data.sourceURI, params.Position.Line) + // return nil + // } return unknownURI(params.TextDocument.URI) } func (handler *InoHandler) ino2cppDidChangeWatchedFilesParams(params *lsp.DidChangeWatchedFilesParams) error { - for index := range params.Changes { - fileEvent := ¶ms.Changes[index] - if data, ok := handler.data[fileEvent.URI]; ok { - fileEvent.URI = data.targetURI - } - } + panic("not implemented") + // for index := range params.Changes { + // fileEvent := ¶ms.Changes[index] + // if data, ok := handler.data[fileEvent.URI]; ok { + // fileEvent.URI = data.targetURI + // } + // } return nil } func (handler *InoHandler) ino2cppExecuteCommand(executeCommand *lsp.ExecuteCommandParams) error { - if len(executeCommand.Arguments) == 1 { - arg := handler.parseCommandArgument(executeCommand.Arguments[0]) - if workspaceEdit, ok := arg.(*lsp.WorkspaceEdit); ok { - executeCommand.Arguments[0] = handler.ino2cppWorkspaceEdit(workspaceEdit) - } - } + panic("not implemented") + // if len(executeCommand.Arguments) == 1 { + // arg := handler.parseCommandArgument(executeCommand.Arguments[0]) + // if workspaceEdit, ok := arg.(*lsp.WorkspaceEdit); ok { + // executeCommand.Arguments[0] = handler.ino2cppWorkspaceEdit(workspaceEdit) + // } + // } return nil } func (handler *InoHandler) ino2cppWorkspaceEdit(origEdit *lsp.WorkspaceEdit) *lsp.WorkspaceEdit { - newEdit := lsp.WorkspaceEdit{Changes: make(map[string][]lsp.TextEdit)} - for uri, edit := range origEdit.Changes { - if data, ok := handler.data[lsp.DocumentURI(uri)]; ok { - newValue := make([]lsp.TextEdit, len(edit)) - for index := range edit { - r := edit[index].Range - newValue[index] = lsp.TextEdit{ - NewText: edit[index].NewText, - Range: lsp.Range{ - Start: lsp.Position{Line: data.targetLineMap[r.Start.Line], Character: r.Start.Character}, - End: lsp.Position{Line: data.targetLineMap[r.End.Line], Character: r.End.Character}, - }, - } - } - newEdit.Changes[string(data.targetURI)] = newValue - } else { - newEdit.Changes[uri] = edit - } - } + panic("not implemented") + newEdit := lsp.WorkspaceEdit{Changes: make(map[lsp.DocumentURI][]lsp.TextEdit)} + // for uri, edit := range origEdit.Changes { + // if data, ok := handler.data[lsp.DocumentURI(uri)]; ok { + // newValue := make([]lsp.TextEdit, len(edit)) + // for index := range edit { + // newValue[index] = lsp.TextEdit{ + // NewText: edit[index].NewText, + // Range: data.sourceMap.InoToCppLSPRange(data.sourceURI, edit[index].Range), + // } + // } + // newEdit.Changes[string(data.targetURI)] = newValue + // } else { + // newEdit.Changes[uri] = edit + // } + // } return &newEdit } func (handler *InoHandler) transformClangdResult(method string, uri lsp.DocumentURI, result interface{}) interface{} { - handler.synchronizer.DataMux.RLock() - defer handler.synchronizer.DataMux.RUnlock() + cppToIno := uri != "" && uri.AsPath().EquivalentTo(handler.buildSketchCpp) - switch method { - case "textDocument/completion": - r := result.(*lsp.CompletionList) - handler.cpp2inoCompletionList(r, uri) - case "textDocument/codeAction": - r := result.(*[]*commandOrCodeAction) - for index := range *r { - command := (*r)[index].Command - if command != nil { - handler.cpp2inoCommand(command) - } - codeAction := (*r)[index].CodeAction - if codeAction != nil { - handler.cpp2inoCodeAction(codeAction, uri) - } - } - case "textDocument/hover": - r := result.(*Hover) + switch r := result.(type) { + case *lsp.Hover: + // method "textDocument/hover" if len(r.Contents.Value) == 0 { return nil } - handler.cpp2inoHover(r, uri) - case "textDocument/definition": - fallthrough - case "textDocument/typeDefinition": - fallthrough - case "textDocument/implementation": - fallthrough - case "textDocument/references": - r := result.(*[]lsp.Location) + if cppToIno { + _, *r.Range = handler.sketchMapper.CppToInoRange(*r.Range) + } + log.Printf("<-- hover(%s)", strconv.Quote(r.Contents.Value)) + return r + + case *lsp.CompletionList: + // method "textDocument/completion" + newItems := make([]lsp.CompletionItem, 0) + + for _, item := range r.Items { + if !strings.HasPrefix(item.InsertText, "_") { + if cppToIno && item.TextEdit != nil { + _, item.TextEdit.Range = handler.sketchMapper.CppToInoRange(item.TextEdit.Range) + } + newItems = append(newItems, item) + } + } + r.Items = newItems + log.Printf("<-- completion(%d items)", len(r.Items)) + return r + + case *lsp.DocumentSymbolArrayOrSymbolInformationArray: + // method "textDocument/documentSymbol" + + if r.DocumentSymbolArray != nil { + // Treat the input as []DocumentSymbol + return handler.cpp2inoDocumentSymbols(*r.DocumentSymbolArray, uri) + } else if r.SymbolInformationArray != nil { + // Treat the input as []SymbolInformation + return handler.cpp2inoSymbolInformation(*r.SymbolInformationArray) + } else { + // Treat the input as null + } + + case *[]lsp.CommandOrCodeAction: + // method "textDocument/codeAction" + log.Printf(" <-- codeAction(%d elements)", len(*r)) + for i, item := range *r { + if item.Command != nil { + log.Printf(" > Command: %s", item.Command.Title) + } + if item.CodeAction != nil { + log.Printf(" > CodeAction: %s", item.CodeAction.Title) + } + (*r)[i] = lsp.CommandOrCodeAction{ + Command: handler.Cpp2InoCommand(item.Command), + CodeAction: handler.cpp2inoCodeAction(item.CodeAction, uri), + } + } + log.Printf("<-- codeAction(%d elements)", len(*r)) + + // case "textDocument/definition": + // fallthrough + // case "textDocument/typeDefinition": + // fallthrough + // case "textDocument/implementation": + // fallthrough + case *[]lsp.Location: // "textDocument/references": for index := range *r { handler.cpp2inoLocation(&(*r)[index]) } - case "textDocument/documentHighlight": - r := result.(*[]lsp.DocumentHighlight) + case *[]lsp.DocumentHighlight: // "textDocument/documentHighlight": for index := range *r { handler.cpp2inoDocumentHighlight(&(*r)[index], uri) } - case "textDocument/formatting": - fallthrough - case "textDocument/rangeFormatting": - fallthrough - case "textDocument/onTypeFormatting": - r := result.(*[]lsp.TextEdit) + // case "textDocument/formatting": + // fallthrough + // case "textDocument/rangeFormatting": + // fallthrough + case *[]lsp.TextEdit: // "textDocument/onTypeFormatting": for index := range *r { handler.cpp2inoTextEdit(&(*r)[index], uri) } - case "textDocument/documentSymbol": - r, ok := result.(*[]*documentSymbolOrSymbolInformation) - - if !ok || len(*r) == 0 { - return result - } - - slice := *r - if slice[0].DocumentSymbol != nil { - // Treat the input as []DocumentSymbol - symbols := make([]DocumentSymbol, len(slice)) - for index := range slice { - symbols[index] = *slice[index].DocumentSymbol - } - return handler.cpp2inoDocumentSymbols(symbols, uri) - } - if slice[0].SymbolInformation != nil { - // Treat the input as []SymbolInformation - symbols := make([]*lsp.SymbolInformation, len(slice)) - for i, s := range slice { - symbols[i] = s.SymbolInformation - } - return handler.cpp2inoSymbolInformation(symbols) - } - case "textDocument/rename": - r := result.(*lsp.WorkspaceEdit) + case *lsp.WorkspaceEdit: // "textDocument/rename": return handler.cpp2inoWorkspaceEdit(r) - case "workspace/symbol": - r := result.(*[]lsp.SymbolInformation) + case *[]lsp.SymbolInformation: // "workspace/symbol": for index := range *r { handler.cpp2inoLocation(&(*r)[index].Location) } @@ -593,163 +910,318 @@ func (handler *InoHandler) transformClangdResult(method string, uri lsp.Document return result } -func (handler *InoHandler) cpp2inoCompletionList(list *lsp.CompletionList, uri lsp.DocumentURI) { - if data, ok := handler.data[uri]; ok { - newItems := make([]lsp.CompletionItem, 0, len(list.Items)) - for _, item := range list.Items { - if !strings.HasPrefix(item.InsertText, "_") { - if item.TextEdit != nil { - r := &item.TextEdit.Range - r.Start.Line = data.sourceLineMap[r.Start.Line] - r.End.Line = data.sourceLineMap[r.End.Line] - } - newItems = append(newItems, item) - } - } - list.Items = newItems +func (handler *InoHandler) cpp2inoCodeAction(codeAction *lsp.CodeAction, uri lsp.DocumentURI) *lsp.CodeAction { + if codeAction == nil { + return nil } -} - -func (handler *InoHandler) cpp2inoCodeAction(codeAction *CodeAction, uri lsp.DocumentURI) { - codeAction.Edit = handler.cpp2inoWorkspaceEdit(codeAction.Edit) - if data, ok := handler.data[uri]; ok { - for index := range codeAction.Diagnostics { - r := &codeAction.Diagnostics[index].Range - r.Start.Line = data.sourceLineMap[r.Start.Line] - r.End.Line = data.sourceLineMap[r.End.Line] - } + inoCodeAction := &lsp.CodeAction{ + Title: codeAction.Title, + Kind: codeAction.Kind, + Edit: handler.cpp2inoWorkspaceEdit(codeAction.Edit), + Diagnostics: codeAction.Diagnostics, + Command: handler.Cpp2InoCommand(codeAction.Command), } -} - -func (handler *InoHandler) cpp2inoCommand(command *lsp.Command) { - if len(command.Arguments) == 1 { - arg := handler.parseCommandArgument(command.Arguments[0]) - if workspaceEdit, ok := arg.(*lsp.WorkspaceEdit); ok { - command.Arguments[0] = handler.cpp2inoWorkspaceEdit(workspaceEdit) + if uri.Ext() == ".ino" { + for i, diag := range inoCodeAction.Diagnostics { + _, inoCodeAction.Diagnostics[i].Range = handler.sketchMapper.CppToInoRange(diag.Range) } } + return inoCodeAction } -func (handler *InoHandler) cpp2inoWorkspaceEdit(origEdit *lsp.WorkspaceEdit) *lsp.WorkspaceEdit { - newEdit := lsp.WorkspaceEdit{Changes: make(map[string][]lsp.TextEdit)} - for uri, edit := range origEdit.Changes { - if data, ok := handler.data[lsp.DocumentURI(uri)]; ok { - newValue := make([]lsp.TextEdit, len(edit)) - for index := range edit { - r := edit[index].Range - newValue[index] = lsp.TextEdit{ - NewText: edit[index].NewText, - Range: lsp.Range{ - Start: lsp.Position{Line: data.sourceLineMap[r.Start.Line], Character: r.Start.Character}, - End: lsp.Position{Line: data.sourceLineMap[r.End.Line], Character: r.End.Character}, - }, +func (handler *InoHandler) Cpp2InoCommand(command *lsp.Command) *lsp.Command { + if command == nil { + return nil + } + inoCommand := &lsp.Command{ + Title: command.Title, + Command: command.Command, + Arguments: command.Arguments, + } + if command.Command == "clangd.applyTweak" { + for i := range command.Arguments { + v := struct { + TweakID string `json:"tweakID"` + File lsp.DocumentURI `json:"file"` + Selection lsp.Range `json:"selection"` + }{} + if err := json.Unmarshal(command.Arguments[0], &v); err == nil { + if v.TweakID == "ExtractVariable" { + log.Println(" > converted clangd ExtractVariable") + if v.File.AsPath().EquivalentTo(handler.buildSketchCpp) { + inoFile, inoSelection := handler.sketchMapper.CppToInoRange(v.Selection) + v.File = lsp.NewDocumentURI(inoFile) + v.Selection = inoSelection + } } } - newEdit.Changes[string(data.sourceURI)] = newValue - } else { - newEdit.Changes[uri] = edit + + converted, err := json.Marshal(v) + if err != nil { + panic("Internal Error: json conversion of codeAcion command arguments") + } + inoCommand.Arguments[i] = converted } } - return &newEdit + return inoCommand } -func (handler *InoHandler) cpp2inoHover(hover *Hover, uri lsp.DocumentURI) { - if data, ok := handler.data[uri]; ok { - r := hover.Range - if r != nil { - r.Start.Line = data.sourceLineMap[r.Start.Line] - r.End.Line = data.sourceLineMap[r.End.Line] +func (handler *InoHandler) cpp2inoWorkspaceEdit(origWorkspaceEdit *lsp.WorkspaceEdit) *lsp.WorkspaceEdit { + if origWorkspaceEdit == nil { + return nil + } + resWorkspaceEdit := &lsp.WorkspaceEdit{ + Changes: map[lsp.DocumentURI][]lsp.TextEdit{}, + } + for editURI, edits := range origWorkspaceEdit.Changes { + // if the edits are not relative to sketch file... + if !editURI.AsPath().EquivalentTo(handler.buildSketchCpp) { + // ...pass them through... + resWorkspaceEdit.Changes[editURI] = edits + continue + } + + // ...otherwise convert edits to the sketch.ino.cpp into multilpe .ino edits + for _, edit := range edits { + cppRange := edit.Range + inoFile, inoRange := handler.sketchMapper.CppToInoRange(cppRange) + inoURI := lsp.NewDocumentURI(inoFile) + if _, have := resWorkspaceEdit.Changes[inoURI]; !have { + resWorkspaceEdit.Changes[inoURI] = []lsp.TextEdit{} + } + resWorkspaceEdit.Changes[inoURI] = append(resWorkspaceEdit.Changes[inoURI], lsp.TextEdit{ + NewText: edit.NewText, + Range: inoRange, + }) } } + return resWorkspaceEdit } func (handler *InoHandler) cpp2inoLocation(location *lsp.Location) { - if data, ok := handler.data[location.URI]; ok { - location.URI = data.sourceURI - location.Range.Start.Line = data.sourceLineMap[location.Range.Start.Line] - location.Range.End.Line = data.sourceLineMap[location.Range.End.Line] - } + panic("not implemented") + // if data, ok := handler.data[location.URI]; ok { + // location.URI = data.sourceURI + // _, location.Range = data.sourceMap.CppToInoRange(location.Range) + // } } func (handler *InoHandler) cpp2inoDocumentHighlight(highlight *lsp.DocumentHighlight, uri lsp.DocumentURI) { - if data, ok := handler.data[uri]; ok { - highlight.Range.Start.Line = data.sourceLineMap[highlight.Range.Start.Line] - highlight.Range.End.Line = data.sourceLineMap[highlight.Range.End.Line] - } + panic("not implemented") + // if data, ok := handler.data[uri]; ok { + // _, highlight.Range = data.sourceMap.CppToInoRange(highlight.Range) + // } } func (handler *InoHandler) cpp2inoTextEdit(edit *lsp.TextEdit, uri lsp.DocumentURI) { - if data, ok := handler.data[uri]; ok { - edit.Range.Start.Line = data.sourceLineMap[edit.Range.Start.Line] - edit.Range.End.Line = data.sourceLineMap[edit.Range.End.Line] - } + panic("not implemented") + // if data, ok := handler.data[uri]; ok { + // _, edit.Range = data.sourceMap.CppToInoRange(edit.Range) + // } } -func (handler *InoHandler) cpp2inoDocumentSymbols(origSymbols []DocumentSymbol, uri lsp.DocumentURI) []DocumentSymbol { - data, ok := handler.data[uri] - if !ok || len(origSymbols) == 0 { +func (handler *InoHandler) cpp2inoDocumentSymbols(origSymbols []lsp.DocumentSymbol, origURI lsp.DocumentURI) []lsp.DocumentSymbol { + if origURI.Ext() != ".ino" || len(origSymbols) == 0 { return origSymbols } - symbolIdx := make(map[string]*DocumentSymbol) - for i := 0; i < len(origSymbols); i++ { - symbol := &origSymbols[i] - symbol.Range.Start.Line = data.sourceLineMap[symbol.Range.Start.Line] - symbol.Range.End.Line = data.sourceLineMap[symbol.Range.End.Line] + inoSymbols := []lsp.DocumentSymbol{} + for _, symbol := range origSymbols { + if handler.sketchMapper.IsPreprocessedCppLine(symbol.Range.Start.Line) { + continue + } + + inoFile, inoRange := handler.sketchMapper.CppToInoRange(symbol.Range) + inoSelectionURI, inoSelectionRange := handler.sketchMapper.CppToInoRange(symbol.SelectionRange) - duplicate := false - other, duplicate := symbolIdx[symbol.Name] - if duplicate { - // We prefer symbols later in the file due to the function header generation. E.g. if one has a function `void foo() {}` somehwre in the code - // the code generation will add a `void foo();` header at the beginning of the cpp file. We care about the function body later in the file, not - // the header early on. - if other.Range.Start.Line < symbol.Range.Start.Line { - continue - } + if inoFile != inoSelectionURI { + log.Printf(" ERROR: symbol range and selection belongs to different URI!") + log.Printf(" > %s != %s", symbol.Range, symbol.SelectionRange) + log.Printf(" > %s:%s != %s:%s", inoFile, inoRange, inoSelectionURI, inoSelectionRange) + continue } - symbol.SelectionRange.Start.Line = data.sourceLineMap[symbol.SelectionRange.Start.Line] - symbol.SelectionRange.End.Line = data.sourceLineMap[symbol.SelectionRange.End.Line] - symbol.Children = handler.cpp2inoDocumentSymbols(symbol.Children, uri) - symbolIdx[symbol.Name] = symbol - } + if inoFile != origURI.Unbox() { + //log.Printf(" skipping symbol related to %s", inoFile) + continue + } - newSymbols := make([]DocumentSymbol, len(symbolIdx)) - j := 0 - for _, s := range symbolIdx { - newSymbols[j] = *s - j++ + inoSymbols = append(inoSymbols, lsp.DocumentSymbol{ + Name: symbol.Name, + Detail: symbol.Detail, + Deprecated: symbol.Deprecated, + Kind: symbol.Kind, + Range: inoRange, + SelectionRange: inoSelectionRange, + Children: handler.cpp2inoDocumentSymbols(symbol.Children, origURI), + }) } - return newSymbols + + return inoSymbols } -func (handler *InoHandler) cpp2inoSymbolInformation(syms []*lsp.SymbolInformation) []lsp.SymbolInformation { - // Much like in cpp2inoDocumentSymbols we de-duplicate symbols based on file in-file location. - idx := make(map[string]*lsp.SymbolInformation) - for _, sym := range syms { - handler.cpp2inoLocation(&sym.Location) +func (handler *InoHandler) cpp2inoSymbolInformation(syms []lsp.SymbolInformation) []lsp.SymbolInformation { + panic("not implemented") + // // Much like in cpp2inoDocumentSymbols we de-duplicate symbols based on file in-file location. + // idx := make(map[string]*lsp.SymbolInformation) + // for _, sym := range syms { + // handler.cpp2inoLocation(&sym.Location) - nme := fmt.Sprintf("%s::%s", sym.ContainerName, sym.Name) - other, duplicate := idx[nme] - if duplicate && other.Location.Range.Start.Line < sym.Location.Range.Start.Line { - continue + // nme := fmt.Sprintf("%s::%s", sym.ContainerName, sym.Name) + // other, duplicate := idx[nme] + // if duplicate && other.Location.Range.Start.Line < sym.Location.Range.Start.Line { + // continue + // } + + // idx[nme] = sym + // } + + // var j int + // symbols := make([]lsp.SymbolInformation, len(idx)) + // for _, sym := range idx { + // symbols[j] = *sym + // j++ + // } + // return symbols +} + +func (handler *InoHandler) cpp2inoDiagnostics(cppDiags *lsp.PublishDiagnosticsParams) ([]*lsp.PublishDiagnosticsParams, error) { + + if len(cppDiags.Diagnostics) == 0 { + // If we receive the empty diagnostic on the preprocessed sketch, + // just return an empty diagnostic array. + if cppDiags.URI.AsPath().EquivalentTo(handler.buildSketchCpp) { + return []*lsp.PublishDiagnosticsParams{}, nil } - idx[nme] = sym + inoURI, _, err := handler.cpp2inoDocumentURI(cppDiags.URI, lsp.Range{}) + return []*lsp.PublishDiagnosticsParams{ + { + URI: inoURI, + Diagnostics: []lsp.Diagnostic{}, + }, + }, err } - var j int - symbols := make([]lsp.SymbolInformation, len(idx)) - for _, sym := range idx { - symbols[j] = *sym - j++ + convertedDiagnostics := map[lsp.DocumentURI]*lsp.PublishDiagnosticsParams{} + for _, cppDiag := range cppDiags.Diagnostics { + inoURI, inoRange, err := handler.cpp2inoDocumentURI(cppDiags.URI, cppDiag.Range) + if err != nil { + return nil, err + } + + inoDiagParam, created := convertedDiagnostics[inoURI] + if !created { + inoDiagParam = &lsp.PublishDiagnosticsParams{ + URI: inoURI, + Diagnostics: []lsp.Diagnostic{}, + } + convertedDiagnostics[inoURI] = inoDiagParam + } + + inoDiag := cppDiag + inoDiag.Range = inoRange + inoDiagParam.Diagnostics = append(inoDiagParam.Diagnostics, inoDiag) + } + + inoDiagParams := []*lsp.PublishDiagnosticsParams{} + for _, v := range convertedDiagnostics { + inoDiagParams = append(inoDiagParams, v) } - return symbols + return inoDiagParams, nil } // FromClangd handles a message received from clangd. func (handler *InoHandler) FromClangd(ctx context.Context, connection *jsonrpc2.Conn, req *jsonrpc2.Request) (interface{}, error) { - params, _, err := handler.transformParamsToStdio(req.Method, req.Params) + defer streams.CatchAndLogPanic() + + handler.synchronizer.DataMux.RLock() + defer handler.synchronizer.DataMux.RUnlock() + + params, err := lsp.ReadParams(req.Method, req.Params) + if err != nil { + return nil, errors.WithMessage(err, "parsing JSON message from clangd") + } + if params == nil { + // passthrough + params = req.Params + } + switch p := params.(type) { + case *lsp.PublishDiagnosticsParams: + // "textDocument/publishDiagnostics" + log.Printf(" <-- publishDiagnostics(%s):", p.URI) + for _, diag := range p.Diagnostics { + log.Printf(" > %d:%d - %v: %s", diag.Range.Start.Line, diag.Range.Start.Character, diag.Severity, diag.Code) + } + + // the diagnostics on sketch.cpp.ino once mapped into their + // .ino counter parts may span over multiple .ino files... + inoDiagnostics, err := handler.cpp2inoDiagnostics(p) + if err != nil { + return nil, err + } + cleanUpInoDiagnostics := false + if len(inoDiagnostics) == 0 { + cleanUpInoDiagnostics = true + } + + // Push back to IDE the converted diagnostics + inoDocsWithDiagnostics := map[lsp.DocumentURI]bool{} + for _, inoDiag := range inoDiagnostics { + if enableLogging { + log.Printf("<-- publishDiagnostics(%s):", inoDiag.URI) + for _, diag := range inoDiag.Diagnostics { + log.Printf(" > %d:%d - %v: %s", diag.Range.Start.Line, diag.Range.Start.Character, diag.Severity, diag.Code) + } + } + + // If we have an "undefined reference" in the .ino code trigger a + // check for newly created symbols (that in turn may trigger a + // new arduino-preprocessing of the sketch). + if inoDiag.URI.Ext() == ".ino" { + inoDocsWithDiagnostics[inoDiag.URI] = true + cleanUpInoDiagnostics = true + for _, diag := range inoDiag.Diagnostics { + if diag.Code == "undeclared_var_use_suggest" { + handler.buildSketchSymbolsCheck = true + } + } + } + + if err := handler.StdioConn.Notify(ctx, "textDocument/publishDiagnostics", inoDiag); err != nil { + return nil, err + } + } + + if cleanUpInoDiagnostics { + // Remove diagnostics from all .ino where there are no errors coming from clang + for sourceURI := range handler.inoDocsWithDiagnostics { + if inoDocsWithDiagnostics[sourceURI] { + // skip if we already sent updated diagnostics + continue + } + // otherwise clear previous diagnostics + msg := lsp.PublishDiagnosticsParams{ + URI: sourceURI, + Diagnostics: []lsp.Diagnostic{}, + } + if enableLogging { + log.Printf("<-- publishDiagnostics(%s):", msg.URI) + } + if err := handler.StdioConn.Notify(ctx, "textDocument/publishDiagnostics", msg); err != nil { + return nil, err + } + } + + handler.inoDocsWithDiagnostics = inoDocsWithDiagnostics + } + return nil, err + + case *lsp.ApplyWorkspaceEditParams: + // "workspace/applyEdit" + p.Edit = *handler.cpp2inoWorkspaceEdit(&p.Edit) + } + if err != nil { log.Println("From clangd: Method:", req.Method, "Error:", err) return nil, err @@ -761,7 +1233,7 @@ func (handler *InoHandler) FromClangd(ctx context.Context, connection *jsonrpc2. log.Println("From clangd:", req.Method) } } else { - result, err = sendRequest(ctx, handler.StdioConn, req.Method, params) + result, err = lsp.SendRequest(ctx, handler.StdioConn, req.Method, params) if enableLogging { log.Println("From clangd:", req.Method, "id", req.ID) } @@ -769,78 +1241,16 @@ func (handler *InoHandler) FromClangd(ctx context.Context, connection *jsonrpc2. return result, err } -func (handler *InoHandler) transformParamsToStdio(method string, raw *json.RawMessage) (params interface{}, uri lsp.DocumentURI, err error) { - handler.synchronizer.DataMux.RLock() - defer handler.synchronizer.DataMux.RUnlock() - - params, err = readParams(method, raw) - if err != nil { - return - } else if params == nil { - params = raw - return - } - switch method { - case "textDocument/publishDiagnostics": - p := params.(*lsp.PublishDiagnosticsParams) - uri = p.URI - err = handler.cpp2inoPublishDiagnosticsParams(p) - case "workspace/applyEdit": - p := params.(*ApplyWorkspaceEditParams) - p.Edit = *handler.cpp2inoWorkspaceEdit(&p.Edit) - } - return -} - -func (handler *InoHandler) cpp2inoPublishDiagnosticsParams(params *lsp.PublishDiagnosticsParams) error { - if data, ok := handler.data[params.URI]; ok { - params.URI = data.sourceURI - newDiagnostics := make([]lsp.Diagnostic, 0, len(params.Diagnostics)) - for index := range params.Diagnostics { - r := ¶ms.Diagnostics[index].Range - if startLine, ok := data.sourceLineMap[r.Start.Line]; ok { - r.Start.Line = startLine - r.End.Line = data.sourceLineMap[r.End.Line] - newDiagnostics = append(newDiagnostics, params.Diagnostics[index]) - } - } - params.Diagnostics = newDiagnostics - } - return nil -} - -func (handler *InoHandler) parseCommandArgument(rawArg interface{}) interface{} { - if m1, ok := rawArg.(map[string]interface{}); ok && len(m1) == 1 && m1["changes"] != nil { - m2 := m1["changes"].(map[string]interface{}) - workspaceEdit := lsp.WorkspaceEdit{Changes: make(map[string][]lsp.TextEdit)} - for uri, rawValue := range m2 { - rawTextEdits := rawValue.([]interface{}) - textEdits := make([]lsp.TextEdit, len(rawTextEdits)) - for index := range rawTextEdits { - m3 := rawTextEdits[index].(map[string]interface{}) - rawRange := m3["range"] - m4 := rawRange.(map[string]interface{}) - rawStart := m4["start"] - m5 := rawStart.(map[string]interface{}) - textEdits[index].Range.Start.Line = int(m5["line"].(float64)) - textEdits[index].Range.Start.Character = int(m5["character"].(float64)) - rawEnd := m4["end"] - m6 := rawEnd.(map[string]interface{}) - textEdits[index].Range.End.Line = int(m6["line"].(float64)) - textEdits[index].Range.End.Character = int(m6["character"].(float64)) - textEdits[index].NewText = m3["newText"].(string) - } - workspaceEdit.Changes[uri] = textEdits - } - return &workspaceEdit - } - return nil -} - func (handler *InoHandler) showMessage(ctx context.Context, msgType lsp.MessageType, message string) { + defer streams.CatchAndLogPanic() + params := lsp.ShowMessageParams{ Type: msgType, Message: message, } handler.StdioConn.Notify(ctx, "window/showMessage", ¶ms) } + +func unknownURI(uri lsp.DocumentURI) error { + return errors.New("Document is not available: " + string(uri)) +} diff --git a/handler/properties.go b/handler/properties.go deleted file mode 100644 index b30de0b..0000000 --- a/handler/properties.go +++ /dev/null @@ -1,41 +0,0 @@ -package handler - -import ( - "bufio" - "io" - "strings" -) - -func readProperties(propsFile io.Reader) (map[string]string, error) { - properties := make(map[string]string) - scanner := bufio.NewScanner(propsFile) - for scanner.Scan() { - line := scanner.Text() - equalIndex := strings.Index(line, "=") - if equalIndex >= 0 { - key := strings.TrimSpace(line[:equalIndex]) - if len(key) > 0 { - value := strings.TrimSpace(line[equalIndex+1:]) - properties[key] = value - } - } - } - return properties, scanner.Err() -} - -func expandProperty(properties map[string]string, name string) string { - value := properties[name] - varStart := strings.Index(value, "{") - for varStart >= 0 { - varEnd := strings.Index(value[varStart:], "}") - if varEnd >= 0 { - referencedName := value[varStart+1 : varStart+varEnd] - expanded := expandProperty(properties, referencedName) - value = value[:varStart] + expanded + value[varStart+varEnd+1:] - varStart = strings.Index(value, "{") - } else { - varStart = -1 - } - } - return value -} diff --git a/handler/properties_test.go b/handler/properties_test.go deleted file mode 100644 index 845ddba..0000000 --- a/handler/properties_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package handler - -import ( - "reflect" - "strings" - "testing" -) - -func TestReadProperties(t *testing.T) { - properties, err := readProperties(strings.NewReader("foo=Hello\n bar = World \nbaz=!")) - if err != nil { - t.Error(err) - } - if !reflect.DeepEqual(properties, map[string]string{ - "foo": "Hello", - "bar": "World", - "baz": "!", - }) { - t.Error(properties) - } -} - -func TestExpandProperty(t *testing.T) { - properties := map[string]string{ - "foo": "Hello {bar} {baz}", - "bar": "{baz} World", - "baz": "!", - } - foo := expandProperty(properties, "foo") - if foo != "Hello ! World !" { - t.Error(foo) - } -} diff --git a/handler/sourcemap.go b/handler/sourcemap.go deleted file mode 100644 index 41840a4..0000000 --- a/handler/sourcemap.go +++ /dev/null @@ -1,204 +0,0 @@ -package handler - -import ( - "bufio" - "fmt" - "io" - "strconv" - "strings" - - lsp "github.com/sourcegraph/go-lsp" -) - -func createSourceMaps(targetFile io.Reader) (sourceLineMap, targetLineMap map[int]int) { - sourceLine := -1 - targetLine := 0 - sourceLineMap = make(map[int]int) - targetLineMap = make(map[int]int) - scanner := bufio.NewScanner(targetFile) - for scanner.Scan() { - lineStr := scanner.Text() - if strings.HasPrefix(lineStr, "#line") { - nrEnd := strings.Index(lineStr[6:], " ") - var l int - var err error - if nrEnd > 0 { - l, err = strconv.Atoi(lineStr[6 : nrEnd+6]) - } else { - l, err = strconv.Atoi(lineStr[6:]) - } - if err == nil && l > 0 { - sourceLine = l - 1 - } - } else if sourceLine >= 0 { - sourceLineMap[targetLine] = sourceLine - targetLineMap[sourceLine] = targetLine - sourceLine++ - } - targetLine++ - } - sourceLineMap[targetLine] = sourceLine - targetLineMap[sourceLine] = targetLine - return -} - -func updateSourceMaps(sourceLineMap, targetLineMap map[int]int, deletedLines, insertLine int, insertText string) { - for i := 1; i <= deletedLines; i++ { - sourceLine := insertLine + 1 - targetLine := targetLineMap[sourceLine] - - // Shift up all following lines by one and put them into a new map - newMappings := make(map[int]int) - maxSourceLine, maxTargetLine := 0, 0 - for t, s := range sourceLineMap { - if t > targetLine && s > sourceLine { - newMappings[t-1] = s - 1 - } else if s > sourceLine { - newMappings[t] = s - 1 - } else if t > targetLine { - newMappings[t-1] = s - } - if s > maxSourceLine { - maxSourceLine = s - } - if t > maxTargetLine { - maxTargetLine = t - } - } - - // Remove mappings for the deleted line - delete(sourceLineMap, maxTargetLine) - delete(targetLineMap, maxSourceLine) - - // Copy the mappings from the intermediate map - copyMappings(sourceLineMap, targetLineMap, newMappings) - } - - addedLines := strings.Count(insertText, "\n") - if addedLines > 0 { - targetLine := targetLineMap[insertLine] - - // Shift down all following lines and put them into a new map - newMappings := make(map[int]int) - for t, s := range sourceLineMap { - if t > targetLine && s > insertLine { - newMappings[t+addedLines] = s + addedLines - } else if s > insertLine { - newMappings[t] = s + addedLines - } else if t > targetLine { - newMappings[t+addedLines] = s - } - } - - // Add mappings for the added lines - for i := 1; i <= addedLines; i++ { - sourceLineMap[targetLine+i] = insertLine + i - targetLineMap[insertLine+i] = targetLine + i - } - - // Copy the mappings from the intermediate map - copyMappings(sourceLineMap, targetLineMap, newMappings) - } -} - -func copyMappings(sourceLineMap, targetLineMap, newMappings map[int]int) { - for t, s := range newMappings { - sourceLineMap[t] = s - targetLineMap[s] = t - } - for t, s := range newMappings { - // In case multiple target lines are present for a source line, use the last one - if t > targetLineMap[s] { - targetLineMap[s] = t - } - } -} - -// OutOfRangeError returned if one attempts to access text out of its range -type OutOfRangeError struct { - Type string - Max int - Req int -} - -func (oor OutOfRangeError) Error() string { - return fmt.Sprintf("%s access out of range: max=%d requested=%d", oor.Type, oor.Max, oor.Req) -} - -func applyTextChange(text string, rang lsp.Range, insertText string) (res string, err error) { - start, err := getOffset(text, rang.Start) - if err != nil { - return "", err - } - end, err := getOffset(text, rang.End) - if err != nil { - return "", err - } - - return text[:start] + insertText + text[end:], nil -} - -// getOffset computes the offset in the text expressed by the lsp.Position. -// Returns OutOfRangeError if the position is out of range. -func getOffset(text string, pos lsp.Position) (int, error) { - // Find line - lineOffset, err := getLineOffset(text, pos.Line) - if err != nil { - return -1, err - } - character := pos.Character - if character == 0 { - return lineOffset, nil - } - - // Find the character and return its offset within the text - var count = len(text[lineOffset:]) - for offset, c := range text[lineOffset:] { - if character == offset { - // We've found the character - return lineOffset + offset, nil - } - if c == '\n' { - // We've reached the end of line. LSP spec says we should default back to the line length. - // See https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#position - if character > offset { - return lineOffset + offset, nil - } - count = offset - break - } - } - if character > 0 { - // We've reached the end of the last line. Default to the text length (see above). - return len(text), nil - } - - // We haven't found the character in the text (character index was negative) - return -1, OutOfRangeError{"Character", count, character} -} - -// getLineOffset finds the offset/position of the beginning of a line within the text. -// For example: -// text := "foo\nfoobar\nbaz" -// getLineOffset(text, 0) == 0 -// getLineOffset(text, 1) == 4 -// getLineOffset(text, 2) == 11 -func getLineOffset(text string, line int) (int, error) { - if line == 0 { - return 0, nil - } - - // Find the line and return its offset within the text - var count int - for offset, c := range text { - if c == '\n' { - count++ - if count == line { - return offset + 1, nil - } - } - } - - // We haven't found the line in the text - return -1, OutOfRangeError{"Line", count, line} -} diff --git a/handler/sourcemapper/ino.go b/handler/sourcemapper/ino.go new file mode 100644 index 0000000..a921b40 --- /dev/null +++ b/handler/sourcemapper/ino.go @@ -0,0 +1,321 @@ +package sourcemapper + +import ( + "bufio" + "bytes" + "fmt" + "sort" + "strconv" + "strings" + + "github.com/bcmi-labs/arduino-language-server/handler/textutils" + "github.com/bcmi-labs/arduino-language-server/lsp" +) + +// InoMapper is a mapping between the .ino sketch and the preprocessed .cpp file +type InoMapper struct { + InoText map[lsp.DocumentURI]*SourceRevision + CppText *SourceRevision + toCpp map[InoLine]int // Converts File.ino:line -> line + toIno map[int]InoLine // Convers line -> File.ino:line + inoPreprocessed map[InoLine]int // map of the lines taken by the preprocessor: File.ino:line -> preprocessed line + cppPreprocessed map[int]InoLine // map of the lines added by the preprocessor: preprocessed line -> File.ino:line +} + +// NotIno are lines that do not belongs to an .ino file +var NotIno = InoLine{"not-ino", 0} + +type SourceRevision struct { + Version int + Text string +} + +// InoLine is a line number into an .ino file +type InoLine struct { + File string + Line int +} + +// InoToCppLine converts a source (.ino) line into a target (.cpp) line +func (s *InoMapper) InoToCppLine(sourceURI lsp.DocumentURI, line int) int { + return s.toCpp[InoLine{sourceURI.Unbox(), line}] +} + +// InoToCppLineOk converts a source (.ino) line into a target (.cpp) line +func (s *InoMapper) InoToCppLineOk(sourceURI lsp.DocumentURI, line int) (int, bool) { + res, ok := s.toCpp[InoLine{sourceURI.Unbox(), line}] + return res, ok +} + +// InoToCppLSPRange convert a lsp.Ranger reference to a .ino into a lsp.Range to .cpp +func (s *InoMapper) InoToCppLSPRange(sourceURI lsp.DocumentURI, r lsp.Range) lsp.Range { + res := r + res.Start.Line = s.InoToCppLine(sourceURI, r.Start.Line) + res.End.Line = s.InoToCppLine(sourceURI, r.End.Line) + return res +} + +// InoToCppLSPRangeOk convert a lsp.Ranger reference to a .ino into a lsp.Range to .cpp and returns +// true if the conversion is successful or false if the conversion is invalid. +func (s *InoMapper) InoToCppLSPRangeOk(sourceURI lsp.DocumentURI, r lsp.Range) (lsp.Range, bool) { + res := r + if l, ok := s.InoToCppLineOk(sourceURI, r.Start.Line); ok { + res.Start.Line = l + } else { + return res, false + } + if l, ok := s.InoToCppLineOk(sourceURI, r.End.Line); ok { + res.End.Line = l + } else { + return res, false + } + return res, true +} + +// CppToInoLine converts a target (.cpp) line into a source.ino:line +func (s *InoMapper) CppToInoLine(targetLine int) (string, int) { + res := s.toIno[targetLine] + return res.File, res.Line +} + +// CppToInoRange converts a target (.cpp) lsp.Range into a source.ino:lsp.Range +func (s *InoMapper) CppToInoRange(r lsp.Range) (string, lsp.Range) { + startFile, startLine := s.CppToInoLine(r.Start.Line) + endFile, endLine := s.CppToInoLine(r.End.Line) + res := r + res.Start.Line = startLine + res.End.Line = endLine + if startFile != endFile { + panic("invalid range conversion") + } + return startFile, res +} + +// CppToInoLineOk converts a target (.cpp) line into a source (.ino) line and +// returns true if the conversion is successful +func (s *InoMapper) CppToInoLineOk(targetLine int) (string, int, bool) { + res, ok := s.toIno[targetLine] + return res.File, res.Line, ok +} + +// IsPreprocessedCppLine returns true if the give .cpp line is part of the +// section added by the arduino preprocessor. +func (s *InoMapper) IsPreprocessedCppLine(cppLine int) bool { + _, preprocessed := s.cppPreprocessed[cppLine] + _, mapsToIno := s.toIno[cppLine] + return preprocessed || !mapsToIno +} + +// CreateInoMapper create a InoMapper from the given target file +func CreateInoMapper(targetFile []byte) *InoMapper { + mapper := &InoMapper{ + toCpp: map[InoLine]int{}, + toIno: map[int]InoLine{}, + inoPreprocessed: map[InoLine]int{}, + cppPreprocessed: map[int]InoLine{}, + CppText: &SourceRevision{ + Version: 1, + Text: string(targetFile), + }, + } + + sourceFile := "" + sourceLine := -1 + targetLine := 0 + scanner := bufio.NewScanner(bytes.NewReader(targetFile)) + for scanner.Scan() { + lineStr := scanner.Text() + if strings.HasPrefix(lineStr, "#line") { + tokens := strings.SplitN(lineStr, " ", 3) + l, err := strconv.Atoi(tokens[1]) + if err == nil && l > 0 { + sourceLine = l - 1 + } + sourceFile = unquoteCppString(tokens[2]) + mapper.toIno[targetLine] = NotIno + } else if sourceFile != "" { + mapper.mapLine(sourceFile, sourceLine, targetLine) + sourceLine++ + } else { + mapper.toIno[targetLine] = NotIno + } + targetLine++ + } + mapper.mapLine(sourceFile, sourceLine, targetLine) + return mapper +} + +func (s *InoMapper) mapLine(sourceFile string, sourceLine, targetLine int) { + inoLine := InoLine{sourceFile, sourceLine} + if line, ok := s.toCpp[inoLine]; ok { + s.cppPreprocessed[line] = inoLine + s.inoPreprocessed[inoLine] = line + } + s.toCpp[inoLine] = targetLine + s.toIno[targetLine] = inoLine +} + +func unquoteCppString(str string) string { + if len(str) >= 2 && strings.HasPrefix(str, `"`) && strings.HasSuffix(str, `"`) { + str = strings.TrimSuffix(str, `"`)[1:] + } + str = strings.Replace(str, "\\\"", "\"", -1) + str = strings.Replace(str, "\\\\", "\\", -1) + return str +} + +// ApplyTextChange performs the text change and updates both .ino and .cpp files. +// It returns true if the change is "dirty", this happens when the change alters preprocessed lines +// and a new preprocessing may be probably required. +func (s *InoMapper) ApplyTextChange(inoURI lsp.DocumentURI, inoChange lsp.TextDocumentContentChangeEvent) (dirty bool) { + inoRange := *inoChange.Range + cppRange := s.InoToCppLSPRange(inoURI, inoRange) + deletedLines := inoRange.End.Line - inoRange.Start.Line + + // Apply text changes + newText, err := textutils.ApplyTextChange(s.CppText.Text, cppRange, inoChange.Text) + if err != nil { + panic("error replacing text: " + err.Error()) + } + s.CppText.Text = newText + s.CppText.Version++ + + if _, is := s.inoPreprocessed[s.toIno[cppRange.Start.Line]]; is { + dirty = true + } + + // Update line references + for deletedLines > 0 { + dirty = dirty || s.deleteCppLine(cppRange.Start.Line) + deletedLines-- + } + addedLines := strings.Count(inoChange.Text, "\n") - 1 + for addedLines > 0 { + dirty = dirty || s.addInoLine(cppRange.Start.Line) + addedLines-- + } + return +} + +func (s *InoMapper) addInoLine(cppLine int) (dirty bool) { + preprocessToShiftCpp := map[InoLine]bool{} + + addedInoLine := s.toIno[cppLine] + carry := s.toIno[cppLine] + carry.Line++ + for { + next, ok := s.toIno[cppLine+1] + s.toIno[cppLine+1] = carry + s.toCpp[carry] = cppLine + 1 + if !ok { + break + } + + if next.File == addedInoLine.File && next.Line >= addedInoLine.Line { + if _, is := s.inoPreprocessed[next]; is { + // fmt.Println("Adding", next, "to cpp to shift") + preprocessToShiftCpp[next] = true + } + next.Line++ + } + + carry = next + cppLine++ + } + + // dumpCppToInoMap(s.toIno) + + preprocessToShiftIno := []InoLine{} + for inoPre := range s.inoPreprocessed { + // fmt.Println(">", inoPre, addedInoLine) + if inoPre.File == addedInoLine.File && inoPre.Line >= addedInoLine.Line { + preprocessToShiftIno = append(preprocessToShiftIno, inoPre) + } + } + for inoPre := range preprocessToShiftCpp { + l := s.inoPreprocessed[inoPre] + delete(s.cppPreprocessed, l) + s.inoPreprocessed[inoPre] = l + 1 + s.cppPreprocessed[l+1] = inoPre + } + for _, inoPre := range preprocessToShiftIno { + l := s.inoPreprocessed[inoPre] + delete(s.inoPreprocessed, inoPre) + inoPre.Line++ + s.inoPreprocessed[inoPre] = l + s.cppPreprocessed[l] = inoPre + s.toIno[l] = inoPre + } + + return +} + +func (s *InoMapper) deleteCppLine(line int) (dirty bool) { + removed := s.toIno[line] + for i := line + 1; ; i++ { + shifted, ok := s.toIno[i] + if !ok { + delete(s.toIno, i-1) + break + } + s.toIno[i-1] = shifted + if shifted != NotIno { + s.toCpp[shifted] = i - 1 + } + } + + if _, ok := s.inoPreprocessed[removed]; ok { + dirty = true + } + + for curr := removed; ; curr.Line++ { + next := curr + next.Line++ + + shifted, ok := s.toCpp[next] + if !ok { + delete(s.toCpp, curr) + break + } + s.toCpp[curr] = shifted + s.toIno[shifted] = curr + + if l, ok := s.inoPreprocessed[next]; ok { + s.inoPreprocessed[curr] = l + s.cppPreprocessed[l] = curr + delete(s.inoPreprocessed, next) + + s.toIno[l] = curr + } + } + return +} + +func dumpCppToInoMap(s map[int]InoLine) { + last := 0 + for cppLine := range s { + if last < cppLine { + last = cppLine + } + } + for line := 0; line <= last; line++ { + target := s[line] + fmt.Printf("%5d -> %s:%d\n", line, target.File, target.Line) + } +} + +func dumpInoToCppMap(s map[InoLine]int) { + keys := []InoLine{} + for k := range s { + keys = append(keys, k) + } + sort.Slice(keys, func(i, j int) bool { + return keys[i].File < keys[j].File || + (keys[i].File == keys[j].File && keys[i].Line < keys[j].Line) + }) + for _, k := range keys { + inoLine := k + cppLine := s[inoLine] + fmt.Printf("%s:%d -> %d\n", inoLine.File, inoLine.Line, cppLine) + } +} diff --git a/handler/sourcemapper/ino_test.go b/handler/sourcemapper/ino_test.go new file mode 100644 index 0000000..d6cf23e --- /dev/null +++ b/handler/sourcemapper/ino_test.go @@ -0,0 +1,293 @@ +package sourcemapper + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCreateSourceMaps(t *testing.T) { + input := `#include +#line 1 "sketch_july2a.ino" +#line 1 "sketch_july2a.ino" + +#line 2 "sketch_july2a.ino" +void setup(); +#line 7 "sketch_july2a.ino" +void loop(); +#line 2 "sketch_july2a.ino" +void setup() { + // put your setup code here, to run once: + +} + +void loop() { + // put your main code here, to run repeatedly: + +} +` + sourceMap := CreateInoMapper([]byte(input)) + require.EqualValues(t, map[InoLine]int{ + {"sketch_july2a.ino", 0}: 3, + {"sketch_july2a.ino", 1}: 9, + {"sketch_july2a.ino", 2}: 10, + {"sketch_july2a.ino", 3}: 11, + {"sketch_july2a.ino", 4}: 12, + {"sketch_july2a.ino", 5}: 13, + {"sketch_july2a.ino", 6}: 14, + {"sketch_july2a.ino", 7}: 15, + {"sketch_july2a.ino", 8}: 16, + {"sketch_july2a.ino", 9}: 17, + {"sketch_july2a.ino", 10}: 18, + }, sourceMap.toCpp) + require.EqualValues(t, map[int]InoLine{ + 0: NotIno, + 1: NotIno, + 2: NotIno, + 3: {"sketch_july2a.ino", 0}, + 4: NotIno, + 5: {"sketch_july2a.ino", 1}, // setup + 6: NotIno, + 7: {"sketch_july2a.ino", 6}, // loop + 8: NotIno, + 9: {"sketch_july2a.ino", 1}, + 10: {"sketch_july2a.ino", 2}, + 11: {"sketch_july2a.ino", 3}, + 12: {"sketch_july2a.ino", 4}, + 13: {"sketch_july2a.ino", 5}, + 14: {"sketch_july2a.ino", 6}, + 15: {"sketch_july2a.ino", 7}, + 16: {"sketch_july2a.ino", 8}, + 17: {"sketch_july2a.ino", 9}, + 18: {"sketch_july2a.ino", 10}, + }, sourceMap.toIno) + require.EqualValues(t, map[int]InoLine{ + 5: {"sketch_july2a.ino", 1}, // setup + 7: {"sketch_july2a.ino", 6}, // loop + }, sourceMap.cppPreprocessed) + + dumpCppToInoMap(sourceMap.toIno) + dumpInoToCppMap(sourceMap.toCpp) + dumpCppToInoMap(sourceMap.cppPreprocessed) + dumpInoToCppMap(sourceMap.inoPreprocessed) + //sourceMap.addInoLine(InoLine{"sketch_july2a.ino", 0}) + sourceMap.addInoLine(3) + fmt.Println("\nAdded line 13") + dumpCppToInoMap(sourceMap.toIno) + dumpInoToCppMap(sourceMap.toCpp) + dumpCppToInoMap(sourceMap.cppPreprocessed) + dumpInoToCppMap(sourceMap.inoPreprocessed) +} + +func TestCreateMultifileSourceMap(t *testing.T) { + input := `#include +#line 1 "/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino" +#include +#include + +#line 4 "/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino" +void setup(); +#line 9 "/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino" +void loop(); +#line 23 "/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino" +void vino(); +#line 2 "/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/SecondTab.ino" +void secondFunction(); +#line 4 "/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino" +void setup() { + // put your setup code here, to run once: + digitalWrite(10, 20); +} + +void loop() { + // put your main code here, to run repeatedly: + long pippo = Serial.available(); + pippo++; + Serial1.write(pippo); + SPI.begin(); + int ciao = millis(); + Serial.println(ciao, HEX); + if (ciao > 10) { + SerialUSB.println(); + } + Serial.println(); +} + +void vino() { +} + +#line 1 "/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/SecondTab.ino" + +void secondFunction() { + +}` + sourceMap := CreateInoMapper([]byte(input)) + require.EqualValues(t, sourceMap.toCpp, map[InoLine]int{ + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 0}: 2, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 1}: 3, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 2}: 4, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 3}: 14, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 4}: 15, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 5}: 16, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 6}: 17, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 7}: 18, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 8}: 19, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 9}: 20, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 10}: 21, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 11}: 22, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 12}: 23, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 13}: 24, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 14}: 25, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 15}: 26, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 16}: 27, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 17}: 28, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 18}: 29, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 19}: 30, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 20}: 31, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 21}: 32, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 22}: 33, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 23}: 34, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 24}: 35, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/SecondTab.ino", 0}: 37, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/SecondTab.ino", 1}: 38, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/SecondTab.ino", 2}: 39, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/SecondTab.ino", 3}: 40, + {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/SecondTab.ino", 4}: 41, + }) + require.EqualValues(t, sourceMap.toIno, map[int]InoLine{ + 0: NotIno, + 1: NotIno, + 2: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 0}, + 3: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 1}, + 4: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 2}, + 5: NotIno, + 6: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 3}, // setup + 7: NotIno, + 8: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 8}, // loop + 9: NotIno, + 10: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 22}, // vino + 11: NotIno, + 12: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/SecondTab.ino", 1}, // secondFunction + 13: NotIno, + 14: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 3}, + 15: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 4}, + 16: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 5}, + 17: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 6}, + 18: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 7}, + 19: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 8}, + 20: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 9}, + 21: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 10}, + 22: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 11}, + 23: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 12}, + 24: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 13}, + 25: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 14}, + 26: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 15}, + 27: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 16}, + 28: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 17}, + 29: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 18}, + 30: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 19}, + 31: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 20}, + 32: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 21}, + 33: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 22}, + 34: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 23}, + 35: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 24}, + 36: {"not-ino", 0}, + 37: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/SecondTab.ino", 0}, + 38: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/SecondTab.ino", 1}, + 39: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/SecondTab.ino", 2}, + 40: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/SecondTab.ino", 3}, + 41: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/SecondTab.ino", 4}, + }) + require.EqualValues(t, map[int]InoLine{ + 6: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 3}, // setup + 8: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 8}, // loop + 10: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/Prova_Spazio.ino", 22}, // vino + 12: {"/home/megabug/Workspace/sketchbook-cores-beta/Prova_Spazio/SecondTab.ino", 1}, // secondFunction + }, sourceMap.cppPreprocessed) + dumpCppToInoMap(sourceMap.toIno) + dumpInoToCppMap(sourceMap.toCpp) + dumpCppToInoMap(sourceMap.cppPreprocessed) + dumpInoToCppMap(sourceMap.inoPreprocessed) + sourceMap.deleteCppLine(21) + fmt.Println("\nRemoved line 21") + dumpCppToInoMap(sourceMap.toIno) + dumpInoToCppMap(sourceMap.toCpp) + dumpCppToInoMap(sourceMap.cppPreprocessed) + dumpInoToCppMap(sourceMap.inoPreprocessed) +} + +// func TestUpdateSourceMaps1(t *testing.T) { +// sourceMap := &InoMapper{ +// toCpp: map[int]int{ +// 0: 1, +// 1: 2, +// 2: 0, +// 3: 5, +// 4: 3, +// 5: 4, +// }, +// toIno: make(map[int]int), +// } +// for s, t := range sourceMap.toCpp { +// sourceMap.toIno[t] = s +// } +// sourceMap.Update(0, 1, "foo\nbar\nbaz") +// if !reflect.DeepEqual(sourceMap.toCpp, map[int]int{ +// 0: 1, +// 1: 2, +// 2: 3, +// 3: 4, +// 4: 0, +// 5: 7, +// 6: 5, +// 7: 6}, +// ) { +// t.Error(sourceMap.toCpp) +// } +// if !reflect.DeepEqual(sourceMap.toIno, map[int]int{ +// 0: 4, +// 1: 0, +// 2: 1, +// 3: 2, +// 4: 3, +// 5: 6, +// 6: 7, +// 7: 5}, +// ) { +// t.Error(sourceMap.toIno) +// } +// } + +// func TestUpdateSourceMaps2(t *testing.T) { +// sourceMap := &InoMapper{ +// toCpp: map[int]int{ +// 0: 1, +// 1: 2, +// 2: 0, +// 3: 5, +// 4: 3, +// 5: 4}, +// toIno: make(map[int]int), +// } +// for s, t := range sourceMap.toCpp { +// sourceMap.toIno[t] = s +// } +// sourceMap.Update(2, 1, "foo") +// if !reflect.DeepEqual(sourceMap.toCpp, map[int]int{ +// 0: 0, +// 1: 1, +// 2: 2, +// 3: 3}, +// ) { +// t.Error(sourceMap.toCpp) +// } +// if !reflect.DeepEqual(sourceMap.toIno, map[int]int{ +// 0: 0, +// 1: 1, +// 2: 2, +// 3: 3}, +// ) { +// t.Error(sourceMap.toIno) +// } +// } diff --git a/handler/streamlog.go b/handler/streamlog.go deleted file mode 100644 index 7df9e99..0000000 --- a/handler/streamlog.go +++ /dev/null @@ -1,147 +0,0 @@ -package handler - -import ( - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "strings" -) - -// StreamLogger maintains log files for all streams involved in the language server -type StreamLogger struct { - Default io.WriteCloser - Stdin io.WriteCloser - Stdout io.WriteCloser - ClangdIn io.WriteCloser - ClangdOut io.WriteCloser - ClangdErr io.WriteCloser -} - -// Close closes all logging streams -func (s *StreamLogger) Close() (err error) { - var errs []string - for _, c := range []io.Closer{s.Default, s.Stdin, s.Stdout, s.ClangdIn, s.ClangdOut, s.ClangdErr} { - if c == nil { - continue - } - - err = c.Close() - if err != nil { - errs = append(errs, err.Error()) - } - } - if len(errs) != 0 { - return fmt.Errorf(strings.Join(errs, ", ")) - } - - return nil -} - -// AttachStdInOut attaches the stdin, stdout logger to the in/out channels -func (s *StreamLogger) AttachStdInOut(in io.ReadCloser, out io.WriteCloser) io.ReadWriteCloser { - return &streamDuplex{ - io.TeeReader(in, s.Stdin), - in, - io.MultiWriter(out, s.Stdout), - out, - } -} - -// AttachClangdInOut attaches the clangd in, out logger to the in/out channels -func (s *StreamLogger) AttachClangdInOut(in io.ReadCloser, out io.WriteCloser) io.ReadWriteCloser { - return &streamDuplex{ - io.TeeReader(in, s.ClangdIn), - in, - io.MultiWriter(out, s.ClangdOut), - out, - } -} - -type streamDuplex struct { - in io.Reader - inc io.Closer - out io.Writer - outc io.Closer -} - -func (sd *streamDuplex) Read(p []byte) (int, error) { - return sd.in.Read(p) -} - -func (sd *streamDuplex) Write(p []byte) (int, error) { - return sd.out.Write(p) -} - -func (sd *streamDuplex) Close() error { - ierr := sd.inc.Close() - oerr := sd.outc.Close() - - if ierr != nil { - return ierr - } - if oerr != nil { - return oerr - } - return nil -} - -// NewStreamLogger creates files for all stream logs. Returns an error if opening a single stream fails. -func NewStreamLogger(basepath string) (res *StreamLogger, err error) { - res = &StreamLogger{} - - res.Default, err = os.OpenFile(filepath.Join(basepath, "inols.log"), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) - if err != nil { - res.Close() - return - } - res.Stdin, err = os.OpenFile(filepath.Join(basepath, "inols-stdin.log"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) - if err != nil { - res.Close() - return - } - res.Stdout, err = os.OpenFile(filepath.Join(basepath, "inols-stdout.log"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) - if err != nil { - res.Close() - return - } - res.ClangdIn, err = os.OpenFile(filepath.Join(basepath, "inols-clangd-in.log"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) - if err != nil { - res.Close() - return - } - res.ClangdOut, err = os.OpenFile(filepath.Join(basepath, "inols-clangd-out.log"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) - if err != nil { - res.Close() - return - } - res.ClangdErr, err = os.OpenFile(filepath.Join(basepath, "inols-clangd-err.log"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) - if err != nil { - res.Close() - return - } - - return -} - -// NewNoopLogger creates a logger that does nothing -func NewNoopLogger() (res *StreamLogger) { - noop := noopCloser{ioutil.Discard} - return &StreamLogger{ - Default: noop, - Stdin: noop, - Stdout: noop, - ClangdIn: noop, - ClangdOut: noop, - ClangdErr: noop, - } -} - -type noopCloser struct { - io.Writer -} - -func (noopCloser) Close() error { - return nil -} diff --git a/handler/syncer.go b/handler/syncer.go index 2cc1b98..452d3c4 100644 --- a/handler/syncer.go +++ b/handler/syncer.go @@ -2,7 +2,6 @@ package handler import ( "context" - "log" "sync" "github.com/sourcegraph/jsonrpc2" @@ -27,26 +26,22 @@ type AsyncHandler struct { func (ah AsyncHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) { needsWriteLock := req.Method == "textDocument/didOpen" || req.Method == "textDocument/didChange" if needsWriteLock { - ah.synchronizer.FileMux.Lock() - if enableLogging { - log.Println("Message processing locked for", req.Method) - } - go ah.runWrite(ctx, conn, req) + go func() { + ah.synchronizer.FileMux.Lock() + defer ah.synchronizer.FileMux.Unlock() + if enableLogging { + // log.Println("Message processing locked for", req.Method) + } + ah.handler.Handle(ctx, conn, req) + if enableLogging { + // log.Println("Message processing unlocked for", req.Method) + } + }() } else { - ah.synchronizer.FileMux.RLock() - go ah.runRead(ctx, conn, req) - } -} - -func (ah AsyncHandler) runRead(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) { - defer ah.synchronizer.FileMux.RUnlock() - ah.handler.Handle(ctx, conn, req) -} - -func (ah AsyncHandler) runWrite(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) { - defer ah.synchronizer.FileMux.Unlock() - ah.handler.Handle(ctx, conn, req) - if enableLogging { - log.Println("Message processing unlocked for", req.Method) + go func() { + ah.synchronizer.FileMux.RLock() + ah.handler.Handle(ctx, conn, req) + ah.synchronizer.FileMux.RUnlock() + }() } } diff --git a/handler/textutils/textutils.go b/handler/textutils/textutils.go new file mode 100644 index 0000000..dbcf0b8 --- /dev/null +++ b/handler/textutils/textutils.go @@ -0,0 +1,112 @@ +package textutils + +import ( + "fmt" + + "github.com/bcmi-labs/arduino-language-server/lsp" +) + +// ApplyLSPTextDocumentContentChangeEvent applies the LSP change in the given text +func ApplyLSPTextDocumentContentChangeEvent(textDoc *lsp.TextDocumentItem, changes []lsp.TextDocumentContentChangeEvent, version int) error { + newText := textDoc.Text + for _, change := range changes { + if t, err := ApplyTextChange(newText, *change.Range, change.Text); err == nil { + newText = t + } else { + return err + } + } + textDoc.Text = newText + textDoc.Version = version + return nil +} + +// ApplyTextChange replaces startingText substring specified by replaceRange with insertText +func ApplyTextChange(startingText string, replaceRange lsp.Range, insertText string) (res string, err error) { + start, err := getOffset(startingText, replaceRange.Start) + if err != nil { + return "", err + } + end, err := getOffset(startingText, replaceRange.End) + if err != nil { + return "", err + } + + return startingText[:start] + insertText + startingText[end:], nil +} + +// getOffset computes the offset in the text expressed by the lsp.Position. +// Returns OutOfRangeError if the position is out of range. +func getOffset(text string, pos lsp.Position) (int, error) { + // Find line + lineOffset, err := getLineOffset(text, pos.Line) + if err != nil { + return -1, err + } + character := pos.Character + if character == 0 { + return lineOffset, nil + } + + // Find the character and return its offset within the text + var count = len(text[lineOffset:]) + for offset, c := range text[lineOffset:] { + if character == offset { + // We've found the character + return lineOffset + offset, nil + } + if c == '\n' { + // We've reached the end of line. LSP spec says we should default back to the line length. + // See https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#position + if character > offset { + return lineOffset + offset, nil + } + count = offset + break + } + } + if character > 0 { + // We've reached the end of the last line. Default to the text length (see above). + return len(text), nil + } + + // We haven't found the character in the text (character index was negative) + return -1, OutOfRangeError{"Character", count, character} +} + +// getLineOffset finds the offset/position of the beginning of a line within the text. +// For example: +// text := "foo\nfoobar\nbaz" +// getLineOffset(text, 0) == 0 +// getLineOffset(text, 1) == 4 +// getLineOffset(text, 2) == 11 +func getLineOffset(text string, line int) (int, error) { + if line == 0 { + return 0, nil + } + + // Find the line and return its offset within the text + var count int + for offset, c := range text { + if c == '\n' { + count++ + if count == line { + return offset + 1, nil + } + } + } + + // We haven't found the line in the text + return -1, OutOfRangeError{"Line", count, line} +} + +// OutOfRangeError returned if one attempts to access text out of its range +type OutOfRangeError struct { + Type string + Max int + Req int +} + +func (oor OutOfRangeError) Error() string { + return fmt.Sprintf("%s access out of range: max=%d requested=%d", oor.Type, oor.Max, oor.Req) +} diff --git a/handler/sourcemap_test.go b/handler/textutils/textutils_test.go similarity index 69% rename from handler/sourcemap_test.go rename to handler/textutils/textutils_test.go index d9fe22a..105b71c 100644 --- a/handler/sourcemap_test.go +++ b/handler/textutils/textutils_test.go @@ -1,76 +1,12 @@ -package handler +package textutils import ( - "reflect" "strings" "testing" - lsp "github.com/sourcegraph/go-lsp" + "github.com/bcmi-labs/arduino-language-server/lsp" ) -func TestCreateSourceMaps(t *testing.T) { - input := `#include -#line 1 "sketch_july2a.ino" -#line 1 "sketch_july2a.ino" - -#line 2 "sketch_july2a.ino" -void setup(); -#line 7 "sketch_july2a.ino" -void loop(); -#line 2 "sketch_july2a.ino" -void setup() { - // put your setup code here, to run once: - -} - -void loop() { - // put your main code here, to run repeatedly: - -} -` - sourceLineMap, targetLineMap := createSourceMaps(strings.NewReader(input)) - if !reflect.DeepEqual(sourceLineMap, map[int]int{ - 3: 0, 5: 1, 7: 6, 9: 1, 10: 2, 11: 3, 12: 4, 13: 5, 14: 6, 15: 7, 16: 8, 17: 9, 18: 10, - }) { - t.Error(sourceLineMap) - } - if !reflect.DeepEqual(targetLineMap, map[int]int{ - 0: 3, 1: 9, 2: 10, 3: 11, 4: 12, 5: 13, 6: 14, 7: 15, 8: 16, 9: 17, 10: 18, - }) { - t.Error(targetLineMap) - } -} - -func TestUpdateSourceMaps1(t *testing.T) { - targetLineMap := map[int]int{0: 1, 1: 2, 2: 0, 3: 5, 4: 3, 5: 4} - sourceLineMap := make(map[int]int) - for s, t := range targetLineMap { - sourceLineMap[t] = s - } - updateSourceMaps(sourceLineMap, targetLineMap, 0, 1, "foo\nbar\nbaz") - if !reflect.DeepEqual(targetLineMap, map[int]int{0: 1, 1: 2, 2: 3, 3: 4, 4: 0, 5: 7, 6: 5, 7: 6}) { - t.Error(targetLineMap) - } - if !reflect.DeepEqual(sourceLineMap, map[int]int{0: 4, 1: 0, 2: 1, 3: 2, 4: 3, 5: 6, 6: 7, 7: 5}) { - t.Error(sourceLineMap) - } -} - -func TestUpdateSourceMaps2(t *testing.T) { - targetLineMap := map[int]int{0: 1, 1: 2, 2: 0, 3: 5, 4: 3, 5: 4} - sourceLineMap := make(map[int]int) - for s, t := range targetLineMap { - sourceLineMap[t] = s - } - updateSourceMaps(sourceLineMap, targetLineMap, 2, 1, "foo") - if !reflect.DeepEqual(targetLineMap, map[int]int{0: 0, 1: 1, 2: 2, 3: 3}) { - t.Error(targetLineMap) - } - if !reflect.DeepEqual(sourceLineMap, map[int]int{0: 0, 1: 1, 2: 2, 3: 3}) { - t.Error(sourceLineMap) - } -} - func TestApplyTextChange(t *testing.T) { tests := []struct { InitialText string @@ -159,7 +95,7 @@ func TestApplyTextChange(t *testing.T) { expectation := strings.ReplaceAll(test.Expectation, "\n", "\\n") t.Logf("applyTextChange(\"%s\", %v, \"%s\") == \"%s\"", initial, test.Range, insertion, expectation) - act, err := applyTextChange(test.InitialText, test.Range, test.Insertion) + act, err := ApplyTextChange(test.InitialText, test.Range, test.Insertion) if act != test.Expectation { t.Errorf("applyTextChange(\"%s\", %v, \"%s\") != \"%s\", got \"%s\"", initial, test.Range, insertion, expectation, strings.ReplaceAll(act, "\n", "\\n")) } diff --git a/lsp/LICENSE b/lsp/LICENSE new file mode 100644 index 0000000..da96b67 --- /dev/null +++ b/lsp/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018 Sourcegraph + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lsp/README.md b/lsp/README.md new file mode 100644 index 0000000..1a48ef0 --- /dev/null +++ b/lsp/README.md @@ -0,0 +1,10 @@ +This module has been imported from: https://github.com/sourcegraph/go-lsp + +# go-lsp + +Package lsp contains Go types for the messages used in the Language Server +Protocol. + +See +https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md +for more information. diff --git a/lsp/jsonrpc2.go b/lsp/jsonrpc2.go new file mode 100644 index 0000000..11e2b86 --- /dev/null +++ b/lsp/jsonrpc2.go @@ -0,0 +1,52 @@ +package lsp + +import ( + "encoding/json" + "strconv" +) + +// ID represents a JSON-RPC 2.0 request ID, which may be either a +// string or number (or null, which is unsupported). +type ID struct { + // At most one of Num or Str may be nonzero. If both are zero + // valued, then IsNum specifies which field's value is to be used + // as the ID. + Num uint64 + Str string + + // IsString controls whether the Num or Str field's value should be + // used as the ID, when both are zero valued. It must always be + // set to true if the request ID is a string. + IsString bool +} + +func (id ID) String() string { + if id.IsString { + return strconv.Quote(id.Str) + } + return strconv.FormatUint(id.Num, 10) +} + +// MarshalJSON implements json.Marshaler. +func (id ID) MarshalJSON() ([]byte, error) { + if id.IsString { + return json.Marshal(id.Str) + } + return json.Marshal(id.Num) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (id *ID) UnmarshalJSON(data []byte) error { + // Support both uint64 and string IDs. + var v uint64 + if err := json.Unmarshal(data, &v); err == nil { + *id = ID{Num: v} + return nil + } + var v2 string + if err := json.Unmarshal(data, &v2); err != nil { + return err + } + *id = ID{Str: v2, IsString: true} + return nil +} diff --git a/handler/protocol.go b/lsp/protocol.go similarity index 64% rename from handler/protocol.go rename to lsp/protocol.go index 177d05e..6af5eeb 100644 --- a/handler/protocol.go +++ b/lsp/protocol.go @@ -1,47 +1,50 @@ -package handler +package lsp import ( "context" "encoding/json" - lsp "github.com/sourcegraph/go-lsp" "github.com/sourcegraph/jsonrpc2" ) -func readParams(method string, raw *json.RawMessage) (interface{}, error) { +func ReadParams(method string, raw *json.RawMessage) (interface{}, error) { switch method { case "initialize": - params := new(lsp.InitializeParams) + params := new(InitializeParams) err := json.Unmarshal(*raw, params) return params, err + case "initialized": + return &InitializedParams{}, nil case "textDocument/didOpen": - params := new(lsp.DidOpenTextDocumentParams) + params := new(DidOpenTextDocumentParams) err := json.Unmarshal(*raw, params) return params, err case "textDocument/didChange": - params := new(lsp.DidChangeTextDocumentParams) + params := new(DidChangeTextDocumentParams) err := json.Unmarshal(*raw, params) return params, err case "textDocument/didSave": - params := new(lsp.DidSaveTextDocumentParams) + params := new(DidSaveTextDocumentParams) err := json.Unmarshal(*raw, params) return params, err case "textDocument/didClose": - params := new(lsp.DidCloseTextDocumentParams) + params := new(DidCloseTextDocumentParams) err := json.Unmarshal(*raw, params) return params, err case "textDocument/completion": - params := new(lsp.CompletionParams) + params := new(CompletionParams) err := json.Unmarshal(*raw, params) return params, err case "textDocument/codeAction": - params := new(lsp.CodeActionParams) + params := new(CodeActionParams) err := json.Unmarshal(*raw, params) return params, err case "textDocument/signatureHelp": fallthrough case "textDocument/hover": - fallthrough + params := new(HoverParams) + err := json.Unmarshal(*raw, params) + return params, err case "textDocument/definition": fallthrough case "textDocument/typeDefinition": @@ -49,43 +52,43 @@ func readParams(method string, raw *json.RawMessage) (interface{}, error) { case "textDocument/implementation": fallthrough case "textDocument/documentHighlight": - params := new(lsp.TextDocumentPositionParams) + params := new(TextDocumentPositionParams) err := json.Unmarshal(*raw, params) return params, err case "textDocument/references": - params := new(lsp.ReferenceParams) + params := new(ReferenceParams) err := json.Unmarshal(*raw, params) return params, err case "textDocument/formatting": - params := new(lsp.DocumentFormattingParams) + params := new(DocumentFormattingParams) err := json.Unmarshal(*raw, params) return params, err case "textDocument/rangeFormatting": - params := new(lsp.DocumentRangeFormattingParams) + params := new(DocumentRangeFormattingParams) err := json.Unmarshal(*raw, params) return params, err case "textDocument/onTypeFormatting": - params := new(lsp.DocumentOnTypeFormattingParams) + params := new(DocumentOnTypeFormattingParams) err := json.Unmarshal(*raw, params) return params, err case "textDocument/documentSymbol": - params := new(lsp.DocumentSymbolParams) + params := new(DocumentSymbolParams) err := json.Unmarshal(*raw, params) return params, err case "textDocument/rename": - params := new(lsp.RenameParams) + params := new(RenameParams) err := json.Unmarshal(*raw, params) return params, err case "workspace/symbol": - params := new(lsp.WorkspaceSymbolParams) + params := new(WorkspaceSymbolParams) err := json.Unmarshal(*raw, params) return params, err case "workspace/didChangeWatchedFiles": - params := new(lsp.DidChangeWatchedFilesParams) + params := new(DidChangeWatchedFilesParams) err := json.Unmarshal(*raw, params) return params, err case "workspace/executeCommand": - params := new(lsp.ExecuteCommandParams) + params := new(ExecuteCommandParams) err := json.Unmarshal(*raw, params) return params, err case "workspace/applyEdit": @@ -93,7 +96,7 @@ func readParams(method string, raw *json.RawMessage) (interface{}, error) { err := json.Unmarshal(*raw, params) return params, err case "textDocument/publishDiagnostics": - params := new(lsp.PublishDiagnosticsParams) + params := new(PublishDiagnosticsParams) err := json.Unmarshal(*raw, params) return params, err case "arduino/selectedBoard": @@ -104,26 +107,26 @@ func readParams(method string, raw *json.RawMessage) (interface{}, error) { return nil, nil } -func sendRequest(ctx context.Context, conn *jsonrpc2.Conn, method string, params interface{}) (interface{}, error) { +func SendRequest(ctx context.Context, conn *jsonrpc2.Conn, method string, params interface{}) (interface{}, error) { switch method { case "initialize": - result := new(lsp.InitializeResult) + result := new(InitializeResult) err := conn.Call(ctx, method, params, result) return result, err case "textDocument/completion": - result := new(lsp.CompletionList) + result := new(CompletionList) err := conn.Call(ctx, method, params, result) return result, err case "textDocument/codeAction": - result := new([]*commandOrCodeAction) + result := new([]CommandOrCodeAction) err := conn.Call(ctx, method, params, result) return result, err case "completionItem/resolve": - result := new(lsp.CompletionItem) + result := new(CompletionItem) err := conn.Call(ctx, method, params, result) return result, err case "textDocument/signatureHelp": - result := new(lsp.SignatureHelp) + result := new(SignatureHelp) err := conn.Call(ctx, method, params, result) return result, err case "textDocument/hover": @@ -137,11 +140,11 @@ func sendRequest(ctx context.Context, conn *jsonrpc2.Conn, method string, params case "textDocument/implementation": fallthrough case "textDocument/references": - result := new([]lsp.Location) + result := new([]Location) err := conn.Call(ctx, method, params, result) return result, err case "textDocument/documentHighlight": - result := new([]lsp.DocumentHighlight) + result := new([]DocumentHighlight) err := conn.Call(ctx, method, params, result) return result, err case "textDocument/formatting": @@ -149,23 +152,23 @@ func sendRequest(ctx context.Context, conn *jsonrpc2.Conn, method string, params case "textDocument/rangeFormatting": fallthrough case "textDocument/onTypeFormatting": - result := new([]lsp.TextEdit) + result := new([]TextEdit) err := conn.Call(ctx, method, params, result) return result, err case "textDocument/documentSymbol": - result := new([]*documentSymbolOrSymbolInformation) + result := new(DocumentSymbolArrayOrSymbolInformationArray) err := conn.Call(ctx, method, params, result) return result, err case "textDocument/rename": - result := new(lsp.WorkspaceEdit) + result := new(WorkspaceEdit) err := conn.Call(ctx, method, params, result) return result, err case "workspace/symbol": - result := new([]lsp.SymbolInformation) + result := new([]SymbolInformation) err := conn.Call(ctx, method, params, result) return result, err case "window/showMessageRequest": - result := new(lsp.MessageActionItem) + result := new(MessageActionItem) err := conn.Call(ctx, method, params, result) return result, err case "workspace/executeCommand": @@ -184,20 +187,20 @@ func sendRequest(ctx context.Context, conn *jsonrpc2.Conn, method string, params // CodeAction structure according to LSP type CodeAction struct { - Title string `json:"title"` - Kind string `json:"kind,omitempty"` - Diagnostics []lsp.Diagnostic `json:"diagnostics,omitempty"` - Edit *lsp.WorkspaceEdit `json:"edit,omitempty"` - Command *lsp.Command `json:"command,omitempty"` + Title string `json:"title"` + Kind string `json:"kind,omitempty"` + Diagnostics []Diagnostic `json:"diagnostics,omitempty"` + Edit *WorkspaceEdit `json:"edit,omitempty"` + Command *Command `json:"command,omitempty"` } -type commandOrCodeAction struct { - Command *lsp.Command +type CommandOrCodeAction struct { + Command *Command CodeAction *CodeAction } -func (entry *commandOrCodeAction) UnmarshalJSON(raw []byte) error { - command := new(lsp.Command) +func (entry *CommandOrCodeAction) UnmarshalJSON(raw []byte) error { + command := new(Command) err := json.Unmarshal(raw, command) if err == nil && len(command.Command) > 0 { entry.Command = command @@ -212,7 +215,7 @@ func (entry *commandOrCodeAction) UnmarshalJSON(raw []byte) error { return nil } -func (entry *commandOrCodeAction) MarshalJSON() ([]byte, error) { +func (entry *CommandOrCodeAction) MarshalJSON() ([]byte, error) { if entry.Command != nil { return json.Marshal(entry.Command) } @@ -225,7 +228,13 @@ func (entry *commandOrCodeAction) MarshalJSON() ([]byte, error) { // Hover structure according to LSP type Hover struct { Contents MarkupContent `json:"contents"` - Range *lsp.Range `json:"range,omitempty"` + Range *Range `json:"range,omitempty"` +} + +// HoverParams structure according to LSP +type HoverParams struct { + TextDocumentPositionParams + // WorkDoneProgressParams } // MarkupContent structure according to LSP @@ -238,50 +247,55 @@ type MarkupContent struct { type DocumentSymbol struct { Name string `json:"name"` Detail string `json:"detail,omitempty"` - Kind lsp.SymbolKind `json:"kind"` + Kind SymbolKind `json:"kind"` Deprecated bool `json:"deprecated,omitempty"` - Range lsp.Range `json:"range"` - SelectionRange lsp.Range `json:"selectionRange"` + Range Range `json:"range"` + SelectionRange Range `json:"selectionRange"` Children []DocumentSymbol `json:"children,omitempty"` } -type documentSymbolOrSymbolInformation struct { - DocumentSymbol *DocumentSymbol - SymbolInformation *lsp.SymbolInformation +type DocumentSymbolArrayOrSymbolInformationArray struct { + DocumentSymbolArray *[]DocumentSymbol + SymbolInformationArray *[]SymbolInformation } -type documentSymbolOrSymbolInformationDiscriminator struct { - Range *lsp.Range `json:"range,omitempty"` - Location *lsp.Location `json:"location,omitempty"` -} - -func (entry *documentSymbolOrSymbolInformation) UnmarshalJSON(raw []byte) error { - discriminator := new(documentSymbolOrSymbolInformationDiscriminator) - err := json.Unmarshal(raw, discriminator) - if err != nil { +func (entry *DocumentSymbolArrayOrSymbolInformationArray) UnmarshalJSON(raw []byte) error { + intermediate := []json.RawMessage{} + if err := json.Unmarshal(raw, &intermediate); err != nil { + return err + } + discriminator := struct { + Range *Range `json:"range,omitempty"` + Location *Location `json:"location,omitempty"` + }{} + if err := json.Unmarshal(intermediate[0], &discriminator); err != nil { return err } if discriminator.Range != nil { - entry.DocumentSymbol = new(DocumentSymbol) - err = json.Unmarshal(raw, entry.DocumentSymbol) - if err != nil { - return err + res := make([]DocumentSymbol, len(intermediate)) + for i, item := range intermediate { + if err := json.Unmarshal(item, &res[i]); err != nil { + return err + } } + entry.DocumentSymbolArray = &res } if discriminator.Location != nil { - entry.SymbolInformation = new(lsp.SymbolInformation) - err = json.Unmarshal(raw, entry.SymbolInformation) - if err != nil { - return err + res := make([]SymbolInformation, len(intermediate)) + for i, item := range intermediate { + if err := json.Unmarshal(item, &res[i]); err != nil { + return err + } } + entry.SymbolInformationArray = &res } return nil } // ApplyWorkspaceEditParams structure according to LSP type ApplyWorkspaceEditParams struct { - Label string `json:"label,omitempty"` - Edit lsp.WorkspaceEdit `json:"edit"` + Label string `json:"label,omitempty"` + Edit WorkspaceEdit `json:"edit"` } // ApplyWorkspaceEditResponse structure according to LSP diff --git a/lsp/protocol_test.go b/lsp/protocol_test.go new file mode 100644 index 0000000..4986bdf --- /dev/null +++ b/lsp/protocol_test.go @@ -0,0 +1,66 @@ +package lsp + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDocumentSymbolParse(t *testing.T) { + docin := ` + [ + { + "kind":12, + "name":"setup", + "range": {"end": {"character":11,"line":6},"start": {"character":0,"line":6}}, + "selectionRange":{"end":{"character":10,"line":6},"start":{"character":5,"line":6}} + },{ + "kind":12, + "name":"newfunc", + "range":{"end":{"character":13,"line":8},"start":{"character":0,"line":8}}, + "selectionRange":{"end":{"character":12,"line":8},"start":{"character":5,"line":8}} + },{ + "kind":12, + "name":"loop", + "range":{"end":{"character":10,"line":10},"start":{"character":0,"line":10}}, + "selectionRange":{"end":{"character":9,"line":10},"start":{"character":5,"line":10}} + },{ + "kind":12, + "name":"secondFunction", + "range":{"end":{"character":20,"line":12},"start":{"character":0,"line":12}}, + "selectionRange":{"end":{"character":19,"line":12},"start":{"character":5,"line":12}} + },{ + "kind":12, + "name":"setup", + "range":{"end":{"character":0,"line":21},"start":{"character":0,"line":14}}, + "selectionRange":{"end":{"character":10,"line":14},"start":{"character":5,"line":14}} + },{ + "kind":12, + "name":"newfunc", + "range":{"end":{"character":16,"line":23},"start":{"character":0,"line":23}}, + "selectionRange":{"end":{"character":12,"line":23},"start":{"character":5,"line":23}} + },{ + "kind":12, + "name":"loop", + "range":{"end":{"character":0,"line":26},"start":{"character":0,"line":24}}, + "selectionRange":{"end":{"character":9,"line":24},"start":{"character":5,"line":24}} + },{ + "kind":12, + "name":"secondFunction", + "range":{"end":{"character":0,"line":32},"start":{"character":0,"line":30}}, + "selectionRange":{"end":{"character":19,"line":30},"start":{"character":5,"line":30}} + } + ]` + var res DocumentSymbolArrayOrSymbolInformationArray + err := json.Unmarshal([]byte(docin), &res) + require.NoError(t, err) + require.NotNil(t, res.DocumentSymbolArray) + symbols := *res.DocumentSymbolArray + require.Equal(t, SymbolKind(12), symbols[2].Kind) + require.Equal(t, "loop", symbols[2].Name) + require.Equal(t, "10:0-10:10", symbols[2].Range.String()) + require.Equal(t, "10:5-10:9", symbols[2].SelectionRange.String()) + fmt.Printf("%+v\n", res) +} diff --git a/lsp/service.go b/lsp/service.go new file mode 100644 index 0000000..b08d5a4 --- /dev/null +++ b/lsp/service.go @@ -0,0 +1,921 @@ +package lsp + +import ( + "bytes" + "encoding/base64" + "encoding/binary" + "encoding/json" + "strings" +) + +type None struct{} + +type InitializeParams struct { + ProcessID int `json:"processId,omitempty"` + + // RootPath is DEPRECATED in favor of the RootURI field. + RootPath string `json:"rootPath,omitempty"` + + RootURI DocumentURI `json:"rootUri,omitempty"` + ClientInfo ClientInfo `json:"clientInfo,omitempty"` + Trace Trace `json:"trace,omitempty"` + InitializationOptions interface{} `json:"initializationOptions,omitempty"` + Capabilities ClientCapabilities `json:"capabilities"` + + WorkDoneToken string `json:"workDoneToken,omitempty"` +} + +type InitializedParams struct{} + +// Root returns the RootURI if set, or otherwise the RootPath with 'file://' prepended. +func (p *InitializeParams) Root() DocumentURI { + if p.RootURI != "" { + return p.RootURI + } + if strings.HasPrefix(p.RootPath, "file://") { + return DocumentURI(p.RootPath) + } + return DocumentURI("file://" + p.RootPath) +} + +type DocumentURI string + +type ClientInfo struct { + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` +} + +type Trace string + +type ClientCapabilities struct { + Workspace WorkspaceClientCapabilities `json:"workspace,omitempty"` + TextDocument TextDocumentClientCapabilities `json:"textDocument,omitempty"` + Window WindowClientCapabilities `json:"window,omitempty"` + Experimental interface{} `json:"experimental,omitempty"` + + // Below are Sourcegraph extensions. They do not live in lspext since + // they are extending the field InitializeParams.Capabilities + + // XFilesProvider indicates the client provides support for + // workspace/xfiles. This is a Sourcegraph extension. + XFilesProvider bool `json:"xfilesProvider,omitempty"` + + // XContentProvider indicates the client provides support for + // textDocument/xcontent. This is a Sourcegraph extension. + XContentProvider bool `json:"xcontentProvider,omitempty"` + + // XCacheProvider indicates the client provides support for cache/get + // and cache/set. + XCacheProvider bool `json:"xcacheProvider,omitempty"` +} + +type WorkspaceClientCapabilities struct { + WorkspaceEdit struct { + DocumentChanges bool `json:"documentChanges,omitempty"` + ResourceOperations []string `json:"resourceOperations,omitempty"` + } `json:"workspaceEdit,omitempty"` + + ApplyEdit bool `json:"applyEdit,omitempty"` + + Symbol struct { + SymbolKind struct { + ValueSet []int `json:"valueSet,omitempty"` + } `json:"symbolKind,omitempty"` + } `json:"symbol,omitempty"` + + ExecuteCommand *struct { + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + } `json:"executeCommand,omitempty"` + + DidChangeWatchedFiles *struct { + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + } `json:"didChangeWatchedFiles,omitempty"` + + WorkspaceFolders bool `json:"workspaceFolders,omitempty"` + + Configuration bool `json:"configuration,omitempty"` +} + +type TextDocumentClientCapabilities struct { + Declaration *struct { + LinkSupport bool `json:"linkSupport,omitempty"` + } `json:"declaration,omitempty"` + + Definition *struct { + LinkSupport bool `json:"linkSupport,omitempty"` + } `json:"definition,omitempty"` + + Implementation *struct { + LinkSupport bool `json:"linkSupport,omitempty"` + + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + } `json:"implementation,omitempty"` + + TypeDefinition *struct { + LinkSupport bool `json:"linkSupport,omitempty"` + } `json:"typeDefinition,omitempty"` + + Synchronization *struct { + WillSave bool `json:"willSave,omitempty"` + DidSave bool `json:"didSave,omitempty"` + WillSaveWaitUntil bool `json:"willSaveWaitUntil,omitempty"` + } `json:"synchronization,omitempty"` + + DocumentSymbol struct { + SymbolKind struct { + ValueSet []int `json:"valueSet,omitempty"` + } `json:"symbolKind,omitempty"` + + HierarchicalDocumentSymbolSupport bool `json:"hierarchicalDocumentSymbolSupport,omitempty"` + } `json:"documentSymbol,omitempty"` + + Formatting *struct { + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + } `json:"formatting,omitempty"` + + RangeFormatting *struct { + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + } `json:"rangeFormatting,omitempty"` + + Rename *struct { + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + + PrepareSupport bool `json:"prepareSupport,omitempty"` + } `json:"rename,omitempty"` + + SemanticHighlightingCapabilities *struct { + SemanticHighlighting bool `json:"semanticHighlighting,omitempty"` + } `json:"semanticHighlightingCapabilities,omitempty"` + + CodeAction struct { + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + + IsPreferredSupport bool `json:"isPreferredSupport,omitempty"` + + CodeActionLiteralSupport struct { + CodeActionKind struct { + ValueSet []CodeActionKind `json:"valueSet,omitempty"` + } `json:"codeActionKind,omitempty"` + } `json:"codeActionLiteralSupport,omitempty"` + } `json:"codeAction,omitempty"` + + Completion struct { + CompletionItem struct { + DocumentationFormat []DocumentationFormat `json:"documentationFormat,omitempty"` + SnippetSupport bool `json:"snippetSupport,omitempty"` + } `json:"completionItem,omitempty"` + + CompletionItemKind struct { + ValueSet []CompletionItemKind `json:"valueSet,omitempty"` + } `json:"completionItemKind,omitempty"` + + ContextSupport bool `json:"contextSupport,omitempty"` + } `json:"completion,omitempty"` + + SignatureHelp *struct { + SignatureInformation struct { + ParameterInformation struct { + LabelOffsetSupport bool `json:"labelOffsetSupport,omitempty"` + } `json:"parameterInformation,omitempty"` + } `json:"signatureInformation,omitempty"` + } `json:"signatureHelp,omitempty"` + + DocumentLink *struct { + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + + TooltipSupport bool `json:"tooltipSupport,omitempty"` + } `json:"documentLink,omitempty"` + + Hover *struct { + ContentFormat []string `json:"contentFormat,omitempty"` + } `json:"hover,omitempty"` + + FoldingRange *struct { + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + + RangeLimit interface{} `json:"rangeLimit,omitempty"` + + LineFoldingOnly bool `json:"lineFoldingOnly,omitempty"` + } `json:"foldingRange,omitempty"` + + CallHierarchy *struct { + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + } `json:"callHierarchy,omitempty"` + + ColorProvider *struct { + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + } `json:"colorProvider,omitempty"` +} + +type WindowClientCapabilities struct { + WorkDoneProgress bool `json:"workDoneProgress,omitempty"` +} + +type InitializeResult struct { + Capabilities ServerCapabilities `json:"capabilities,omitempty"` +} + +type InitializeError struct { + Retry bool `json:"retry"` +} + +type ResourceOperation string + +const ( + ROCreate ResourceOperation = "create" + RODelete ResourceOperation = "delete" + RORename ResourceOperation = "rename" +) + +// TextDocumentSyncKind is a DEPRECATED way to describe how text +// document syncing works. Use TextDocumentSyncOptions instead (or the +// Options field of TextDocumentSyncOptionsOrKind if you need to +// support JSON-(un)marshaling both). +type TextDocumentSyncKind int + +const ( + TDSKNone TextDocumentSyncKind = 0 + TDSKFull TextDocumentSyncKind = 1 + TDSKIncremental TextDocumentSyncKind = 2 +) + +type TextDocumentSyncOptions struct { + OpenClose bool `json:"openClose,omitempty"` + Change TextDocumentSyncKind `json:"change"` + WillSave bool `json:"willSave,omitempty"` + WillSaveWaitUntil bool `json:"willSaveWaitUntil,omitempty"` + Save *SaveOptions `json:"save,omitempty"` +} + +// TextDocumentSyncOptions holds either a TextDocumentSyncKind or +// TextDocumentSyncOptions. The LSP API allows either to be specified +// in the (ServerCapabilities).TextDocumentSync field. +type TextDocumentSyncOptionsOrKind struct { + Kind *TextDocumentSyncKind + Options *TextDocumentSyncOptions +} + +// MarshalJSON implements json.Marshaler. +func (v *TextDocumentSyncOptionsOrKind) MarshalJSON() ([]byte, error) { + if v == nil { + return []byte("null"), nil + } + if v.Kind != nil { + return json.Marshal(v.Kind) + } + return json.Marshal(v.Options) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (v *TextDocumentSyncOptionsOrKind) UnmarshalJSON(data []byte) error { + if bytes.Equal(data, []byte("null")) { + *v = TextDocumentSyncOptionsOrKind{} + return nil + } + var kind TextDocumentSyncKind + if err := json.Unmarshal(data, &kind); err == nil { + // Create equivalent TextDocumentSyncOptions using the same + // logic as in vscode-languageclient. Also set the Kind field + // so that JSON-marshaling and unmarshaling are inverse + // operations (for backward compatibility, preserving the + // original input but accepting both). + *v = TextDocumentSyncOptionsOrKind{ + Options: &TextDocumentSyncOptions{OpenClose: true, Change: kind}, + Kind: &kind, + } + return nil + } + var tmp TextDocumentSyncOptions + if err := json.Unmarshal(data, &tmp); err != nil { + return err + } + *v = TextDocumentSyncOptionsOrKind{Options: &tmp} + return nil +} + +type SaveOptions struct { + IncludeText bool `json:"includeText"` +} + +type ServerCapabilities struct { + TextDocumentSync *TextDocumentSyncOptionsOrKind `json:"textDocumentSync,omitempty"` + HoverProvider bool `json:"hoverProvider,omitempty"` + CompletionProvider *CompletionOptions `json:"completionProvider,omitempty"` + SignatureHelpProvider *SignatureHelpOptions `json:"signatureHelpProvider,omitempty"` + DefinitionProvider bool `json:"definitionProvider,omitempty"` + TypeDefinitionProvider bool `json:"typeDefinitionProvider,omitempty"` + ReferencesProvider bool `json:"referencesProvider,omitempty"` + DocumentHighlightProvider bool `json:"documentHighlightProvider,omitempty"` + DocumentSymbolProvider bool `json:"documentSymbolProvider,omitempty"` + WorkspaceSymbolProvider bool `json:"workspaceSymbolProvider,omitempty"` + ImplementationProvider bool `json:"implementationProvider,omitempty"` + CodeActionProvider bool `json:"codeActionProvider,omitempty"` + CodeLensProvider *CodeLensOptions `json:"codeLensProvider,omitempty"` + DocumentFormattingProvider bool `json:"documentFormattingProvider,omitempty"` + DocumentRangeFormattingProvider bool `json:"documentRangeFormattingProvider,omitempty"` + DocumentOnTypeFormattingProvider *DocumentOnTypeFormattingOptions `json:"documentOnTypeFormattingProvider,omitempty"` + RenameProvider bool `json:"renameProvider,omitempty"` + ExecuteCommandProvider *ExecuteCommandOptions `json:"executeCommandProvider,omitempty"` + SemanticHighlighting *SemanticHighlightingOptions `json:"semanticHighlighting,omitempty"` + + // XWorkspaceReferencesProvider indicates the server provides support for + // xworkspace/references. This is a Sourcegraph extension. + XWorkspaceReferencesProvider bool `json:"xworkspaceReferencesProvider,omitempty"` + + // XDefinitionProvider indicates the server provides support for + // textDocument/xdefinition. This is a Sourcegraph extension. + XDefinitionProvider bool `json:"xdefinitionProvider,omitempty"` + + // XWorkspaceSymbolByProperties indicates the server provides support for + // querying symbols by properties with WorkspaceSymbolParams.symbol. This + // is a Sourcegraph extension. + XWorkspaceSymbolByProperties bool `json:"xworkspaceSymbolByProperties,omitempty"` + + Experimental interface{} `json:"experimental,omitempty"` +} + +type CompletionOptions struct { + ResolveProvider bool `json:"resolveProvider,omitempty"` + TriggerCharacters []string `json:"triggerCharacters,omitempty"` +} + +type DocumentOnTypeFormattingOptions struct { + FirstTriggerCharacter string `json:"firstTriggerCharacter"` + MoreTriggerCharacter []string `json:"moreTriggerCharacter,omitempty"` +} + +type CodeLensOptions struct { + ResolveProvider bool `json:"resolveProvider,omitempty"` +} + +type SignatureHelpOptions struct { + TriggerCharacters []string `json:"triggerCharacters,omitempty"` +} + +type ExecuteCommandOptions struct { + Commands []string `json:"commands"` +} + +type ExecuteCommandParams struct { + Command string `json:"command"` + Arguments []interface{} `json:"arguments,omitempty"` +} + +type SemanticHighlightingOptions struct { + Scopes [][]string `json:"scopes,omitempty"` +} + +type CompletionItemKind int + +const ( + _ CompletionItemKind = iota + CIKText + CIKMethod + CIKFunction + CIKConstructor + CIKField + CIKVariable + CIKClass + CIKInterface + CIKModule + CIKProperty + CIKUnit + CIKValue + CIKEnum + CIKKeyword + CIKSnippet + CIKColor + CIKFile + CIKReference + CIKFolder + CIKEnumMember + CIKConstant + CIKStruct + CIKEvent + CIKOperator + CIKTypeParameter +) + +func (c CompletionItemKind) String() string { + return completionItemKindName[c] +} + +var completionItemKindName = map[CompletionItemKind]string{ + CIKText: "text", + CIKMethod: "method", + CIKFunction: "function", + CIKConstructor: "constructor", + CIKField: "field", + CIKVariable: "variable", + CIKClass: "class", + CIKInterface: "interface", + CIKModule: "module", + CIKProperty: "property", + CIKUnit: "unit", + CIKValue: "value", + CIKEnum: "enum", + CIKKeyword: "keyword", + CIKSnippet: "snippet", + CIKColor: "color", + CIKFile: "file", + CIKReference: "reference", + CIKFolder: "folder", + CIKEnumMember: "enumMember", + CIKConstant: "constant", + CIKStruct: "struct", + CIKEvent: "event", + CIKOperator: "operator", + CIKTypeParameter: "typeParameter", +} + +type CompletionItem struct { + Label string `json:"label"` + Kind CompletionItemKind `json:"kind,omitempty"` + Detail string `json:"detail,omitempty"` + Documentation string `json:"documentation,omitempty"` + SortText string `json:"sortText,omitempty"` + FilterText string `json:"filterText,omitempty"` + InsertText string `json:"insertText,omitempty"` + InsertTextFormat InsertTextFormat `json:"insertTextFormat,omitempty"` + TextEdit *TextEdit `json:"textEdit,omitempty"` + Data interface{} `json:"data,omitempty"` +} + +type CompletionList struct { + IsIncomplete bool `json:"isIncomplete"` + Items []CompletionItem `json:"items"` +} + +type CompletionTriggerKind int + +const ( + CTKInvoked CompletionTriggerKind = 1 + CTKTriggerCharacter = 2 +) + +type DocumentationFormat string + +const ( + DFPlainText DocumentationFormat = "plaintext" +) + +type CodeActionKind string + +const ( + CAKEmpty CodeActionKind = "" + CAKQuickFix CodeActionKind = "quickfix" + CAKRefactor CodeActionKind = "refactor" + CAKRefactorExtract CodeActionKind = "refactor.extract" + CAKRefactorInline CodeActionKind = "refactor.inline" + CAKRefactorRewrite CodeActionKind = "refactor.rewrite" + CAKSource CodeActionKind = "source" + CAKSourceOrganizeImports CodeActionKind = "source.organizeImports" +) + +type InsertTextFormat int + +const ( + ITFPlainText InsertTextFormat = 1 + ITFSnippet = 2 +) + +type CompletionContext struct { + TriggerKind CompletionTriggerKind `json:"triggerKind"` + TriggerCharacter string `json:"triggerCharacter,omitempty"` +} + +type CompletionParams struct { + TextDocumentPositionParams + Context CompletionContext `json:"context,omitempty"` +} + +// type Hover struct { +// Contents []MarkedString `json:"contents"` +// Range *Range `json:"range,omitempty"` +// } + +// type hover Hover + +// func (h Hover) MarshalJSON() ([]byte, error) { +// if h.Contents == nil { +// return json.Marshal(hover{ +// Contents: []MarkedString{}, +// Range: h.Range, +// }) +// } +// return json.Marshal(hover(h)) +// } + +type MarkedString markedString + +type markedString struct { + Language string `json:"language"` + Value string `json:"value"` + + isRawString bool +} + +func (m *MarkedString) UnmarshalJSON(data []byte) error { + if d := strings.TrimSpace(string(data)); len(d) > 0 && d[0] == '"' { + // Raw string + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + m.Value = s + m.isRawString = true + return nil + } + // Language string + ms := (*markedString)(m) + return json.Unmarshal(data, ms) +} + +func (m MarkedString) MarshalJSON() ([]byte, error) { + if m.isRawString { + return json.Marshal(m.Value) + } + return json.Marshal((markedString)(m)) +} + +// RawMarkedString returns a MarkedString consisting of only a raw +// string (i.e., "foo" instead of {"value":"foo", "language":"bar"}). +func RawMarkedString(s string) MarkedString { + return MarkedString{Value: s, isRawString: true} +} + +type SignatureHelp struct { + Signatures []SignatureInformation `json:"signatures"` + ActiveSignature int `json:"activeSignature"` + ActiveParameter int `json:"activeParameter"` +} + +type SignatureInformation struct { + Label string `json:"label"` + Documentation string `json:"documentation,omitempty"` + Parameters []ParameterInformation `json:"parameters,omitempty"` +} + +type ParameterInformation struct { + Label string `json:"label"` + Documentation string `json:"documentation,omitempty"` +} + +type ReferenceContext struct { + IncludeDeclaration bool `json:"includeDeclaration"` + + // Sourcegraph extension + XLimit int `json:"xlimit,omitempty"` +} + +type ReferenceParams struct { + TextDocumentPositionParams + Context ReferenceContext `json:"context"` +} + +type DocumentHighlightKind int + +const ( + Text DocumentHighlightKind = 1 + Read = 2 + Write = 3 +) + +type DocumentHighlight struct { + Range Range `json:"range"` + Kind int `json:"kind,omitempty"` +} + +type DocumentSymbolParams struct { + TextDocument TextDocumentIdentifier `json:"textDocument"` +} + +type SymbolKind int + +// The SymbolKind values are defined at https://microsoft.github.io/language-server-protocol/specification. +const ( + SKFile SymbolKind = 1 + SKModule SymbolKind = 2 + SKNamespace SymbolKind = 3 + SKPackage SymbolKind = 4 + SKClass SymbolKind = 5 + SKMethod SymbolKind = 6 + SKProperty SymbolKind = 7 + SKField SymbolKind = 8 + SKConstructor SymbolKind = 9 + SKEnum SymbolKind = 10 + SKInterface SymbolKind = 11 + SKFunction SymbolKind = 12 + SKVariable SymbolKind = 13 + SKConstant SymbolKind = 14 + SKString SymbolKind = 15 + SKNumber SymbolKind = 16 + SKBoolean SymbolKind = 17 + SKArray SymbolKind = 18 + SKObject SymbolKind = 19 + SKKey SymbolKind = 20 + SKNull SymbolKind = 21 + SKEnumMember SymbolKind = 22 + SKStruct SymbolKind = 23 + SKEvent SymbolKind = 24 + SKOperator SymbolKind = 25 + SKTypeParameter SymbolKind = 26 +) + +func (s SymbolKind) String() string { + return symbolKindName[s] +} + +var symbolKindName = map[SymbolKind]string{ + SKFile: "File", + SKModule: "Module", + SKNamespace: "Namespace", + SKPackage: "Package", + SKClass: "Class", + SKMethod: "Method", + SKProperty: "Property", + SKField: "Field", + SKConstructor: "Constructor", + SKEnum: "Enum", + SKInterface: "Interface", + SKFunction: "Function", + SKVariable: "Variable", + SKConstant: "Constant", + SKString: "String", + SKNumber: "Number", + SKBoolean: "Boolean", + SKArray: "Array", + SKObject: "Object", + SKKey: "Key", + SKNull: "Null", + SKEnumMember: "EnumMember", + SKStruct: "Struct", + SKEvent: "Event", + SKOperator: "Operator", + SKTypeParameter: "TypeParameter", +} + +type SymbolInformation struct { + Name string `json:"name"` + Kind SymbolKind `json:"kind"` + Location Location `json:"location"` + ContainerName string `json:"containerName,omitempty"` +} + +type WorkspaceSymbolParams struct { + Query string `json:"query"` + Limit int `json:"limit"` +} + +type ConfigurationParams struct { + Items []ConfigurationItem `json:"items"` +} + +type ConfigurationItem struct { + ScopeURI string `json:"scopeUri,omitempty"` + Section string `json:"section,omitempty"` +} + +type ConfigurationResult []interface{} + +type CodeActionContext struct { + Diagnostics []Diagnostic `json:"diagnostics"` +} + +type CodeActionParams struct { + TextDocument TextDocumentIdentifier `json:"textDocument"` + Range Range `json:"range"` + Context CodeActionContext `json:"context"` +} + +type CodeLensParams struct { + TextDocument TextDocumentIdentifier `json:"textDocument"` +} + +type CodeLens struct { + Range Range `json:"range"` + Command Command `json:"command,omitempty"` + Data interface{} `json:"data,omitempty"` +} + +type DocumentFormattingParams struct { + TextDocument TextDocumentIdentifier `json:"textDocument"` + Options FormattingOptions `json:"options"` +} + +type FormattingOptions struct { + TabSize int `json:"tabSize"` + InsertSpaces bool `json:"insertSpaces"` + Key string `json:"key"` +} + +type RenameParams struct { + TextDocument TextDocumentIdentifier `json:"textDocument"` + Position Position `json:"position"` + NewName string `json:"newName"` +} + +type DidOpenTextDocumentParams struct { + TextDocument TextDocumentItem `json:"textDocument"` +} + +type DidChangeTextDocumentParams struct { + TextDocument VersionedTextDocumentIdentifier `json:"textDocument"` + ContentChanges []TextDocumentContentChangeEvent `json:"contentChanges"` +} + +type TextDocumentContentChangeEvent struct { + Range *Range `json:"range,omitempty"` + RangeLength uint `json:"rangeLength,omitempty"` + Text string `json:"text"` +} + +type DidCloseTextDocumentParams struct { + TextDocument TextDocumentIdentifier `json:"textDocument"` +} + +type DidSaveTextDocumentParams struct { + TextDocument TextDocumentIdentifier `json:"textDocument"` +} + +type MessageType int + +const ( + MTError MessageType = 1 + MTWarning = 2 + Info = 3 + Log = 4 +) + +type ShowMessageParams struct { + Type MessageType `json:"type"` + Message string `json:"message"` +} + +type MessageActionItem struct { + Title string `json:"title"` +} + +type ShowMessageRequestParams struct { + Type MessageType `json:"type"` + Message string `json:"message"` + Actions []MessageActionItem `json:"actions"` +} + +type LogMessageParams struct { + Type MessageType `json:"type"` + Message string `json:"message"` +} + +type DidChangeConfigurationParams struct { + Settings interface{} `json:"settings"` +} + +type FileChangeType int + +const ( + Created FileChangeType = 1 + Changed = 2 + Deleted = 3 +) + +type FileEvent struct { + URI DocumentURI `json:"uri"` + Type int `json:"type"` +} + +type DidChangeWatchedFilesParams struct { + Changes []FileEvent `json:"changes"` +} + +type PublishDiagnosticsParams struct { + URI DocumentURI `json:"uri"` + Diagnostics []Diagnostic `json:"diagnostics"` +} + +type DocumentRangeFormattingParams struct { + TextDocument TextDocumentIdentifier `json:"textDocument"` + Range Range `json:"range"` + Options FormattingOptions `json:"options"` +} + +type DocumentOnTypeFormattingParams struct { + TextDocument TextDocumentIdentifier `json:"textDocument"` + Position Position `json:"position"` + Ch string `json:"ch"` + Options FormattingOptions `json:"formattingOptions"` +} + +type CancelParams struct { + ID ID `json:"id"` +} + +type SemanticHighlightingParams struct { + TextDocument VersionedTextDocumentIdentifier `json:"textDocument"` + Lines []SemanticHighlightingInformation `json:"lines"` +} + +// SemanticHighlightingInformation represents a semantic highlighting +// information that has to be applied on a specific line of the text +// document. +type SemanticHighlightingInformation struct { + // Line is the zero-based line position in the text document. + Line int `json:"line"` + + // Tokens is a base64 encoded string representing every single highlighted + // characters with its start position, length and the "lookup table" index of + // the semantic highlighting [TextMate scopes](https://manual.macromates.com/en/language_grammars). + // If the `tokens` is empty or not defined, then no highlighted positions are + // available for the line. + Tokens SemanticHighlightingTokens `json:"tokens,omitempty"` +} + +type semanticHighlightingInformation struct { + Line int `json:"line"` + Tokens *string `json:"tokens"` +} + +// MarshalJSON implements json.Marshaler. +func (v *SemanticHighlightingInformation) MarshalJSON() ([]byte, error) { + tokens := string(v.Tokens.Serialize()) + return json.Marshal(&semanticHighlightingInformation{ + Line: v.Line, + Tokens: &tokens, + }) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (v *SemanticHighlightingInformation) UnmarshalJSON(data []byte) error { + var info semanticHighlightingInformation + err := json.Unmarshal(data, &info) + if err != nil { + return err + } + + if info.Tokens != nil { + v.Tokens, err = DeserializeSemanticHighlightingTokens([]byte(*info.Tokens)) + if err != nil { + return err + } + } + + v.Line = info.Line + return nil +} + +type SemanticHighlightingTokens []SemanticHighlightingToken + +func (v SemanticHighlightingTokens) Serialize() []byte { + var chunks [][]byte + + // Writes each token to `tokens` in the byte format specified by the LSP + // proposal. Described below: + // |<---- 4 bytes ---->|<-- 2 bytes -->|<--- 2 bytes -->| + // | character | length | index | + for _, token := range v { + chunk := make([]byte, 8) + binary.BigEndian.PutUint32(chunk[:4], token.Character) + binary.BigEndian.PutUint16(chunk[4:6], token.Length) + binary.BigEndian.PutUint16(chunk[6:], token.Scope) + chunks = append(chunks, chunk) + } + + src := make([]byte, len(chunks)*8) + for i, chunk := range chunks { + copy(src[i*8:i*8+8], chunk) + } + + dst := make([]byte, base64.StdEncoding.EncodedLen(len(src))) + base64.StdEncoding.Encode(dst, src) + return dst +} + +func DeserializeSemanticHighlightingTokens(src []byte) (SemanticHighlightingTokens, error) { + dst := make([]byte, base64.StdEncoding.DecodedLen(len(src))) + n, err := base64.StdEncoding.Decode(dst, src) + if err != nil { + return nil, err + } + + var chunks [][]byte + for i := 7; i < len(dst[:n]); i += 8 { + chunks = append(chunks, dst[i-7:i+1]) + } + + var tokens SemanticHighlightingTokens + for _, chunk := range chunks { + tokens = append(tokens, SemanticHighlightingToken{ + Character: binary.BigEndian.Uint32(chunk[:4]), + Length: binary.BigEndian.Uint16(chunk[4:6]), + Scope: binary.BigEndian.Uint16(chunk[6:]), + }) + } + + return tokens, nil +} + +type SemanticHighlightingToken struct { + Character uint32 + Length uint16 + Scope uint16 +} diff --git a/lsp/structures.go b/lsp/structures.go new file mode 100644 index 0000000..e7a93b6 --- /dev/null +++ b/lsp/structures.go @@ -0,0 +1,168 @@ +package lsp + +import ( + "encoding/json" + "fmt" +) + +type Position struct { + /** + * Line position in a document (zero-based). + */ + Line int `json:"line"` + + /** + * Character offset on a line in a document (zero-based). + */ + Character int `json:"character"` +} + +func (p Position) String() string { + return fmt.Sprintf("%d:%d", p.Line, p.Character) +} + +type Range struct { + /** + * The range's start position. + */ + Start Position `json:"start"` + + /** + * The range's end position. + */ + End Position `json:"end"` +} + +func (r Range) String() string { + return fmt.Sprintf("%s-%s", r.Start, r.End) +} + +type Location struct { + URI DocumentURI `json:"uri"` + Range Range `json:"range"` +} + +type Diagnostic struct { + /** + * The range at which the message applies. + */ + Range Range `json:"range"` + + /** + * The diagnostic's severity. Can be omitted. If omitted it is up to the + * client to interpret diagnostics as error, warning, info or hint. + */ + Severity DiagnosticSeverity `json:"severity,omitempty"` + + /** + * The diagnostic's code. Can be omitted. + */ + Code string `json:"code,omitempty"` + + /** + * A human-readable string describing the source of this + * diagnostic, e.g. 'typescript' or 'super lint'. + */ + Source string `json:"source,omitempty"` + + /** + * The diagnostic's message. + */ + Message string `json:"message"` +} + +type DiagnosticSeverity int + +const ( + Error DiagnosticSeverity = 1 + Warning = 2 + Information = 3 + Hint = 4 +) + +type Command struct { + /** + * Title of the command, like `save`. + */ + Title string `json:"title"` + /** + * The identifier of the actual command handler. + */ + Command string `json:"command"` + /** + * Arguments that the command handler should be + * invoked with. + */ + Arguments []json.RawMessage `json:"arguments"` +} + +type TextEdit struct { + /** + * The range of the text document to be manipulated. To insert + * text into a document create a range where start === end. + */ + Range Range `json:"range"` + + /** + * The string to be inserted. For delete operations use an + * empty string. + */ + NewText string `json:"newText"` +} + +type WorkspaceEdit struct { + /** + * Holds changes to existing resources. + */ + Changes map[DocumentURI][]TextEdit `json:"changes"` +} + +type TextDocumentIdentifier struct { + /** + * The text document's URI. + */ + URI DocumentURI `json:"uri"` +} + +type TextDocumentItem struct { + /** + * The text document's URI. + */ + URI DocumentURI `json:"uri"` + + /** + * The text document's language identifier. + */ + LanguageID string `json:"languageId"` + + /** + * The version number of this document (it will strictly increase after each + * change, including undo/redo). + */ + Version int `json:"version"` + + /** + * The content of the opened text document. + */ + Text string `json:"text"` +} + +type VersionedTextDocumentIdentifier struct { + TextDocumentIdentifier + /** + * The version number of this document. + */ + Version int `json:"version"` +} + +type TextDocumentPositionParams struct { + /** + * The text document. + */ + TextDocument TextDocumentIdentifier `json:"textDocument"` + + /** + * The position inside the text document. + */ + Position Position `json:"position"` +} diff --git a/handler/uri.go b/lsp/uri.go similarity index 51% rename from handler/uri.go rename to lsp/uri.go index 0553723..9c84adf 100644 --- a/handler/uri.go +++ b/lsp/uri.go @@ -1,4 +1,4 @@ -package handler +package lsp import ( "net/url" @@ -7,13 +7,18 @@ import ( "runtime" "strings" - "github.com/pkg/errors" - lsp "github.com/sourcegraph/go-lsp" + "github.com/arduino/go-paths-helper" ) var expDriveID = regexp.MustCompile("[a-zA-Z]:") -func uriToPath(uri lsp.DocumentURI) string { +// AsPath convert the DocumentURI to a paths.Path +func (uri DocumentURI) AsPath() *paths.Path { + return paths.New(uri.Unbox()) +} + +// Unbox convert the DocumentURI to a file path string +func (uri DocumentURI) Unbox() string { urlObj, err := url.Parse(string(uri)) if err != nil { return string(uri) @@ -34,7 +39,22 @@ func uriToPath(uri lsp.DocumentURI) string { return path } -func pathToURI(path string) lsp.DocumentURI { +func (uri DocumentURI) String() string { + return string(uri) +} + +// Ext returns the extension of the file pointed by the URI +func (uri DocumentURI) Ext() string { + return filepath.Ext(string(uri)) +} + +// NewDocumentURIFromPath create a DocumentURI from the given Path object +func NewDocumentURIFromPath(path *paths.Path) DocumentURI { + return NewDocumentURI(path.String()) +} + +// NewDocumentURI create a DocumentURI from the given string path +func NewDocumentURI(path string) DocumentURI { urlObj, err := url.Parse("file://") if err != nil { panic(err) @@ -45,9 +65,5 @@ func pathToURI(path string) lsp.DocumentURI { urlObj.Path += "/" + url.PathEscape(segment) } } - return lsp.DocumentURI(urlObj.String()) -} - -func unknownURI(uri lsp.DocumentURI) error { - return errors.New("Document is not available: " + string(uri)) + return DocumentURI(urlObj.String()) } diff --git a/handler/uri_test.go b/lsp/uri_test.go similarity index 59% rename from handler/uri_test.go rename to lsp/uri_test.go index 3a760f7..98e5fb3 100644 --- a/handler/uri_test.go +++ b/lsp/uri_test.go @@ -1,50 +1,48 @@ -package handler +package lsp import ( "path/filepath" "runtime" "testing" - - lsp "github.com/sourcegraph/go-lsp" ) func TestUriToPath(t *testing.T) { var path string if runtime.GOOS == "windows" { - path = uriToPath(lsp.DocumentURI("file:///C:/Users/test/Sketch.ino")) + path = DocumentURI("file:///C:/Users/test/Sketch.ino").Unbox() if path != "C:\\Users\\test\\Sketch.ino" { t.Error(path) } - path = uriToPath(lsp.DocumentURI("file:///c%3A/Users/test/Sketch.ino")) + path = DocumentURI("file:///c%3A/Users/test/Sketch.ino").Unbox() if path != "C:\\Users\\test\\Sketch.ino" { t.Error(path) } } else { - path = uriToPath(lsp.DocumentURI("file:///Users/test/Sketch.ino")) + path = DocumentURI("file:///Users/test/Sketch.ino").Unbox() if path != "/Users/test/Sketch.ino" { t.Error(path) } } - path = uriToPath(lsp.DocumentURI("file:///%25F0%259F%2598%259B")) + path = DocumentURI("file:///%25F0%259F%2598%259B").Unbox() if path != string(filepath.Separator)+"\U0001F61B" { t.Error(path) } } func TestPathToUri(t *testing.T) { - var uri lsp.DocumentURI + var uri DocumentURI if runtime.GOOS == "windows" { - uri = pathToURI("C:\\Users\\test\\Sketch.ino") + uri = NewDocumentURI("C:\\Users\\test\\Sketch.ino") if uri != "file:///C:/Users/test/Sketch.ino" { t.Error(uri) } } else { - uri = pathToURI("/Users/test/Sketch.ino") + uri = NewDocumentURI("/Users/test/Sketch.ino") if uri != "file:///Users/test/Sketch.ino" { t.Error(uri) } } - uri = pathToURI("\U0001F61B") + uri = NewDocumentURI("\U0001F61B") if uri != "file:///%25F0%259F%2598%259B" { t.Error(uri) } diff --git a/main.go b/main.go index 9980174..d76c81a 100644 --- a/main.go +++ b/main.go @@ -2,13 +2,14 @@ package main import ( "flag" - "fmt" "io" "log" "os" - "os/exec" + "github.com/arduino/go-paths-helper" "github.com/bcmi-labs/arduino-language-server/handler" + "github.com/bcmi-labs/arduino-language-server/lsp" + "github.com/bcmi-labs/arduino-language-server/streams" ) var clangdPath string @@ -29,46 +30,29 @@ func main() { flag.StringVar(&loggingBasePath, "logpath", ".", "Location where to write logging files to when logging is enabled") flag.Parse() - // var stdinLog, stdoutLog, clangdinLog, clangdoutLog, clangderrLog io.Writer - var logStreams *handler.StreamLogger - if enableLogging { - var err error - logStreams, err = handler.NewStreamLogger(loggingBasePath) - if err != nil { - log.Fatal(err) - } - defer logStreams.Close() + if loggingBasePath != "" { + streams.GlobalLogDirectory = paths.New(loggingBasePath) + } else if enableLogging { + log.Fatalf("Please specify logpath") + } - log.SetOutput(logStreams.Default) + if enableLogging { + logfile := streams.OpenLogFileAs("inols-err.log") + log.SetOutput(io.MultiWriter(logfile, os.Stderr)) + defer streams.CatchAndLogPanic() } else { - logStreams = handler.NewNoopLogger() log.SetOutput(os.Stderr) } - handler.Setup(cliPath, enableLogging, true) - initialBoard := handler.Board{Fqbn: initialFqbn, Name: initialBoardName} - inoHandler := handler.NewInoHandler(os.Stdin, os.Stdout, logStreams, startClangd, initialBoard) - defer inoHandler.StopClangd() - <-inoHandler.StdioConn.DisconnectNotify() -} + handler.Setup(cliPath, clangdPath, enableLogging, true) + initialBoard := lsp.Board{Fqbn: initialFqbn, Name: initialBoardName} -func startClangd() (clangdIn io.WriteCloser, clangdOut io.ReadCloser, clangdErr io.ReadCloser) { + stdio := streams.NewReadWriteCloser(os.Stdin, os.Stdout) if enableLogging { - log.Println("Starting clangd process:", clangdPath) + stdio = streams.LogReadWriteCloserAs(stdio, "inols.log") } - var clangdCmd *exec.Cmd - if compileCommandsDir != "" { - clangdCmd = exec.Command(clangdPath) - } else { - clangdCmd = exec.Command(clangdPath, fmt.Sprintf(`--compile-commands-dir="%s"`, compileCommandsDir)) - } - clangdIn, _ = clangdCmd.StdinPipe() - clangdOut, _ = clangdCmd.StdoutPipe() - clangdErr, _ = clangdCmd.StderrPipe() - err := clangdCmd.Start() - if err != nil { - panic(err) - } - return + inoHandler := handler.NewInoHandler(stdio, initialBoard) + defer inoHandler.StopClangd() + <-inoHandler.StdioConn.DisconnectNotify() } diff --git a/streams/dumper.go b/streams/dumper.go new file mode 100644 index 0000000..a948aa1 --- /dev/null +++ b/streams/dumper.go @@ -0,0 +1,89 @@ +package streams + +import ( + "fmt" + "io" + "log" + "os" + + "github.com/arduino/go-paths-helper" +) + +// GlobalLogDirectory is the directory where logs are created +var GlobalLogDirectory *paths.Path + +// LogReadWriteCloserAs return a proxy for the given upstream io.ReadWriteCloser +// that forward and logs all read/write/close operations on the given filename +// that is created in the GlobalLogDirectory. +func LogReadWriteCloserAs(upstream io.ReadWriteCloser, filename string) io.ReadWriteCloser { + return &dumper{ + upstream: upstream, + logfile: OpenLogFileAs(filename), + } +} + +// LogReadWriteCloserToFile return a proxy for the given upstream io.ReadWriteCloser +// that forward and logs all read/write/close operations on the given file. +func LogReadWriteCloserToFile(upstream io.ReadWriteCloser, file *os.File) io.ReadWriteCloser { + return &dumper{ + upstream: upstream, + logfile: file, + } +} + +// OpenLogFileAs creates a log file in GlobalLogDirectory. +func OpenLogFileAs(filename string) *os.File { + path := GlobalLogDirectory.Join(filename) + res, err := path.Append() + if err != nil { + log.Fatalf("Error opening log file: %s", err) + } else { + abs, _ := path.Abs() + log.Printf("logging to %s", abs) + } + return res +} + +type dumper struct { + upstream io.ReadWriteCloser + logfile *os.File + reading bool + writing bool +} + +func (d *dumper) Read(buff []byte) (int, error) { + n, err := d.upstream.Read(buff) + if err != nil { + d.logfile.Write([]byte(fmt.Sprintf("<<< Read Error: %s\n", err))) + } else { + if !d.reading { + d.reading = true + d.writing = false + d.logfile.Write([]byte("\n<<<\n")) + } + d.logfile.Write(buff[:n]) + } + return n, err +} + +func (d *dumper) Write(buff []byte) (int, error) { + n, err := d.upstream.Write(buff) + if err != nil { + _, _ = d.logfile.Write([]byte(fmt.Sprintf(">>> Write Error: %s\n", err))) + } else { + if !d.writing { + d.writing = true + d.reading = false + d.logfile.Write([]byte("\n>>>\n")) + } + _, _ = d.logfile.Write(buff[:n]) + } + return n, err +} + +func (d *dumper) Close() error { + err := d.upstream.Close() + _, _ = d.logfile.Write([]byte(fmt.Sprintf("--- Stream closed, err=%s\n", err))) + _ = d.logfile.Close() + return err +} diff --git a/streams/panics.go b/streams/panics.go new file mode 100644 index 0000000..c4b7d0d --- /dev/null +++ b/streams/panics.go @@ -0,0 +1,17 @@ +package streams + +import ( + "fmt" + "log" + "runtime/debug" +) + +// CatchAndLogPanic will recover a panic, log it on standard logger, and rethrow it +// to continue stack unwinding. +func CatchAndLogPanic() { + if r := recover(); r != nil { + reason := fmt.Sprintf("%v", r) + log.Println(fmt.Sprintf("Panic: %s\n\n%s", reason, string(debug.Stack()))) + panic(reason) + } +} diff --git a/streams/streams.go b/streams/streams.go new file mode 100644 index 0000000..10d313c --- /dev/null +++ b/streams/streams.go @@ -0,0 +1,33 @@ +package streams + +import "io" + +// NewReadWriteCloser create an io.ReadWriteCloser from given io.ReadCloser and io.WriteCloser. +func NewReadWriteCloser(in io.ReadCloser, out io.WriteCloser) io.ReadWriteCloser { + return &combinedReadWriteCloser{in, out} +} + +type combinedReadWriteCloser struct { + reader io.ReadCloser + writer io.WriteCloser +} + +func (sd *combinedReadWriteCloser) Read(p []byte) (int, error) { + return sd.reader.Read(p) +} + +func (sd *combinedReadWriteCloser) Write(p []byte) (int, error) { + return sd.writer.Write(p) +} + +func (sd *combinedReadWriteCloser) Close() error { + ierr := sd.reader.Close() + oerr := sd.writer.Close() + if ierr != nil { + return ierr + } + if oerr != nil { + return oerr + } + return nil +}