├── .editorconfig ├── .gitattributes ├── .gitignore ├── .npmignore ├── README.md ├── package-lock.json ├── package.json └── src ├── cli.ts ├── logger.ts ├── server.ts ├── tsServerClient.ts ├── tsconfig.json └── utils.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | 9 | [*] 10 | charset = utf-8 11 | indent_style = space 12 | indent_size = 4 13 | 14 | [{package.json,package-lock.json}] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | /lib/ 4 | /node_modules/ 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | src/ 3 | npm-debug.log 4 | .gitattributes 5 | .npmignore 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typescript-language-server 2 | [Language Server Protocol](https://github.com/Microsoft/language-server-protocol) implementation for Typescript via tsserver.cmd 3 | 4 | **:warning: Deprecated in favor of https://github.com/theia-ide/typescript-language-server :warning:** 5 | 6 | # Installing 7 | 8 | ```sh 9 | npm install -g typescript-language-server 10 | ``` 11 | 12 | # Running the language server 13 | 14 | ``` 15 | typescript-language-server --stdio 16 | ``` 17 | 18 | **Note:** `typescript-language-server` requires `tsserver` to be in path. You can install using `npm install -g typescript`. 19 | If you would like to use a different `tsserver` specify the absolute path using `--tserver-path`. Make sure to append `.cmd` if windows. 20 | 21 | ## Options 22 | 23 | ``` 24 | $ typescript-language-server --help 25 | 26 | Usage: typescript-language-server [options] 27 | 28 | 29 | Options: 30 | 31 | -V, --version output the version number 32 | --stdio use stdio 33 | --node-ipc use node-ipc 34 | --socket use socket. example: --socket=5000 35 | --tsserver-path absolute path to tsserver. example: --tsserver-path=c:\tsc\tsserver 36 | -h, --help output usage information 37 | ``` 38 | 39 | # Supported Protocol features 40 | 41 | * textDocument/completion 42 | * textDocument/definition 43 | * textDocument/didChange 44 | * textDocument/didClose 45 | * textDocument/didOpen 46 | * textDocument/didSave 47 | * textDocument/documentSymbol 48 | * textDocument/hover 49 | * textDocument/rename 50 | * textDocument/references 51 | * workspace/symbol 52 | 53 | # Development 54 | 55 | ### Build 56 | 57 | ```sh 58 | npm install 59 | npm run build 60 | ``` 61 | 62 | ### Watch 63 | 64 | ```sh 65 | npm install 66 | npm run watch 67 | ``` 68 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-language-server", 3 | "version": "0.0.19", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/commander": { 8 | "version": "2.9.1", 9 | "resolved": "https://registry.npmjs.org/@types/commander/-/commander-2.9.1.tgz", 10 | "integrity": "sha512-P4s1aEJYjHB1pmS5th4WXQ/NEYykK1FmCFJL+NIrDCptWs2DIS/SsVjmaubvcWZ17VzlG+CwiAv52ghkfkepWA==", 11 | "dev": true, 12 | "requires": { 13 | "@types/node": "8.0.19" 14 | } 15 | }, 16 | "@types/node": { 17 | "version": "8.0.19", 18 | "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.19.tgz", 19 | "integrity": "sha512-VRQB+Q0L3YZWs45uRdpN9oWr82meL/8TrJ6faoKT5tp0uub2l/aRMhtm5fo68h7kjYKH60f9/bay1nF7ZpTW5g==", 20 | "dev": true 21 | }, 22 | "@types/pino": { 23 | "version": "3.0.1", 24 | "resolved": "https://registry.npmjs.org/@types/pino/-/pino-3.0.1.tgz", 25 | "integrity": "sha1-kNuGjYu3I8FrOkL8dT/iVwlVNHk=", 26 | "dev": true, 27 | "requires": { 28 | "@types/node": "8.0.19" 29 | } 30 | }, 31 | "ansi-styles": { 32 | "version": "3.2.0", 33 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", 34 | "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", 35 | "requires": { 36 | "color-convert": "1.9.0" 37 | } 38 | }, 39 | "chalk": { 40 | "version": "2.1.0", 41 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz", 42 | "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==", 43 | "requires": { 44 | "ansi-styles": "3.2.0", 45 | "escape-string-regexp": "1.0.5", 46 | "supports-color": "4.2.1" 47 | } 48 | }, 49 | "color-convert": { 50 | "version": "1.9.0", 51 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", 52 | "integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=", 53 | "requires": { 54 | "color-name": "1.1.3" 55 | } 56 | }, 57 | "color-name": { 58 | "version": "1.1.3", 59 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 60 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 61 | }, 62 | "commander": { 63 | "version": "2.11.0", 64 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", 65 | "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==" 66 | }, 67 | "core-util-is": { 68 | "version": "1.0.2", 69 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 70 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 71 | }, 72 | "crypto-random-string": { 73 | "version": "1.0.0", 74 | "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", 75 | "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=" 76 | }, 77 | "end-of-stream": { 78 | "version": "1.4.0", 79 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.0.tgz", 80 | "integrity": "sha1-epDYM+/abPpurA9JSduw+tOmMgY=", 81 | "requires": { 82 | "once": "1.4.0" 83 | } 84 | }, 85 | "escape-string-regexp": { 86 | "version": "1.0.5", 87 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 88 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 89 | }, 90 | "fast-json-parse": { 91 | "version": "1.0.3", 92 | "resolved": "https://registry.npmjs.org/fast-json-parse/-/fast-json-parse-1.0.3.tgz", 93 | "integrity": "sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw==" 94 | }, 95 | "fast-safe-stringify": { 96 | "version": "1.2.0", 97 | "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-1.2.0.tgz", 98 | "integrity": "sha1-69QmZv0Y/k8rpPDSlQZfP4XK3pY=" 99 | }, 100 | "flatstr": { 101 | "version": "1.0.5", 102 | "resolved": "https://registry.npmjs.org/flatstr/-/flatstr-1.0.5.tgz", 103 | "integrity": "sha1-W0UbCMvUji6sVKK74L9GFlqhS+M=" 104 | }, 105 | "has-flag": { 106 | "version": "2.0.0", 107 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", 108 | "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" 109 | }, 110 | "inherits": { 111 | "version": "2.0.3", 112 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 113 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 114 | }, 115 | "isarray": { 116 | "version": "1.0.0", 117 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 118 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 119 | }, 120 | "once": { 121 | "version": "1.4.0", 122 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 123 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 124 | "requires": { 125 | "wrappy": "1.0.2" 126 | } 127 | }, 128 | "pify": { 129 | "version": "2.3.0", 130 | "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", 131 | "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" 132 | }, 133 | "pino": { 134 | "version": "4.7.1", 135 | "resolved": "https://registry.npmjs.org/pino/-/pino-4.7.1.tgz", 136 | "integrity": "sha1-2CCN6SUGX0nJ1KcvVFCaFn/hgBg=", 137 | "requires": { 138 | "chalk": "2.1.0", 139 | "fast-json-parse": "1.0.3", 140 | "fast-safe-stringify": "1.2.0", 141 | "flatstr": "1.0.5", 142 | "pump": "1.0.2", 143 | "quick-format-unescaped": "1.1.1", 144 | "split2": "2.1.1" 145 | } 146 | }, 147 | "process-nextick-args": { 148 | "version": "1.0.7", 149 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", 150 | "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" 151 | }, 152 | "pump": { 153 | "version": "1.0.2", 154 | "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.2.tgz", 155 | "integrity": "sha1-Oz7mUS+U8OV1U4wXmV+fFpkKXVE=", 156 | "requires": { 157 | "end-of-stream": "1.4.0", 158 | "once": "1.4.0" 159 | } 160 | }, 161 | "quick-format-unescaped": { 162 | "version": "1.1.1", 163 | "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-1.1.1.tgz", 164 | "integrity": "sha1-53VV7z5m4QXUA54T73kgEoT+6RY=", 165 | "requires": { 166 | "fast-safe-stringify": "1.2.0" 167 | } 168 | }, 169 | "readable-stream": { 170 | "version": "2.3.3", 171 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", 172 | "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", 173 | "requires": { 174 | "core-util-is": "1.0.2", 175 | "inherits": "2.0.3", 176 | "isarray": "1.0.0", 177 | "process-nextick-args": "1.0.7", 178 | "safe-buffer": "5.1.1", 179 | "string_decoder": "1.0.3", 180 | "util-deprecate": "1.0.2" 181 | } 182 | }, 183 | "safe-buffer": { 184 | "version": "5.1.1", 185 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 186 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 187 | }, 188 | "split2": { 189 | "version": "2.1.1", 190 | "resolved": "https://registry.npmjs.org/split2/-/split2-2.1.1.tgz", 191 | "integrity": "sha1-eh9VHhdqkOzTNF9yRqDP4XXvT9A=", 192 | "requires": { 193 | "through2": "2.0.3" 194 | } 195 | }, 196 | "string_decoder": { 197 | "version": "1.0.3", 198 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", 199 | "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", 200 | "requires": { 201 | "safe-buffer": "5.1.1" 202 | } 203 | }, 204 | "supports-color": { 205 | "version": "4.2.1", 206 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.2.1.tgz", 207 | "integrity": "sha512-qxzYsob3yv6U+xMzPrv170y8AwGP7i74g+pbixCfD6rgso8BscLT2qXIuz6TpOaiJZ3mFgT5O9lyT9nMU4LfaA==", 208 | "requires": { 209 | "has-flag": "2.0.0" 210 | } 211 | }, 212 | "temp-dir": { 213 | "version": "1.0.0", 214 | "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", 215 | "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=" 216 | }, 217 | "tempy": { 218 | "version": "0.1.0", 219 | "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.1.0.tgz", 220 | "integrity": "sha1-hSdBPNBxAINPzJy7gkK+lboOH+4=", 221 | "requires": { 222 | "pify": "2.3.0", 223 | "temp-dir": "1.0.0", 224 | "unique-string": "1.0.0" 225 | } 226 | }, 227 | "through2": { 228 | "version": "2.0.3", 229 | "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", 230 | "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", 231 | "requires": { 232 | "readable-stream": "2.3.3", 233 | "xtend": "4.0.1" 234 | } 235 | }, 236 | "typescript": { 237 | "version": "2.5.2", 238 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.2.tgz", 239 | "integrity": "sha1-A4qV99m7tCCxvzW6MdTFwd0//jQ=" 240 | }, 241 | "unique-string": { 242 | "version": "1.0.0", 243 | "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", 244 | "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", 245 | "requires": { 246 | "crypto-random-string": "1.0.0" 247 | } 248 | }, 249 | "util-deprecate": { 250 | "version": "1.0.2", 251 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 252 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 253 | }, 254 | "vscode-jsonrpc": { 255 | "version": "3.3.1", 256 | "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-3.3.1.tgz", 257 | "integrity": "sha512-iLlG27498AJF0j4GJ4yua7Z9bpJfLfwpAaAA9mihe6VDoYHwK8TyFgnpXdgjoTb8X9/DnzimQeg0bjIWINvPWw==" 258 | }, 259 | "vscode-languageserver": { 260 | "version": "3.3.0", 261 | "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-3.3.0.tgz", 262 | "integrity": "sha512-8YvEbjl77Nl24/cgcSbQC/CPEhSoJwvLLKGJZJbIF4DsGo4jrVDbuARXfmUt9S6vCEEr++o3fbbZ17iLZ0QH0A==", 263 | "requires": { 264 | "vscode-jsonrpc": "3.3.1", 265 | "vscode-languageserver-types": "3.3.0" 266 | } 267 | }, 268 | "vscode-languageserver-types": { 269 | "version": "3.3.0", 270 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.3.0.tgz", 271 | "integrity": "sha512-5BMClM4D3mRl5JlWFxIxhhJAbcVW9dFviz8ubppmG8epCTzl1bPpndcnvsjOjUlVsO9V8l8Ktklqc70Ew6soew==" 272 | }, 273 | "wrappy": { 274 | "version": "1.0.2", 275 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 276 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 277 | }, 278 | "xtend": { 279 | "version": "4.0.1", 280 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", 281 | "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" 282 | } 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-language-server", 3 | "version": "0.0.19", 4 | "description": "Language Server Protocol (LSP) implementation for TypeScript using tsserver", 5 | "scripts": { 6 | "build": "tsc -p src", 7 | "watch": "tsc --watch -p src", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "bin": { 11 | "typescript-language-server": "./lib/cli.js" 12 | }, 13 | "author": "", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@types/commander": "^2.9.1", 17 | "@types/node": "^8.0.19", 18 | "@types/pino": "^3.0.1" 19 | }, 20 | "dependencies": { 21 | "commander": "^2.11.0", 22 | "pino": "^4.7.1", 23 | "tempy": "^0.1.0", 24 | "typescript": "^2.5.2", 25 | "vscode-languageserver": "^3.3.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Command } from 'commander'; 4 | 5 | import { Server } from './server'; 6 | import * as utils from './utils'; 7 | 8 | const program = new Command('typescript-language-server') 9 | .version(require('../package.json').version) 10 | .option('--stdio', 'use stdio') 11 | .option('--node-ipc', 'use node-ipc') 12 | .option('--socket ', 'use socket. example: --socket=5000') 13 | .option('--tsserver-path ', 14 | `absolute path to tsserver. example: --tsserver-path=${utils.isWindows() ? 'c:\\tsc\\tsserver.cmd' : '/bin/tsserver'}`, 15 | utils.isWindows() ? 'tsserver.cmd' : 'tsserver') 16 | .parse(process.argv); 17 | 18 | if (!(program.stdio || program.socket || program['node-ipc'])) { 19 | console.error('Connection type required (stdio, node-ipc, socket). Refer to --help for more details.'); 20 | process.exit(1); 21 | } 22 | 23 | new Server({ 24 | tsserverPath: program.tsserverPath 25 | }).listen(); 26 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | import * as pino from 'pino'; 2 | import * as fs from 'fs'; 3 | 4 | export type ILogger = pino.Logger; 5 | 6 | export function createLogger(logFile?: string): ILogger { 7 | if (logFile) { 8 | return pino(fs.createWriteStream(logFile)); 9 | } else { 10 | const logger = pino(); 11 | logger.level = 'silent'; 12 | return logger; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createConnection, 3 | CompletionList, 4 | CompletionItem, 5 | Definition, 6 | DidChangeTextDocumentParams, 7 | DidOpenTextDocumentParams, 8 | Hover, 9 | IConnection, 10 | InitializeParams, 11 | InitializeResult, 12 | Location, 13 | RenameParams, 14 | SymbolKind, 15 | SymbolInformation, 16 | TextDocumentPositionParams, 17 | TextDocumentSyncKind, 18 | TextEdit, 19 | WorkspaceSymbolParams, 20 | WorkspaceEdit, 21 | } from 'vscode-languageserver'; 22 | 23 | import * as tempy from 'tempy'; 24 | import * as fs from 'fs'; 25 | 26 | import { ILogger, createLogger } from './logger'; 27 | import { TsServerClient } from './tsServerClient'; 28 | 29 | import * as TsProtocol from 'typescript/lib/protocol'; 30 | 31 | import * as utils from './utils'; 32 | 33 | export interface IServerOptions { 34 | logFile?: string; 35 | tsserverPath: string; 36 | tsserverLogFile?: string; 37 | } 38 | 39 | export class Server { 40 | 41 | private connection: IConnection; 42 | 43 | private initializeParams: InitializeParams; 44 | private initializeResult: InitializeResult; 45 | private tsServerClient: TsServerClient; 46 | 47 | logger: ILogger; 48 | 49 | private _lastFile: string; // dummy file required for navto 50 | 51 | constructor( 52 | private options: IServerOptions) { 53 | this.logger = createLogger(options.logFile); 54 | this.connection = createConnection(); 55 | 56 | this.registerLspEvents(); 57 | } 58 | 59 | private registerLspEvents(): void { 60 | this.connection.onInitialize(this._onInitialize.bind(this)); 61 | this.connection.onDidOpenTextDocument(this.onDidOpenTextDocument.bind(this)); 62 | this.connection.onDidSaveTextDocument(this.onDidSaveTextDocument.bind(this)); 63 | this.connection.onDidCloseTextDocument(this.onDidCloseTextDocument.bind(this)); 64 | this.connection.onDidChangeTextDocument(this.onDidChangeTextDocument.bind(this)); 65 | this.connection.onDefinition(this.onDefinition.bind(this)); 66 | this.connection.onDocumentSymbol(this.onDocumentSymbol.bind(this)); 67 | this.connection.onCompletion(this.onCompletion.bind(this)); 68 | this.connection.onHover(this.onHover.bind(this)); 69 | this.connection.onReferences(this.onReferences.bind(this)); 70 | this.connection.onRenameRequest(this.onRename.bind(this)); 71 | this.connection.onWorkspaceSymbol(this.onWorkspaceSymbol.bind(this)); 72 | } 73 | 74 | listen() { 75 | this.logger.info('Server.listen()'); 76 | this.connection.listen(); 77 | } 78 | 79 | private _onInitialize(params: InitializeParams): InitializeResult { 80 | this.logger.info('Server._onInitialize()', params); 81 | 82 | // TODO: validate rootPath and rootUri 83 | 84 | this.initializeParams = params; 85 | 86 | this.tsServerClient = new TsServerClient({ 87 | tsserverPath: this.options.tsserverPath, 88 | logFile: this.options.tsserverLogFile, 89 | logger: this.logger 90 | }); 91 | 92 | this.tsServerClient.start(); 93 | 94 | this.initializeResult = { 95 | capabilities: { 96 | textDocumentSync: { 97 | change: TextDocumentSyncKind.Full, 98 | openClose: true, 99 | save: { 100 | includeText: true 101 | } 102 | }, 103 | completionProvider: { 104 | triggerCharacters: ['.'], 105 | resolveProvider: false 106 | }, 107 | definitionProvider: true, 108 | documentSymbolProvider: true, 109 | hoverProvider: true, 110 | renameProvider: true, 111 | referencesProvider: true, 112 | workspaceSymbolProvider: true 113 | } 114 | }; 115 | 116 | this.logger.info('Server._onInitialize() result', this.initializeResult); 117 | return this.initializeResult; 118 | } 119 | 120 | private onDidOpenTextDocument(params: DidOpenTextDocumentParams): void { 121 | const path = utils.uriToPath(params.textDocument.uri); 122 | this._lastFile = path; 123 | this.logger.info('Server.onDidOpenTextDocument()', params, path); 124 | this.tsServerClient.sendOpen(path); 125 | } 126 | 127 | private onDidCloseTextDocument(params: DidOpenTextDocumentParams): void { 128 | const path = utils.uriToPath(params.textDocument.uri); 129 | this.logger.info('Server.onDidCloseTextDocument()', params, path); 130 | this.tsServerClient.sendClose(path); 131 | } 132 | 133 | private onDidChangeTextDocument(params: DidChangeTextDocumentParams): void { 134 | const path = utils.uriToPath(params.textDocument.uri), 135 | tempPath = tempy.file({ extension: 'ts'}); 136 | 137 | this._lastFile = path; 138 | 139 | this.logger.info('Server.onDidCloseTextDocument()', params, path, tempPath); 140 | 141 | fs.writeFileSync(tempPath, params.contentChanges[0].text, 'utf8'); 142 | this.tsServerClient.sendReload(path, tempPath); 143 | } 144 | 145 | private onDidSaveTextDocument(params: DidChangeTextDocumentParams): void { 146 | const path = utils.uriToPath(params.textDocument.uri), 147 | tempPath = tempy.file({ extension: 'ts'}); 148 | 149 | this._lastFile = path; 150 | 151 | this.logger.info('Server.onDidChangeTextDocument()', params, path, tempPath); 152 | 153 | fs.writeFileSync(tempPath, params.contentChanges[0].text, 'utf8'); 154 | this.tsServerClient.sendSaveTo(path, tempPath); 155 | } 156 | 157 | private onDefinition(params: TextDocumentPositionParams): Thenable { 158 | const path = utils.uriToPath(params.textDocument.uri); 159 | 160 | this.logger.info('Server.onDefinition()', params, path); 161 | 162 | return this.tsServerClient.sendDefinition( 163 | path, 164 | params.position.line + 1, 165 | params.position.character + 1) 166 | .then(result => { 167 | return result.body 168 | .map(fileSpan => utils.tsServerFileSpanToLspLocation(fileSpan)); 169 | }); 170 | } 171 | 172 | private onDocumentSymbol(params: TextDocumentPositionParams): Thenable { 173 | const path = utils.uriToPath(params.textDocument.uri); 174 | 175 | this.logger.info('Server.onDocumentSymbol()', params, path); 176 | 177 | return this.tsServerClient.sendNavTree( 178 | path) 179 | .then(result => { 180 | // walk navtree in pre-order 181 | const symbols: SymbolInformation[] = [], 182 | stack: TsProtocol.NavigationTree[] = []; 183 | 184 | if (!!result.body) { 185 | stack.push(result.body); 186 | } 187 | 188 | while (stack.length > 0) { 189 | const item = stack.pop(), 190 | span = item.spans[0]; 191 | 192 | if (span) { 193 | symbols.push({ 194 | location: { 195 | uri: params.textDocument.uri, 196 | range: { 197 | start: utils.tsServerLocationToLspPosition(span.start), 198 | end: utils.tsServerLocationToLspPosition(span.end) 199 | } 200 | }, 201 | // TODO: for now make everything as variable for unknown 202 | kind: utils.symbolKindsMapping[item.kind] || SymbolKind.Variable, 203 | name: item.text 204 | }); 205 | } 206 | 207 | if (item.childItems) { 208 | item.childItems.forEach(childItem => stack.push(childItem)); 209 | } 210 | } 211 | 212 | return symbols; 213 | }); 214 | } 215 | 216 | private onCompletion(params: TextDocumentPositionParams): Thenable { 217 | const path = utils.uriToPath(params.textDocument.uri); 218 | 219 | this.logger.info('Server.onCompletion()', params, path); 220 | 221 | return this.tsServerClient.sendCompletions( 222 | path, 223 | params.position.line + 1, 224 | params.position.character + 1, 225 | '') 226 | .then(result => { 227 | return { 228 | isIncomplete: false, 229 | items: result.body 230 | .map(item => { 231 | return { 232 | label: item.name, 233 | kind: utils.completionKindsMapping[item.kind] 234 | }; 235 | }) 236 | }; 237 | }); 238 | } 239 | 240 | private onHover(params: TextDocumentPositionParams): Thenable { 241 | const path = utils.uriToPath(params.textDocument.uri); 242 | 243 | this.logger.info('Server.onHover()', params, path); 244 | 245 | return this.tsServerClient.sendQuickInfo( 246 | path, 247 | params.position.line + 1, 248 | params.position.character + 1) 249 | .then(result => { 250 | return { 251 | contents: result.body.displayString, 252 | range: { 253 | start: utils.tsServerLocationToLspPosition(result.body.start), 254 | end: utils.tsServerLocationToLspPosition(result.body.end) 255 | } 256 | }; 257 | }); 258 | } 259 | 260 | private onRename(params: RenameParams): Thenable { 261 | const path = utils.uriToPath(params.textDocument.uri); 262 | 263 | this.logger.info('Server.onRename()', params, path); 264 | 265 | return this.tsServerClient.sendRename( 266 | path, 267 | params.position.line + 1, 268 | params.position.character + 1) 269 | .then(result => { 270 | if (!result.body || !result.body.info.canRename || result.body.locs.length == 0) { 271 | return {}; 272 | } 273 | 274 | const workspaceEdit = { 275 | changes: {} 276 | }; 277 | 278 | result.body.locs 279 | .forEach((spanGroup) => { 280 | const uri = utils.pathToUri(spanGroup.file), 281 | textEdits = workspaceEdit.changes[uri] || (workspaceEdit.changes[uri] = []); 282 | 283 | spanGroup.locs.forEach((textSpan) => { 284 | textEdits.push({ 285 | newText: params.newName, 286 | range: { 287 | start: utils.tsServerLocationToLspPosition(textSpan.start), 288 | end: utils.tsServerLocationToLspPosition(textSpan.end) 289 | } 290 | }); 291 | }); 292 | }); 293 | 294 | return workspaceEdit; 295 | }); 296 | } 297 | 298 | private onReferences(params: TextDocumentPositionParams): Thenable { 299 | const path = utils.uriToPath(params.textDocument.uri); 300 | 301 | this.logger.info('Server.onReferences()', params, path); 302 | 303 | return this.tsServerClient.sendReferences( 304 | path, 305 | params.position.line + 1, 306 | params.position.character + 1) 307 | .then(result => { 308 | return result.body.refs 309 | .map(fileSpan => utils.tsServerFileSpanToLspLocation(fileSpan)); 310 | }); 311 | } 312 | 313 | private onWorkspaceSymbol(params: WorkspaceSymbolParams): Thenable { 314 | return this.tsServerClient.sendNavTo( 315 | params.query, 316 | this._lastFile 317 | ) 318 | .then((result) => { 319 | return result.body.map(item => { 320 | return { 321 | location: { 322 | uri: utils.pathToUri(item.file), 323 | range: { 324 | start: utils.tsServerLocationToLspPosition(item.start), 325 | end: utils.tsServerLocationToLspPosition(item.end) 326 | } 327 | }, 328 | // TODO: for now make everything as variable for unknown 329 | kind: utils.symbolKindsMapping[item.kind] || SymbolKind.Variable, 330 | name: item.name 331 | }; 332 | }); 333 | }); 334 | } 335 | 336 | 337 | } 338 | -------------------------------------------------------------------------------- /src/tsServerClient.ts: -------------------------------------------------------------------------------- 1 | import * as cp from 'child_process'; 2 | import * as readline from 'readline'; 3 | import * as protocol from 'typescript/lib/protocol'; 4 | 5 | import { ILogger } from './logger'; 6 | 7 | import * as utils from './utils'; 8 | 9 | export interface ITsServerClientOptions { 10 | logger: ILogger; 11 | tsserverPath: string; 12 | logFile?: string; 13 | } 14 | 15 | export class TsServerClient { 16 | private started = false; 17 | private cp: cp.ChildProcess; 18 | private seq = 0; 19 | 20 | private buffer = ''; 21 | private header:Record; 22 | 23 | private deferreds = {}; 24 | 25 | get logger(): ILogger { 26 | return this.options.logger; 27 | } 28 | 29 | constructor( 30 | private options: ITsServerClientOptions) { 31 | } 32 | 33 | start() { 34 | if (this.started) { 35 | return; 36 | } 37 | 38 | this.logger.info('TsServerClient.start(): starting', { tsserverPath: this.options.tsserverPath, logFile: this.options.logFile }); 39 | this.cp = cp.spawn(this.options.tsserverPath, this.options.logFile ? ['-logToFile', 'true', '-file', this.options.logFile] : [] ); 40 | this.cp.stdout.setEncoding('utf8'); 41 | this.cp.stdout.addListener('data', this.onTsServerData.bind(this)); 42 | this.logger.info('TsServerClient.start(): started'); 43 | } 44 | 45 | private onTsServerData(data): void { 46 | this.buffer += data; 47 | 48 | while(true) { 49 | if (!this.header) { 50 | this.logger.info('we have not parsed the header yet'); 51 | const headerEndIndex = this.buffer.indexOf('\r\n\r\n'); 52 | if (headerEndIndex >= 0) { 53 | this.logger.info('we have full header'); 54 | this.header = {}; 55 | 56 | this.buffer.substring(0, headerEndIndex) 57 | .split('\r\n') 58 | .forEach(header => { 59 | const kvp = header.split(':').map(values => values.trim()); 60 | if (kvp[0].toUpperCase() === 'CONTENT-LENGTH') { 61 | this.header['Content-Length'] = parseInt(kvp[1]); 62 | } else { 63 | this.header[kvp[0]] = kvp[1]; 64 | } 65 | }); 66 | 67 | this.buffer = this.buffer.substring(headerEndIndex + 4); 68 | } else { 69 | this.logger.info('wait for next buffer to arrive'); 70 | break; 71 | } 72 | } else { 73 | const contentLength = this.header['Content-Length']; 74 | if (this.buffer.length >= contentLength) { 75 | const messageString = this.buffer.substring(0, contentLength); 76 | this.logger.info('we have the full message', this.header, messageString); 77 | const message: protocol.Response = JSON.parse(messageString); 78 | this.buffer = this.buffer.substring(contentLength); 79 | this.header = null; 80 | 81 | this.processMessage(message); 82 | 83 | if (this.buffer.length > 0) { 84 | this.logger.info('we have more data in the buffer so try parsing the new headers from top', this.buffer); 85 | continue 86 | } else { 87 | this.logger.info('we are done processing the message here so stop') 88 | break 89 | } 90 | } else { 91 | this.logger.info('we do not have the entire message body, so wait for the next bufer'); 92 | break; 93 | } 94 | } 95 | } 96 | } 97 | 98 | sendRequest(command: string, notification: boolean, args?: any): Thenable | undefined { 99 | this.seq = this.seq + 1; 100 | 101 | let request: protocol.Request = { 102 | command, 103 | seq: this.seq, 104 | type: 'request' 105 | }; 106 | 107 | if (args) { 108 | request.arguments = args; 109 | } 110 | 111 | const serializedRequest = JSON.stringify(request) + '\n'; 112 | 113 | this.logger.info('---->', serializedRequest); 114 | 115 | if (!this.cp.stdin.write(serializedRequest)) { 116 | this.logger.info('not flushed'); 117 | } 118 | 119 | return notification ? undefined : (this.deferreds[this.seq] = new utils.Deferred()).promise; 120 | } 121 | 122 | private processMessage(message: protocol.Response): void { 123 | this.logger.info(Object.keys(message)); 124 | const deferred = this.deferreds[message.request_seq]; 125 | this.logger.info('has deferred', !!deferred, message.type, message.command, message.request_seq, Object.keys(this.deferreds)); 126 | if (deferred) { 127 | if (message.success) { 128 | this.deferreds[message.request_seq].resolve(message); 129 | } else { 130 | this.deferreds[message.request_seq].reject(message); 131 | } 132 | delete this.deferreds[message.request_seq]; 133 | } 134 | } 135 | 136 | sendOpen(file: string): void { 137 | const args = { file }; 138 | this.logger.info('TsServerClient.sendOpen()', file); 139 | this.sendRequest('open', true, args); 140 | } 141 | 142 | sendClose(file: string): void { 143 | const args = { file }; 144 | this.logger.info('TsServerClient.sendClose()', file); 145 | this.sendRequest('close', true, args); 146 | } 147 | 148 | sendSaveTo(file: string, tmpfile: string): void { 149 | const args = { file, tmpfile }; 150 | this.logger.info('TsServerClient.sendSaveTo()', file, tmpfile); 151 | this.sendRequest('saveto', true, args); 152 | } 153 | 154 | sendReload(file: string, tmpfile: string): void { 155 | const args = { file, tmpfile }; 156 | this.logger.info('TsServerClient.sendReload()', file, tmpfile); 157 | this.sendRequest('reload', true, args); 158 | } 159 | 160 | sendDefinition(file: string, line: number, offset: number): Thenable { 161 | const args = { file, line, offset }; 162 | this.logger.info('TsServerClient.sendDefinition()', args); 163 | return this.sendRequest('definition', false, args); 164 | } 165 | 166 | sendNavTo(searchValue: string, file: string): Thenable { 167 | let args: any = { searchValue, file }; 168 | this.logger.info('TsServerClient.sendNavTo()', args); 169 | return this.sendRequest('navto', false, args); 170 | } 171 | 172 | sendNavTree(file: string): Thenable { 173 | const args = { file }; 174 | this.logger.info('TsServerClient.sendNavTree()', args); 175 | return this.sendRequest('navtree', false, args); 176 | } 177 | 178 | sendCompletions(file: string, line: number, offset: number, prefix: string): Thenable { 179 | const args = { file, line, offset, prefix }; 180 | this.logger.info('TsServerClient.sendCompletions()', args); 181 | return this.sendRequest('completions', false, args); 182 | } 183 | 184 | sendQuickInfo(file: string, line: number, offset: number): Thenable { 185 | const args = { file, line, offset }; 186 | this.logger.info('TsServerClient.sendQuickInfo()', args); 187 | return this.sendRequest('quickinfo', false, args); 188 | } 189 | 190 | sendRename(file: string, line: number, offset: number): Thenable { 191 | const args = { file, line, offset }; 192 | this.logger.info('TsServerClient.sendRename()', args); 193 | return this.sendRequest('rename', false, args); 194 | } 195 | 196 | sendReferences(file: string, line: number, offset: number): Thenable { 197 | const args = { file, line, offset }; 198 | this.logger.info('TsServerClient.sendReferences()', args); 199 | return this.sendRequest('references', false, args); 200 | } 201 | 202 | } 203 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib" : [ "es2016" ], 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "newLine": "LF", 7 | "outDir": "../lib", 8 | "sourceMap": false, 9 | "target": "es2015" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { 4 | Location, 5 | Position, 6 | SymbolKind 7 | } from 'vscode-languageserver'; 8 | 9 | import * as TsProtocol from 'typescript/lib/protocol'; 10 | 11 | import { 12 | CompletionItemKind 13 | } from 'vscode-languageserver'; 14 | 15 | export function isWindows(): boolean { 16 | return /^win/.test(process.platform); 17 | } 18 | 19 | export function uriToPath(uri: string): string { 20 | const p = path.resolve(uri.replace(/file:\/\/\//, '')); 21 | return isWindows() ? p.replace(/\//g, '\\') : p; 22 | } 23 | 24 | export function pathToUri(p: string): string { 25 | return 'file://' + (isWindows() ? '/' + p.replace(/\//g, '/') : p); 26 | } 27 | 28 | export function tsServerLocationToLspPosition(location: TsProtocol.Location): Position { 29 | return { 30 | line: location.line - 1, 31 | character: location.offset - 1 32 | } 33 | } 34 | 35 | export function tsServerFileSpanToLspLocation(fileSpan: TsProtocol.FileSpan): Location { 36 | return { 37 | uri: pathToUri(fileSpan.file), 38 | range: { 39 | start: tsServerLocationToLspPosition(fileSpan.start), 40 | end: tsServerLocationToLspPosition(fileSpan.end) 41 | } 42 | }; 43 | } 44 | 45 | export const completionKindsMapping: { [name: string]: CompletionItemKind } = { 46 | class: CompletionItemKind.Class, 47 | constructor: CompletionItemKind.Constructor, 48 | enum: CompletionItemKind.Enum, 49 | field: CompletionItemKind.Field, 50 | file: CompletionItemKind.File, 51 | function: CompletionItemKind.Function, 52 | interface: CompletionItemKind.Interface, 53 | keyword: CompletionItemKind.Keyword, 54 | method: CompletionItemKind.Method, 55 | module: CompletionItemKind.Module, 56 | property: CompletionItemKind.Property, 57 | reference: CompletionItemKind.Reference, 58 | snippet: CompletionItemKind.Snippet, 59 | text: CompletionItemKind.Text, 60 | unit: CompletionItemKind.Unit, 61 | value: CompletionItemKind.Value, 62 | variable: CompletionItemKind.Variable 63 | }; 64 | 65 | export const symbolKindsMapping: { [name: string]: SymbolKind } = { 66 | 'enum member': SymbolKind.Constant, 67 | 'JSX attribute': SymbolKind.Property, 68 | 'local class': SymbolKind.Class, 69 | 'local function': SymbolKind.Function, 70 | 'local var': SymbolKind.Variable, 71 | 'type parameter': SymbolKind.Variable, 72 | alias: SymbolKind.Variable, 73 | class: SymbolKind.Class, 74 | const: SymbolKind.Constant, 75 | constructor: SymbolKind.Constructor, 76 | enum: SymbolKind.Enum, 77 | field: SymbolKind.Field, 78 | file: SymbolKind.File, 79 | function: SymbolKind.Function, 80 | getter: SymbolKind.Method, 81 | interface: SymbolKind.Interface, 82 | let: SymbolKind.Variable, 83 | method: SymbolKind.Method, 84 | module: SymbolKind.Module, 85 | parameter: SymbolKind.Variable, 86 | property: SymbolKind.Property, 87 | setter: SymbolKind.Method, 88 | var: SymbolKind.Variable 89 | }; 90 | 91 | export class Deferred { 92 | resolve: (value?: T) => void; 93 | reject: (err?: any) => void; 94 | 95 | promise = new Promise((resolve, reject) => { 96 | this.resolve = resolve; 97 | this.reject = reject; 98 | }); 99 | } 100 | --------------------------------------------------------------------------------