├── .gitignore ├── Default.sublime-keymap ├── README.md ├── core ├── __init__.py ├── install.py └── project.py ├── src └── ts │ ├── compilerservice.ts │ └── main.ts ├── test ├── test_code.ts ├── test_code_2.ts └── test_dep.ts ├── typescript.py ├── typescript.sublime-settings └── typescript.tmLanguage /.gitignore: -------------------------------------------------------------------------------- 1 | lib/*.js 2 | src/*.js 3 | core/settings.py 4 | *.pyc 5 | -------------------------------------------------------------------------------- /Default.sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "command": "typescript_complete", 4 | "args": {"characters": "."}, 5 | "keys": ["."], 6 | "context": [ {"key": "typescript"} ] 7 | }, 8 | { 9 | "command": "typescript_complete", 10 | "args": {"characters": ""}, 11 | "keys": ["ctrl+space"], 12 | "context": [ {"key": "typescript"} ] 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WARNING 2 | ------- 3 | 4 | UPDATE : A new project, [T3S](https://github.com/Railk/T3S), has risen, and does all this project does apparently, and more, so give it a try ! 5 | 6 | THIS PLUGIN IS NOT MAINTAINED ANYMORE, AND WILL NOT BE. 7 | IF YOU NEED IMPROVEMENTS/BUG FIXES YOU WILL HAVE TO DO THEM YOURSELF. 8 | IF YOU WANT TO TAKE OWNERSHIP OF THE PROJECT, JUST TELL ME AND I WILL BE HAPPY TO GIVE IT TO YOU. 9 | 10 | Sublime-Typescript 11 | ================== 12 | 13 | A Sublime Text plugin for the Typescript language 14 | 15 | Installation 16 | ------------ 17 | 18 | You need to have node.js installed before anything. 19 | 20 | Clone the repository in your sublime "Packages" directory. 21 | You also need to ensure that the node executable is on your path, or that the "node_path" key is set somewhere in a 22 | typescript.sublime-settings settings file that sublime text can reach. 23 | 24 | ~~~sh 25 | git clone https://github.com/raph-amiard/sublime-typescript 26 | ~~~ 27 | 28 | After that you're set and you can use the plugin ! 29 | First run might take long to set up, and it will need an internet connection, because the plugin is actually : 30 | - Getting the typescript sources online. 31 | - Compiling it's JS part the first time you will use it. 32 | 33 | After that you can use the plugin offline. 34 | If you don't have an internet connection, getting typescript sources and putting them in the lib/typescript directory will work too. 35 | 36 | Usage 37 | ----- 38 | 39 | For the moment the functionnality is very basic : 40 | - Errors get highlighted and the errors messages shows in the status bar 41 | - Autocompletion works (quite well thanks to the TypeScript language service) 42 | 43 | ![Autocompletion feature screenshot](http://i.imgur.com/UR1kn.png) 44 | 45 | ### Settings 46 | 47 | All the settings discussed here can be set either in the typescript.sublime-settings file of the plugin folder, or in your own typescript.sublime-settings, as is usual with sublime text configuration 48 | 49 | #### Node path 50 | 51 | If node isn't on your path, or you want to set the node executable path manually, you can set the "node_path" key to refer to the node executable path, **including the executable name**. 52 | 53 | ~~~json 54 | { 55 | "node_path":"/my/path/to/node/node" 56 | } 57 | ~~~ 58 | 59 | ### Projects 60 | 61 | By default, a new instance of the plugin server is created for every file. 62 | The TypeScript language service has an odd behaviour, as in, every file you add to the service will be considered to 63 | be in the same compilation unit as the others. 64 | If you want to specify to the plugin that some files are part of the same project, put a .sublimets file in the folder. 65 | If you don't do that, every file will be opened in a separate plugin instance 66 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raph-amiard/sublime-typescript/92185c71bcdb551b32e07e334ca4954c82df7c19/core/__init__.py -------------------------------------------------------------------------------- /core/install.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import shutil 3 | import subprocess 4 | from os import path 5 | import os 6 | 7 | REPO_NAME = "sublime-typescript" 8 | PLUGIN_FILE_NAME = "typescript.py" 9 | TYPESCRIPT_SOURCE_LINK = "http://download-codeplex.sec.s-msft.com/Download/SourceControlFileDownload.ashx?ProjectName=typescript&changeSetId=6c2e2c092ba8" 10 | 11 | ts_settings = sublime.load_settings("typescript.sublime-settings") 12 | node_path = "node" 13 | 14 | startupinfo = None 15 | if os.name == 'nt': 16 | startupinfo = subprocess.STARTUPINFO() 17 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 18 | 19 | if ts_settings.has("node_path"): 20 | node_path = ts_settings.get("node_path") 21 | def get_node_path(): 22 | return node_path 23 | 24 | def check_for_node(): 25 | node_path = get_node_path() 26 | try: 27 | subprocess.call([node_path, "--help"], startupinfo = startupinfo) 28 | except Exception, e: 29 | sublime.error_message("The node executable hasn't been found, you might want to set it in your typescript settings by adding the \"node_path\" key") 30 | raise e 31 | 32 | def check_plugin_path(): 33 | if ts_settings.has("plugin_path") and path.isdir(ts_settings.get("plugin_path")): 34 | return 35 | 36 | # Find the plugin path (hackish way because i have no other way) 37 | packages_path = sublime.packages_path() 38 | plugin_path = path.join(packages_path, "sublime-typescript") 39 | if not path.isdir(plugin_path): 40 | plugin_path = None 41 | for d in os.listdir(packages_path): 42 | if path.isdir(d): 43 | for f in os.listdir(d): 44 | if f == PLUGIN_FILE_NAME: 45 | plugin_path = path.join(packages_path, d) 46 | if not plugin_path: 47 | sublime.error_message("Plugin is not in the expected directory") 48 | plugin_path = path.abspath(plugin_path) 49 | 50 | # Write the plugin path into the settings file 51 | ts_settings.set("plugin_path", plugin_path) 52 | sublime.save_settings("typescript.sublime-settings") 53 | 54 | def needs_to_compile_plugin(): 55 | return not path.exists(path.join(ts_settings.get("plugin_path"), "bin/main.js")) 56 | 57 | def compile_plugin(plugin_path): 58 | def plugin_file(f): 59 | return path.join(plugin_path, f) 60 | 61 | # Check if we got typescript in there 62 | typescript_dir = plugin_file("lib/typescript") 63 | 64 | if len(os.listdir(typescript_dir)) == 0: 65 | # We need to get typescript and unzip it 66 | import urllib 67 | import zipfile 68 | zf_path = plugin_file("lib/typescript/ts.zip") 69 | urllib.urlretrieve(TYPESCRIPT_SOURCE_LINK, zf_path) 70 | zipf = zipfile.ZipFile(zf_path) 71 | zipf.extractall(path=plugin_file("lib/typescript/")) 72 | zipf.close() 73 | os.remove(zf_path) 74 | 75 | # Compile the plugin 76 | bindir = plugin_file("bin") 77 | if not path.exists(bindir): 78 | os.makedirs(bindir) 79 | 80 | subprocess.call([get_node_path(), 81 | plugin_file("lib/typescript/bin/tsc.js"), 82 | plugin_file("src/ts/main.ts"), 83 | "--out", plugin_file("bin/main.js")], 84 | startupinfo = startupinfo) 85 | 86 | # Copy needed files to bin directory 87 | shutil.copyfile(plugin_file("lib/typescript/bin/typescript.js"), 88 | plugin_file("bin/typescript.js")) 89 | shutil.copyfile(plugin_file("lib/typescript/bin/typescriptServices.js"), 90 | plugin_file("bin/typescriptServices.js")) 91 | shutil.copyfile(plugin_file("lib/typescript/bin/lib.d.ts"), 92 | plugin_file("bin/lib.d.ts")) 93 | -------------------------------------------------------------------------------- /core/project.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | import os 3 | 4 | def find_project_file(file_path): 5 | a, b = path.split(file_path) 6 | while b != "": 7 | files = os.listdir(a) 8 | if ".sublimets" in files: 9 | print "FOUND project file", path.join(a, ".sublimets"), "for ts file ", file_path 10 | return path.join(a, ".sublimets") 11 | a, b = path.split(a) 12 | 13 | return file_path 14 | -------------------------------------------------------------------------------- /src/ts/compilerservice.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | 6 | declare var it; 7 | declare var describe; 8 | declare var run; 9 | declare var IO: IIO; 10 | declare var __dirname; // Node-specific 11 | 12 | var global = Function("return this").call(null); 13 | 14 | function switchToForwardSlashes(path: string) { 15 | return path.replace(/\\/g, "/"); 16 | } 17 | 18 | function filePath(fullPath: string) { 19 | fullPath = switchToForwardSlashes(fullPath); 20 | var components = fullPath.split("/"); 21 | var path: string[] = components.slice(0, components.length - 1); 22 | return path.join("/") + "/"; 23 | } 24 | 25 | module CService { 26 | 27 | export var userSpecifiedroot = ""; 28 | export function readFile(path: string) { 29 | return IO.readFile(userSpecifiedroot + path); 30 | } 31 | 32 | export module Compiler { 33 | // Aggregate various writes into a single array of lines. Useful for passing to the 34 | // TypeScript compiler to fill with source code or errors. 35 | export class WriterAggregator implements ITextWriter { 36 | public lines: string[] = []; 37 | public currentLine = ""; 38 | 39 | public Write(str) { 40 | this.currentLine += str; 41 | } 42 | 43 | public WriteLine(str) { 44 | this.lines.push(this.currentLine + str); 45 | this.currentLine = ""; 46 | } 47 | 48 | public Close() { 49 | this.lines.push(this.currentLine); 50 | this.currentLine = ""; 51 | } 52 | 53 | public reset() { 54 | this.lines = []; 55 | this.currentLine = ""; 56 | } 57 | } 58 | 59 | var libFolder: string = global['WScript'] ? TypeScript.filePath(global['WScript'].ScriptFullName) : (__dirname + '/'); 60 | export var libText = IO ? IO.readFile(libFolder + "lib.d.ts") : ''; 61 | 62 | var stdout = new WriterAggregator(); 63 | var stderr = new WriterAggregator(); 64 | var currentUnit = 0; 65 | var maxUnit = 0; 66 | 67 | export var compiler: TypeScript.TypeScriptCompiler; 68 | recreate(); 69 | 70 | // Types 71 | export class Type { 72 | constructor (public type, public code, public identifier) { } 73 | 74 | public normalizeToArray(arg: any) { 75 | if ((Array.isArray && Array.isArray(arg)) || arg instanceof Array) 76 | return arg; 77 | 78 | return [arg]; 79 | } 80 | 81 | public compilesOk(testCode): bool { 82 | var errors = null; 83 | compileString(testCode, 'test.ts', function (compilerResult) { 84 | errors = compilerResult.errors; 85 | }) 86 | 87 | return errors.length === 0; 88 | } 89 | 90 | public isSubtypeOf(other: Type) { 91 | var testCode = 'class __test1__ {\n'; 92 | testCode += ' public test() {\n'; 93 | testCode += ' ' + other.code + ';\n'; 94 | testCode += ' return ' + other.identifier + ';\n'; 95 | testCode += ' }\n'; 96 | testCode += '}\n'; 97 | testCode += 'class __test2__ extends __test1__ {\n'; 98 | testCode += ' public test() {\n'; 99 | testCode += ' ' + this.code + ';\n'; 100 | testCode += ' return ' + other.identifier + ';\n'; 101 | testCode += ' }\n'; 102 | testCode += '}\n'; 103 | 104 | return this.compilesOk(testCode); 105 | } 106 | 107 | // TODO: Find an implementation of isIdenticalTo that works. 108 | public isIdenticalTo(other: Type) { 109 | var testCode = 'module __test1__ {\n'; 110 | testCode += ' ' + this.code + ';\n'; 111 | testCode += ' export var __val__ = ' + this.identifier + ';\n'; 112 | testCode += '}\n'; 113 | testCode += 'var __test1__val__ = __test1__.__val__;\n'; 114 | 115 | testCode += 'module __test2__ {\n'; 116 | testCode += ' ' + other.code + ';\n'; 117 | testCode += ' export var __val__ = ' + other.identifier + ';\n'; 118 | testCode += '}\n'; 119 | testCode += 'var __test2__val__ = __test2__.__val__;\n'; 120 | 121 | testCode += 'function __test__function__() { if(true) { return __test1__val__ }; return __test2__val__; }'; 122 | 123 | return this.compilesOk(testCode); 124 | } 125 | 126 | public assertSubtypeOf(others: any) { 127 | others = this.normalizeToArray(others); 128 | 129 | for (var i = 0; i < others.length; i++) { 130 | if (!this.isSubtypeOf(others[i])) { 131 | throw new Error("Expected " + this.type + " to be a subtype of " + others[i].type); 132 | } 133 | } 134 | } 135 | 136 | public assertNotSubtypeOf(others: any) { 137 | others = this.normalizeToArray(others); 138 | 139 | for (var i = 0; i < others.length; i++) { 140 | if (this.isSubtypeOf(others[i])) { 141 | throw new Error("Expected " + this.type + " to be a subtype of " + others[i].type); 142 | } 143 | } 144 | } 145 | 146 | public assertIdenticalTo(other: Type) { 147 | if (!this.isIdenticalTo(other)) { 148 | throw new Error("Expected " + this.type + " to be identical to " + other.type); 149 | } 150 | } 151 | 152 | public assertNotIdenticalTo(other: Type) { 153 | if (!this.isIdenticalTo(other)) { 154 | throw new Error("Expected " + this.type + " to not be identical to " + other.type); 155 | } 156 | } 157 | 158 | public isAssignmentCompatibleWith(other: Type) { 159 | var testCode = 'module __test1__ {\n'; 160 | testCode += ' ' + this.code + ';\n'; 161 | testCode += ' export var __val__ = ' + this.identifier + ';\n'; 162 | testCode += '}\n'; 163 | testCode += 'var __test1__val__ = __test1__.__val__;\n'; 164 | 165 | testCode += 'module __test2__ {\n'; 166 | testCode += ' export ' + other.code + ';\n'; 167 | testCode += ' export var __val__ = ' + other.identifier + ';\n'; 168 | testCode += '}\n'; 169 | testCode += 'var __test2__val__ = __test2__.__val__;\n'; 170 | 171 | testCode += '__test2__val__ = __test1__val__;'; 172 | 173 | return this.compilesOk(testCode); 174 | } 175 | 176 | public assertAssignmentCompatibleWith(others: any) { 177 | others = this.normalizeToArray(others); 178 | 179 | for (var i = 0; i < others.length; i++) { 180 | var other = others[i]; 181 | 182 | if (!this.isAssignmentCompatibleWith(other)) { 183 | throw new Error("Expected " + this.type + " to be assignment compatible with " + other.type); 184 | } 185 | } 186 | } 187 | 188 | public assertNotAssignmentCompatibleWith(others: any) { 189 | others = this.normalizeToArray(others); 190 | 191 | for (var i = 0; i < others.length; i++) { 192 | var other = others[i]; 193 | 194 | if (this.isAssignmentCompatibleWith(other)) { 195 | throw new Error("Expected " + this.type + " to not be assignment compatible with " + other.type); 196 | } 197 | } 198 | } 199 | } 200 | 201 | export class TypeFactory { 202 | public any: Type; 203 | public number: Type; 204 | public string: Type; 205 | public bool: Type; 206 | 207 | constructor () { 208 | this.any = this.get('var x : any', 'x'); 209 | this.number = this.get('var x : number', 'x'); 210 | this.string = this.get('var x : string', 'x'); 211 | this.bool = this.get('var x : bool', 'x'); 212 | } 213 | 214 | public get(code: string, identifier: string) { 215 | var errors = null; 216 | compileString(code, 'test.ts', function (compilerResult) { 217 | errors = compilerResult.errors; 218 | }) 219 | 220 | if (errors.length > 0) 221 | throw new Error("Type definition contains errors: " + errors.join(",")); 222 | 223 | // REVIEW: For a multi-file test, this won't work 224 | var script = compiler.scripts.members[1]; 225 | var enclosingScopeContext = TypeScript.findEnclosingScopeAt(new TypeScript.NullLogger(), script, new TypeScript.StringSourceText(code), 0, false); 226 | var entries = new TypeScript.ScopeTraversal(compiler).getScopeEntries(enclosingScopeContext); 227 | for (var i = 0; i < entries.length; i++) { 228 | if (entries[i].name === identifier) { 229 | return new Type(entries[i].type, code, identifier); 230 | } 231 | } 232 | } 233 | 234 | } 235 | 236 | export function generateDeclFile(code: string, verifyNoDeclFile: bool): string { 237 | reset(); 238 | 239 | compiler.settings.generateDeclarationFiles = true; 240 | var oldOutputMany = compiler.settings.outputMany; 241 | try { 242 | addUnit(code); 243 | compiler.reTypeCheck(); 244 | 245 | var outputs = {}; 246 | 247 | compiler.settings.outputMany = true; 248 | compiler.emitDeclarationFile((fn: string) => { 249 | outputs[fn] = new Compiler.WriterAggregator(); 250 | return outputs[fn]; 251 | }); 252 | 253 | for (var fn in outputs) { 254 | if (fn.indexOf('.d.ts') >= 0) { 255 | var writer = outputs[fn]; 256 | writer.Close(); 257 | if (verifyNoDeclFile) { 258 | throw new Error('Compilation should not produce ' + fn); 259 | } 260 | return writer.lines.join('\n'); 261 | } 262 | } 263 | 264 | if (!verifyNoDeclFile) { 265 | throw new Error('Compilation did not produced .d.ts files'); 266 | } 267 | } finally { 268 | compiler.settings.generateDeclarationFiles = false; 269 | compiler.settings.outputMany = oldOutputMany; 270 | } 271 | 272 | return ''; 273 | } 274 | 275 | // Contains the code and errors of a compilation and some helper methods to check its status. 276 | export class CompilerResult { 277 | public code: string; 278 | public errors: CompilerError[]; 279 | 280 | constructor (codeLines: string[], errorLines: string[], public scripts: TypeScript.Script[]) { 281 | this.code = codeLines.join("\n") 282 | this.errors = []; 283 | 284 | for (var i = 0; i < errorLines.length; i++) { 285 | var match = errorLines[i].match(/([^\(]*)\((\d+),(\d+)\):\s+((.*[\s\r\n]*.*)+)\s*$/); 286 | if (match) { 287 | this.errors.push(new CompilerError(match[1], parseFloat(match[2]), parseFloat(match[3]), match[4])); 288 | } 289 | else { 290 | WScript.Echo("non-match on: " + errorLines[i]); 291 | } 292 | } 293 | } 294 | 295 | public isErrorAt(line: number, column: number, message: string) { 296 | for (var i = 0; i < this.errors.length; i++) { 297 | if (this.errors[i].line === line && this.errors[i].column === column && this.errors[i].message === message) 298 | return true; 299 | } 300 | 301 | return false; 302 | } 303 | } 304 | 305 | // Compiler Error. 306 | export class CompilerError { 307 | constructor (public file: string, 308 | public line: number, 309 | public column: number, 310 | public message: string) { } 311 | 312 | public toString() { 313 | return this.file + "(" + this.line + "," + this.column + "): " + this.message; 314 | } 315 | } 316 | 317 | export function recreate() { 318 | compiler = new TypeScript.TypeScriptCompiler(stderr); 319 | compiler.parser.errorRecovery = true; 320 | compiler.settings.codeGenTarget = TypeScript.CodeGenTarget.ES5; 321 | compiler.settings.controlFlow = true; 322 | compiler.settings.controlFlowUseDef = true; 323 | TypeScript.moduleGenTarget = TypeScript.ModuleGenTarget.Synchronous; 324 | compiler.addUnit(libText, 'lib.d.ts', true); 325 | compiler.typeCheck(); 326 | currentUnit = 0; 327 | maxUnit = 0; 328 | } 329 | export function reset() { 330 | stdout.reset(); 331 | stderr.reset(); 332 | 333 | for (var i = 0; i < currentUnit; i++) { 334 | compiler.updateUnit('', i + '.ts', false/*setRecovery*/); 335 | } 336 | 337 | compiler.errorReporter.hasErrors = false; 338 | currentUnit = 0; 339 | } 340 | 341 | export function addUnit(code: string, isResident?: bool, isDeclareFile?: bool) { 342 | var script: TypeScript.Script = null; 343 | if (currentUnit >= maxUnit) { 344 | script = compiler.addUnit(code, currentUnit++ + (isDeclareFile ? '.d.ts' : '.ts'), isResident); 345 | maxUnit++; 346 | } else { 347 | var filename = currentUnit + (isDeclareFile ? '.d.ts' : '.ts'); 348 | compiler.updateUnit(code, filename, false/*setRecovery*/); 349 | 350 | for (var i = 0; i < compiler.units.length; i++) { 351 | if (compiler.units[i].filename === filename) 352 | script = compiler.scripts.members[i]; 353 | } 354 | 355 | currentUnit++; 356 | } 357 | 358 | return script; 359 | } 360 | 361 | export function compileUnit(path: string, callback: (res: CompilerResult) => void , settingsCallback?: () => void ) { 362 | if (settingsCallback) { 363 | settingsCallback(); 364 | } 365 | path = switchToForwardSlashes(path); 366 | compileString(readFile(path), path.match(/[^\/]*$/)[0], callback); 367 | } 368 | export function compileUnits(callback: (res: Compiler.CompilerResult) => void, settingsCallback?: () => void ) { 369 | reset(); 370 | if (settingsCallback) { 371 | settingsCallback(); 372 | } 373 | 374 | compiler.reTypeCheck(); 375 | compiler.emitToOutfile(stdout); 376 | 377 | callback(new CompilerResult(stdout.lines, stderr.lines, [])); 378 | 379 | recreate(); 380 | reset(); 381 | } 382 | export function compileString(code: string, unitName: string, callback: (res: Compiler.CompilerResult) => void , refreshUnitsForLSTests? = false) { 383 | var scripts: TypeScript.Script[] = []; 384 | 385 | // TODO: How to overload? 386 | if (typeof unitName === 'function') { 387 | callback = <(res: CompilerResult) => void >(unitName); 388 | unitName = 'test.ts'; 389 | } 390 | 391 | reset(); 392 | 393 | // Some command-line tests may pollute the global namespace, which could interfere with 394 | // with language service testing. 395 | // In the case of LS tests, make sure that we refresh the first unit, and not try to update it 396 | if (refreshUnitsForLSTests) { 397 | maxUnit = 0; 398 | } 399 | 400 | scripts.push(addUnit(code)); 401 | compiler.reTypeCheck(); 402 | compiler.emitToOutfile(stdout); 403 | 404 | callback(new CompilerResult(stdout.lines, stderr.lines, scripts)); 405 | } 406 | } 407 | 408 | export class ScriptInfo { 409 | public version: number; 410 | public editRanges: { length: number; editRange: TypeScript.ScriptEditRange; }[] = []; 411 | 412 | constructor (public name: string, public content: string, public isResident: bool, public maxScriptVersions: number) { 413 | this.version = 1; 414 | } 415 | 416 | public updateContent(content: string, isResident: bool) { 417 | this.editRanges = []; 418 | this.content = content; 419 | this.isResident = isResident; 420 | this.version++; 421 | } 422 | 423 | public editContent(minChar: number, limChar: number, newText: string) { 424 | // Apply edits 425 | var prefix = this.content.substring(0, minChar); 426 | var middle = newText; 427 | var suffix = this.content.substring(limChar); 428 | this.content = prefix + middle + suffix; 429 | 430 | // Store edit range + new length of script 431 | this.editRanges.push({ 432 | length: this.content.length, 433 | editRange: new TypeScript.ScriptEditRange(minChar, limChar, (limChar - minChar) + newText.length) 434 | }); 435 | 436 | if (this.editRanges.length > this.maxScriptVersions) { 437 | this.editRanges.splice(0, this.maxScriptVersions - this.editRanges.length); 438 | } 439 | 440 | // Update version # 441 | this.version++; 442 | } 443 | 444 | public getEditRangeSinceVersion(version: number): TypeScript.ScriptEditRange { 445 | if (this.version == version) { 446 | // No edits! 447 | return null; 448 | } 449 | 450 | var initialEditRangeIndex = this.editRanges.length - (this.version - version); 451 | if (initialEditRangeIndex < 0 || initialEditRangeIndex >= this.editRanges.length) { 452 | // Too far away from what we know 453 | return TypeScript.ScriptEditRange.unknown(); 454 | } 455 | 456 | var entries = this.editRanges.slice(initialEditRangeIndex); 457 | 458 | var minDistFromStart = entries.map(x => x.editRange.minChar).reduce((prev, current) => Math.min(prev, current)); 459 | var minDistFromEnd = entries.map(x => x.length - x.editRange.limChar).reduce((prev, current) => Math.min(prev, current)); 460 | var aggDelta = entries.map(x => x.editRange.delta).reduce((prev, current) => prev + current); 461 | 462 | return new TypeScript.ScriptEditRange(minDistFromStart, entries[0].length - minDistFromEnd, aggDelta); 463 | } 464 | } 465 | 466 | export class TypeScriptLS implements Services.ILanguageServiceShimHost { 467 | private ls: Services.ILanguageServiceShim = null; 468 | 469 | public scripts: ScriptInfo[] = []; 470 | public maxScriptVersions = 100; 471 | 472 | public addDefaultLibrary() { 473 | this.addScript("lib.d.ts", Compiler.libText, true); 474 | } 475 | 476 | public addFile(name: string, isResident = false) { 477 | var code: string = readFile(name); 478 | this.addScript(name, code, isResident); 479 | } 480 | 481 | public addScript(name: string, content: string, isResident = false) { 482 | var script = new ScriptInfo(name, content, isResident, this.maxScriptVersions); 483 | this.scripts.push(script); 484 | } 485 | 486 | public updateScript(name: string, content: string, isResident = false) { 487 | for (var i = 0; i < this.scripts.length; i++) { 488 | if (this.scripts[i].name == name) { 489 | this.scripts[i].updateContent(content, isResident); 490 | return; 491 | } 492 | } 493 | 494 | this.addScript(name, content, isResident); 495 | } 496 | 497 | public editScript(name: string, minChar: number, limChar: number, newText: string) { 498 | for (var i = 0; i < this.scripts.length; i++) { 499 | if (this.scripts[i].name == name) { 500 | this.scripts[i].editContent(minChar, limChar, newText); 501 | return; 502 | } 503 | } 504 | 505 | throw new Error("No script with name '" + name + "'"); 506 | } 507 | 508 | public getScriptContent(scriptIndex: number): string { 509 | return this.scripts[scriptIndex].content; 510 | } 511 | 512 | ////////////////////////////////////////////////////////////////////// 513 | // ILogger implementation 514 | // 515 | public information(): bool { return false; } 516 | public debug(): bool { return true; } 517 | public warning(): bool { return true; } 518 | public error(): bool { return true; } 519 | public fatal(): bool { return true; } 520 | 521 | public log(s: string): void { 522 | // For debugging... 523 | //IO.printLine("TypeScriptLS:" + s); 524 | } 525 | 526 | ////////////////////////////////////////////////////////////////////// 527 | // ILanguageServiceShimHost implementation 528 | // 529 | 530 | public getCompilationSettings(): string/*json for Tools.CompilationSettings*/ { 531 | return ""; // i.e. default settings 532 | } 533 | 534 | public getScriptCount(): number { 535 | return this.scripts.length; 536 | } 537 | 538 | public getScriptSourceText(scriptIndex: number, start: number, end: number): string { 539 | return this.scripts[scriptIndex].content.substring(start, end); 540 | } 541 | 542 | public getScriptSourceLength(scriptIndex: number): number { 543 | return this.scripts[scriptIndex].content.length; 544 | } 545 | 546 | public getScriptId(scriptIndex: number): string { 547 | return this.scripts[scriptIndex].name; 548 | } 549 | 550 | public getScriptIsResident(scriptIndex: number): bool { 551 | return this.scripts[scriptIndex].isResident; 552 | } 553 | 554 | public getScriptVersion(scriptIndex: number): number { 555 | return this.scripts[scriptIndex].version; 556 | } 557 | 558 | public getScriptEditRangeSinceVersion(scriptIndex: number, scriptVersion: number): string { 559 | var range = this.scripts[scriptIndex].getEditRangeSinceVersion(scriptVersion); 560 | var result = (range.minChar + "," + range.limChar + "," + range.delta); 561 | return result; 562 | } 563 | 564 | // 565 | // Return a new instance of the language service shim, up-to-date wrt to typecheck. 566 | // To access the non-shim (i.e. actual) language service, use the "ls.languageService" property. 567 | // 568 | public getLanguageService(): Services.ILanguageServiceShim { 569 | var ls = new Services.TypeScriptServicesFactory().createLanguageServiceShim(this); 570 | ls.refresh(true); 571 | this.ls = ls; 572 | return ls; 573 | } 574 | 575 | // 576 | // Parse file given its source text 577 | // 578 | public parseSourceText(fileName: string, sourceText: TypeScript.ISourceText): TypeScript.Script { 579 | var parser = new TypeScript.Parser(); 580 | parser.setErrorRecovery(null); 581 | parser.errorCallback = (a, b, c, d) => { }; 582 | 583 | var script = parser.parse(sourceText, fileName, 0); 584 | return script; 585 | } 586 | 587 | // 588 | // Parse a file on disk given its filename 589 | // 590 | public parseFile(fileName: string) { 591 | var sourceText = new TypeScript.StringSourceText(IO.readFile(fileName)) 592 | return this.parseSourceText(fileName, sourceText); 593 | } 594 | 595 | // 596 | // line and column are 1-based 597 | // 598 | public lineColToPosition(fileName: string, line: number, col: number): number { 599 | var script = this.ls.languageService.getScriptAST(fileName); 600 | //assert.notNull(script); 601 | //assert.is(line >= 1); 602 | //assert.is(col >= 1); 603 | //assert.is(line < script.locationInfo.lineMap.length); 604 | 605 | return TypeScript.getPositionFromLineColumn(script, line, col); 606 | } 607 | 608 | // 609 | // line and column are 1-based 610 | // 611 | public positionToLineCol(fileName: string, position: number): TypeScript.ILineCol { 612 | var script = this.ls.languageService.getScriptAST(fileName); 613 | //assert.notNull(script); 614 | 615 | var result = TypeScript.getLineColumnFromPosition(script, position); 616 | 617 | //assert.is(result.line >= 1); 618 | //assert.is(result.col >= 1); 619 | return result; 620 | } 621 | 622 | // 623 | // Verify that applying edits to "sourceFileName" result in the content of the file 624 | // "baselineFileName" 625 | // 626 | public checkEdits(sourceFileName: string, baselineFileName: string, edits: Services.TextEdit[]) { 627 | var script = readFile(sourceFileName); 628 | var formattedScript = this.applyEdits(script, edits); 629 | var baseline = readFile(baselineFileName); 630 | 631 | //assert.noDiff(formattedScript, baseline); 632 | //assert.equal(formattedScript, baseline); 633 | } 634 | 635 | 636 | // 637 | // Apply an array of text edits to a string, and return the resulting string. 638 | // 639 | public applyEdits(content: string, edits: Services.TextEdit[]): string { 640 | var result = content; 641 | edits = this.normalizeEdits(edits); 642 | 643 | for (var i = edits.length - 1; i >= 0; i--) { 644 | var edit = edits[i]; 645 | var prefix = result.substring(0, edit.minChar); 646 | var middle = edit.text; 647 | var suffix = result.substring(edit.limChar); 648 | result = prefix + middle + suffix; 649 | } 650 | return result; 651 | } 652 | 653 | // 654 | // Normalize an array of edits by removing overlapping entries and sorting 655 | // entries on the "minChar" position. 656 | // 657 | private normalizeEdits(edits: Services.TextEdit[]): Services.TextEdit[] { 658 | var result: Services.TextEdit[] = []; 659 | 660 | function mapEdits(edits: Services.TextEdit[]): { edit: Services.TextEdit; index: number; }[] { 661 | var result = []; 662 | for (var i = 0; i < edits.length; i++) { 663 | result.push({ edit: edits[i], index: i }); 664 | } 665 | return result; 666 | } 667 | 668 | var temp = mapEdits(edits).sort(function (a, b) { 669 | var result = a.edit.minChar - b.edit.minChar; 670 | if (result == 0) 671 | result = a.index - b.index; 672 | return result; 673 | }); 674 | 675 | var current = 0; 676 | var next = 1; 677 | while (current < temp.length) { 678 | var currentEdit = temp[current].edit; 679 | 680 | // Last edit 681 | if (next >= temp.length) { 682 | result.push(currentEdit); 683 | current++; 684 | continue; 685 | } 686 | var nextEdit = temp[next].edit; 687 | 688 | var gap = nextEdit.minChar - currentEdit.limChar; 689 | 690 | // non-overlapping edits 691 | if (gap >= 0) { 692 | result.push(currentEdit); 693 | current = next; 694 | next++; 695 | continue; 696 | } 697 | 698 | // overlapping edits: for now, we only support ignoring an next edit 699 | // entirely contained in the current edit. 700 | if (currentEdit.limChar >= nextEdit.limChar) { 701 | next++; 702 | continue; 703 | } 704 | else { 705 | throw new Error("Trying to apply overlapping edits"); 706 | } 707 | } 708 | 709 | return result; 710 | } 711 | 712 | } 713 | } 714 | -------------------------------------------------------------------------------- /src/ts/main.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | var readline = require('readline'); 5 | 6 | /* 7 | var typescriptLS = new CService.TypeScriptLS(); 8 | var file_name = 'bin/test_code.ts'; 9 | typescriptLS.addFile(file_name); 10 | var ls = typescriptLS.getLanguageService(); 11 | 12 | var pos = lineColToPosition(file_name,11, 3) 13 | var result = ls.languageService.getCompletionsAtPosition(file_name, pos, true); 14 | console.log(result); 15 | 16 | pos = lineColToPosition(file_name,36, 9) 17 | result = ls.languageService.getCompletionsAtPosition(file_name, pos, true); 18 | console.log(result); 19 | 20 | console.log("HO HAI"); 21 | */ 22 | 23 | var ts_ls : CService.TypeScriptLS = new CService.TypeScriptLS(); 24 | var ls : Services.ILanguageServiceShim; 25 | 26 | function lineColToPosition(fileName: string, line: number, col: number): number { 27 | var script = ls.languageService.getScriptAST(fileName); 28 | var lineMap = script.locationInfo.lineMap; 29 | var offset = lineMap[line] + (col - 1); 30 | return offset; 31 | } 32 | 33 | function repl(prompt : string, callback : (string) => void) { 34 | var rl = readline.createInterface(process.stdin, process.stdout); 35 | rl.setPrompt(prompt); 36 | rl.prompt(); 37 | rl.on('line', function (line) { callback(line); rl.prompt(); }); 38 | } 39 | 40 | 41 | var repl_actions = { 42 | // Set the root of the project 43 | // root_path : the root path relative to which 44 | // file paths will be resolved 45 | "set_root" : function (root_path) { 46 | CService.userSpecifiedroot = root_path; 47 | return {status: "OK"}; 48 | }, 49 | 50 | // Add a file to the list of tracked scripts 51 | // file_path : the path of the file to complete 52 | "add_file" : function (file_path) { 53 | ts_ls.addFile(file_path); 54 | ls = ts_ls.getLanguageService(); 55 | return {status: "OK"}; 56 | }, 57 | 58 | // Initiate a completion request 59 | // file_path : the path of the file to complete 60 | // pos : either a number (absolute pos in the file), or a couple [line, col] 61 | // is_member : wether the completion is a member completion (eg a.***) 62 | "complete" : function (file_path, pos, is_member) { 63 | var ipos : number = (pos instanceof Array) ? 64 | lineColToPosition(file_path, pos[0], pos[1]) : pos; 65 | 66 | return {status: "OK", 67 | result: ls.languageService 68 | .getCompletionsAtPosition(file_path, ipos, is_member)}; 69 | }, 70 | 71 | "edit_script": function (file_path, min_char, lim_char, new_text) { 72 | ts_ls.editScript(file_path, min_char, lim_char, new_text); 73 | return {status: "OK"}; 74 | }, 75 | 76 | "update_script": function (file_path, content) { 77 | ts_ls.updateScript(file_path, content); 78 | return {status: "OK"}; 79 | }, 80 | 81 | "get_errors": function(file_path) { 82 | return {status: "OK", 83 | result: ls.languageService 84 | .getScriptErrors(file_path, 100)}; 85 | }, 86 | 87 | "dummy": function () { 88 | return {status: "OK", data:"dummy"}; 89 | } 90 | } 91 | 92 | repl("", (line) => { 93 | var json_data = JSON.parse(line); 94 | var result; 95 | try { 96 | result = repl_actions[json_data[0]].apply(null, json_data.slice(1)); 97 | console.error(result); 98 | } catch (err) { 99 | console.error(err); 100 | result = {status: "ERROR", error:err}; 101 | } 102 | console.log(JSON.stringify(result)); 103 | }) 104 | -------------------------------------------------------------------------------- /test/test_code.ts: -------------------------------------------------------------------------------- 1 | declare var document; 2 | declare var alert; 3 | module Foo { 4 | var testing = ""; 5 | } 6 | 7 | class C1 { 8 | public pubMeth() {this.pubMeth();} // test on 'this.' 9 | private privMeth() {} 10 | public pubProp = 0; 11 | private privProp = 0; 12 | public testMeth() { 13 | this.pubMeth() 14 | return this; 15 | } 16 | } 17 | 18 | var f = new C1(); 19 | f.pubMeth(); // test on F. 20 | 21 | module M { 22 | export class C { 23 | public pub = 0; 24 | private priv = 1; 25 | public test = 123; 26 | } 27 | export var V = 0; 28 | } 29 | 30 | 31 | var c = new M.C(); 32 | c.test; 33 | 34 | class Greeter { 35 | greeting: string; 36 | constructor (message: string) { 37 | this.greeting = message; 38 | } 39 | greet() { 40 | return "Hello, " + this.greeting; 41 | } 42 | 43 | public test(a : string, b : string) : string { 44 | return a + " " + b; 45 | } 46 | } 47 | 48 | var greeter = new Greeter("world"); 49 | greeter.greet() // test on greeter. 50 | greeter.test("lol", "hh"); 51 | var gr2 : Greeter = new Greeter("haha"); 52 | 53 | var button = document.createElement('button') 54 | button.innerText = "Say Hello" 55 | button.onclick = function() { 56 | alert(greeter.greet()) 57 | } 58 | 59 | document.body.appendChild(button) 60 | -------------------------------------------------------------------------------- /test/test_code_2.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare var console; 4 | 5 | class Test { 6 | public test () { 7 | console.log("oh hai"); 8 | } 9 | public test_2(a : number, b : number) : number { 10 | return a + b; 11 | } 12 | } 13 | 14 | var v : C1 = new C1(); 15 | var a : number = v.testMeth2(12, 15); 16 | var t : Test = new Test(); 17 | v.pubMeth(); 18 | var v2 : C1 = new C1(); 19 | -------------------------------------------------------------------------------- /test/test_dep.ts: -------------------------------------------------------------------------------- 1 | 2 | class C1 { 3 | public pubMeth() {this.pubMeth();} // test on 'this.' 4 | private privMeth() {} 5 | public pubProp = 0; 6 | private privProp = 0; 7 | public testMeth() { 8 | this.pubMeth() 9 | return this; 10 | } 11 | public testMeth2(a : number, b : number) { 12 | return a - b; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /typescript.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sublime, sublime_plugin 4 | from subprocess import Popen, PIPE 5 | import subprocess 6 | import json 7 | from os import path 8 | import os 9 | from threading import Thread, RLock, Semaphore 10 | from time import sleep, time 11 | import re 12 | import difflib 13 | from core import project, install 14 | from itertools import cycle, chain 15 | from collections import defaultdict 16 | 17 | class AtomicValue: 18 | def __init__(self): 19 | self.val = 0 20 | self.lock = RLock() 21 | 22 | def inc(self): 23 | self.lock.acquire() 24 | self.val += 1 25 | self.lock.release() 26 | 27 | def dec(self): 28 | self.lock.acquire() 29 | self.val -= 1 30 | self.lock.release() 31 | 32 | loading_files = AtomicValue() 33 | 34 | ts_settings = sublime.load_settings("typescript.sublime-settings") 35 | 36 | install.check_for_node() 37 | do_compile = install.check_plugin_path() 38 | plugin_path = ts_settings.get("plugin_path") 39 | def install_helper(): 40 | loading_files.inc() 41 | install.compile_plugin(plugin_path) 42 | loading_files.dec() 43 | thread_install = None 44 | if install.needs_to_compile_plugin(): 45 | thread_install = Thread(target=install_helper) 46 | thread_install.start() 47 | 48 | # ========================== GENERAL HELPERS ======================= # 49 | 50 | # === Helpers for async API calls 51 | global async_api_result 52 | async_api_result = None 53 | async_call_lock = Semaphore() 54 | def async_api_call(func, *args, **kwargs): 55 | 56 | def timeout_func(): 57 | global async_api_result 58 | async_api_result = func(*args, **kwargs) 59 | async_call_lock.release() 60 | 61 | async_call_lock.acquire() 62 | sublime.set_timeout(timeout_func, 0) 63 | async_call_lock.acquire() 64 | async_call_lock.release() 65 | return async_api_result 66 | 67 | def is_ts(view): 68 | return view.file_name() and view.file_name().endswith(".ts") 69 | 70 | def get_all_text(view): 71 | return view.substr(sublime.Region(0, view.size())) 72 | 73 | def get_file_view(filename): 74 | for w in sublime.windows(): 75 | for v in w.views(): 76 | if v.file_name() == filename: 77 | return v 78 | return None 79 | 80 | def get_dep_text(filename): 81 | view = get_file_view(filename) 82 | if view: 83 | return get_all_text(view) 84 | else: 85 | f = open(filename) 86 | ct = f.read() 87 | f.close() 88 | return ct 89 | 90 | def format_diffs(old_content, new_content): 91 | seqmatcher = difflib.SequenceMatcher(None, old_content, new_content) 92 | return [(oc[1], oc[2], new_content[oc[3]:oc[4]] if oc[0] in ['insert', 'replace'] else "") 93 | for oc in seqmatcher.get_opcodes() 94 | if oc[0] in ['insert', 'delete', 'replace']] 95 | 96 | prefixes = { 97 | "method": u"◉", 98 | "property": u"●", 99 | "class":u"◆", 100 | "interface":u"◇", 101 | "keyword":u"∆", 102 | "variable": u"∨", 103 | } 104 | 105 | js_id_re = re.compile( 106 | ur'^[_$a-zA-Z\u00FF-\uFFFF][_$a-zA-Z0-9\u00FF-\uFFFF]*' 107 | ) 108 | 109 | def is_member_completion(line): 110 | def partial_completion(): 111 | sp = line.split(".") 112 | if len(sp) > 1: 113 | return js_id_re.match(sp[-1]) is not None 114 | return False 115 | return line.endswith(".") or partial_completion() 116 | 117 | def format_completion_entry(c_entry): 118 | prefix = prefixes.get(c_entry["kind"], u"-") 119 | prefix += " " 120 | middle = c_entry["name"] 121 | suffix = "\t" + c_entry["type"] 122 | return prefix + middle + suffix 123 | 124 | def partition_by(lst, disc): 125 | partitions = defaultdict(list) 126 | for el in lst: 127 | partitions[disc(el)].append(el) 128 | return partitions.values() 129 | 130 | def sort_completions(entries): 131 | return [(format_completion_entry(item), item["name"]) 132 | for sublist in partition_by(entries, lambda entry: entry["kind"]) 133 | for item in sorted(sublist, key=lambda entry: entry["name"])] 134 | 135 | def completions_ts_to_sublime(json_completions): 136 | return sort_completions(json_completions["entries"]) 137 | 138 | def ts_errors_to_regions(ts_errors): 139 | return [sublime.Region(e["minChar"], e["limChar"]) for e in ts_errors] 140 | 141 | def text_from_diff(old_content, minChar, limChar, new_text): 142 | prefix = old_content[0:minChar] 143 | suffix = old_content[limChar:] 144 | return (prefix + new_text + suffix) 145 | 146 | def get_pos(view): 147 | return view.sel()[0].begin() 148 | 149 | def get_plugin_path(): 150 | return plugin_path 151 | 152 | def plugin_file(file_path): 153 | return path.join(get_plugin_path(), file_path) 154 | 155 | node_path = "node" 156 | if ts_settings.has("node_path"): 157 | node_path = ts_settings.get("node_path") 158 | 159 | def get_node_path(): 160 | return node_path 161 | 162 | # ================ SERVER AND COMMUNICATION HELPERS =============== # 163 | 164 | class PluginInstance(object): 165 | def __init__(self): 166 | print "PLUGIN_FILE ", plugin_file("bin/main.js") 167 | self.open_files = set() 168 | self.views_text = {} 169 | self.errors_intervals = {} 170 | self.init_sem = Semaphore() 171 | 172 | def init_async(): 173 | if thread_install: 174 | thread_install.join() 175 | loading_files.inc() 176 | kwargs = {} 177 | errorlog = None 178 | if os.name == 'nt': 179 | errorlog = open(os.devnull, 'w') 180 | startupinfo = subprocess.STARTUPINFO() 181 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 182 | kwargs = {"stderr":errorlog, "startupinfo":startupinfo} 183 | self.p = Popen([get_node_path(), plugin_file("bin/main.js")], stdin=PIPE, stdout=PIPE, **kwargs) 184 | 185 | if errorlog: 186 | errorlog.close() 187 | 188 | self.serv_add_file(plugin_file("bin/lib.d.ts")) 189 | loading_files.dec() 190 | print "OUT OF INIT ASYNC" 191 | self.init_sem.release() 192 | 193 | self.init_sem.acquire() 194 | Thread(target=init_async).start() 195 | 196 | def close_process(self): 197 | self.p.terminate() 198 | 199 | def msg(self, *args): 200 | res = None 201 | message = json.dumps(args) + "\n" 202 | t = time() 203 | self.p.stdin.write(message) 204 | msg_content = self.p.stdout.readline() 205 | res = json.loads(msg_content) 206 | return res 207 | 208 | def serv_add_file(self, file_name): 209 | resp = self.msg("add_file", file_name) 210 | 211 | def serv_update_file(self, file_name, content): 212 | resp = self.msg("update_script", file_name, content) 213 | 214 | def serv_edit_file(self, file_name, min_char, new_char, new_text): 215 | resp = self.msg("edit_script", file_name, min_char, new_char, new_text) 216 | 217 | def serv_get_completions(self, file_name, pos, is_member): 218 | resp = self.msg("complete", file_name, pos, is_member) 219 | return resp["result"] 220 | 221 | def serv_get_errors(self, file_name): 222 | resp = self.msg("get_errors", file_name) 223 | return resp["result"] 224 | 225 | 226 | def init_file(self, filename): 227 | is_open = filename in self.open_files 228 | content = async_api_call(get_dep_text, filename) 229 | if not is_open: 230 | deps = [ref[1:-1] for ref in 231 | re.findall("/// *= l and pos <= h: 292 | return error 293 | return None 294 | 295 | def set_error_status(self, view): 296 | error = self.get_error_for_pos(get_pos(view)) 297 | if error: 298 | sublime.status_message(error) 299 | else: 300 | sublime.status_message("") 301 | 302 | 303 | # ========================= STATUS MESSAGE MANAGEMENT ============= # 304 | def status_msg_setter(text): 305 | def set_status_msg(): 306 | sublime.status_message(text) 307 | return set_status_msg 308 | 309 | def loading_status_msg(): 310 | msg_base = "Loading typescript plugin" 311 | is_loading = False 312 | for el in cycle("|/-\\"): 313 | if loading_files.val > 0: 314 | is_loading = True 315 | msg = msg_base + " " + el 316 | sublime.set_timeout(status_msg_setter(msg), 0) 317 | elif is_loading == True: 318 | is_loading = False 319 | sublime.set_timeout(status_msg_setter(""), 0) 320 | sleep(0.1) 321 | 322 | Thread(target=loading_status_msg).start() 323 | 324 | # ========================= INITIALIZATION ======================== # 325 | 326 | # Iterate on every open view, add file to server if needed 327 | # for window in sublime.windows(): 328 | # for view in window.views(): 329 | # init_view(view) 330 | 331 | plugin_instances = {} 332 | project_files = {} 333 | 334 | def init_view(view): 335 | project_file = get_project_file(view) 336 | if project_file not in plugin_instances: 337 | plugin_instances[project_file] = PluginInstance() 338 | plugin_instances[project_file].views_text[view.file_name()] = get_all_text(view) 339 | plugin_instances[project_file].init_view(view) 340 | 341 | def close_view(view): 342 | project_file = get_project_file(view) 343 | if project_file in plugin_instances: 344 | plugin_instances[project_file].views_text.pop(view.file_name(), None) 345 | if len(plugin_instances[project_file].views_text) == 0: 346 | plugin_instances[project_file].close_process() 347 | plugin_instances.pop(project_file, None) 348 | 349 | def get_project_file(view): 350 | filename = view.file_name() 351 | if filename in project_files: 352 | return project_files[filename] 353 | else: 354 | pfile = project.find_project_file(filename) 355 | project_files[filename] = pfile 356 | return pfile 357 | 358 | def get_plugin(view): 359 | return plugin_instances[get_project_file(view)] 360 | 361 | # ========================= EVENT HANDLERS ======================== # 362 | 363 | class TypescriptComplete(sublime_plugin.TextCommand): 364 | 365 | def run(self, edit, characters): 366 | # Insert the autocomplete char 367 | for region in self.view.sel(): 368 | self.view.insert(edit, region.end(), characters) 369 | # Update the code on the server side for the current file 370 | get_plugin(self.view).update_server_code(self.view.file_name(), get_all_text(self.view)) 371 | self.view.run_command("auto_complete") 372 | 373 | class AsyncWorker(object): 374 | 375 | def __init__(self, view): 376 | self.view = view 377 | self.plugin = get_plugin(view) 378 | self.content = view.substr(sublime.Region(0, view.size())) 379 | self.filename = view.file_name() 380 | self.view_id = view.buffer_id() 381 | self.errors = None 382 | self.sem = Semaphore() 383 | self.sem.acquire() 384 | self.has_round_queued = False 385 | 386 | def do_more_work(self): 387 | self.content = self.view.substr(sublime.Region(0, self.view.size())) 388 | if not self.has_round_queued: 389 | self.sem.release() 390 | self.has_round_queued = True 391 | 392 | def final(self): 393 | self.plugin.handle_errors(self.view, self.errors) 394 | self.plugin.set_error_status(self.view) 395 | self.has_round_queued = False 396 | 397 | def work(self): 398 | while True: 399 | # Wait on semaphore 400 | self.sem.acquire() 401 | # Update the script 402 | self.plugin.update_server_code(self.filename, self.content) 403 | # Get errors 404 | self.errors = self.plugin.serv_get_errors(self.filename) 405 | sublime.set_timeout(self.final, 1) 406 | self.content = self.plugin.views_text[self.filename] 407 | sleep(1.3) 408 | 409 | 410 | class TestEvent(sublime_plugin.EventListener): 411 | 412 | workers = {} 413 | 414 | def get_worker_thread(self, view): 415 | bid = view.buffer_id() 416 | if not bid in self.workers: 417 | worker = AsyncWorker(view) 418 | Thread(target=worker.work).start() 419 | self.workers[bid] = worker 420 | return self.workers[bid] 421 | 422 | def on_load(self, view): 423 | print "IN ON LOAD FOR VIEW : ", view.file_name() 424 | if is_ts(view): 425 | init_view(view) 426 | 427 | def on_close(self, view): 428 | if is_ts(view): 429 | close_view(view) 430 | 431 | def on_modified(self, view): 432 | if view.is_loading(): return 433 | if is_ts(view): 434 | t = self.get_worker_thread(view) 435 | t.do_more_work() 436 | 437 | def on_selection_modified(self, view): 438 | if is_ts(view): 439 | get_plugin(view).set_error_status(view) 440 | 441 | def on_query_completions(self, view, prefix, locations): 442 | if is_ts(view): 443 | # Get the position of the cursor (first one in case of multiple sels) 444 | pos = view.sel()[0].begin() 445 | line = view.substr(sublime.Region(view.line(pos-1).a, pos)) 446 | bword_pos = sublime.Region(view.word(pos).a, pos) 447 | word = view.substr(bword_pos) 448 | print "WORD : ", word 449 | completions_json = get_plugin(view).serv_get_completions( 450 | view.file_name(), bword_pos.a, is_member_completion(line) 451 | ) 452 | get_plugin(view).set_error_status(view) 453 | return completions_ts_to_sublime(completions_json) 454 | 455 | 456 | def on_query_context(self, view, key, operator, operand, match_all): 457 | if key == "typescript": 458 | view = sublime.active_window().active_view() 459 | return is_ts(view) 460 | 461 | 462 | # msg("add_file", "bin/test_code.ts") 463 | -------------------------------------------------------------------------------- /typescript.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | "extensions": ["ts", "str"], 3 | "auto_complete": false 4 | } 5 | -------------------------------------------------------------------------------- /typescript.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | comment 6 | TypeScript Syntax: version 1.0 7 | fileTypes 8 | 9 | ts 10 | str 11 | 12 | name 13 | TypeScript 14 | patterns 15 | 16 | 17 | captures 18 | 19 | 1 20 | 21 | name 22 | keyword.operator.ts 23 | 24 | 2 25 | 26 | name 27 | variable.parameter.function.ts 28 | 29 | 30 | comment 31 | Match stuff like: module name {...} 32 | match 33 | \b(module)\s*(\s*[a-zA-Z0-9_?.$][\w?.$]*)\s* 34 | name 35 | meta.function.ts 36 | 37 | 38 | comment 39 | Match variable type keywords 40 | match 41 | \b(string|bool|number)\b 42 | name 43 | storage.type.variable.ts 44 | 45 | 46 | captures 47 | 48 | 1 49 | 50 | name 51 | storage.type.variable.ts 52 | 53 | 54 | comment 55 | Match this. 56 | match 57 | \b(this)\. 58 | name 59 | 60 | 61 | 62 | comment 63 | Match stuff like: constructor 64 | match 65 | \b(constructor|declare|interface|as|AS)\b 66 | name 67 | keyword.operator.ts 68 | 69 | 70 | captures 71 | 72 | 1 73 | 74 | name 75 | storage.type.variable.ts 76 | 77 | 78 | comment 79 | Match stuff like: super(argument, list) 80 | match 81 | (super)\s*\(([a-zA-Z0-9,_?.$\s]+\s*)\) 82 | name 83 | keyword.other.ts 84 | 85 | 86 | captures 87 | 88 | 1 89 | 90 | name 91 | entity.name.function.ts 92 | 93 | 94 | comment 95 | Match stuff like: function() {...} 96 | match 97 | ([a-zA-Z_?.$][\w?.$]*)\(\) \{ 98 | name 99 | meta.function.ts 100 | 101 | 102 | captures 103 | 104 | 1 105 | 106 | name 107 | variable.parameter.function.ts 108 | 109 | 2 110 | 111 | name 112 | variable.parameter.function.ts 113 | 114 | 115 | comment 116 | Match stuff like: (function: return type) 117 | match 118 | ([a-zA-Z0-9_?.$][\w?.$]*)\s*:\s*([a-zA-Z0-9_?.$][\w?.$]*) 119 | name 120 | meta.function.ts 121 | 122 | 123 | include 124 | source.js 125 | 126 | 127 | scopeName 128 | source.ts 129 | uuid 130 | 21e323af-f665-4161-96e7-5087d262557e 131 | 132 | 133 | --------------------------------------------------------------------------------