TypeScriptの言語サービスを使ってサクラエディタで入力補完してみる
TypeScriptプロジェクトにはtypescriptServices.jsが含まれており、コンパイラが持つ機能を外部から利用できます。
その中の言語サービス(Language Service API)を使ってコードの補完候補を取得し、SIでおなじみサクラエディタで表示してみました。
PowerShell + WSH(JScript)で構成されているため、標準的なWindows 7以降の環境で動作するはずです。
全体の構成
下記のts-sakura以下を、サクラエディタのプラグインフォルダに置きます。
typeScriptServices.js、lib.d.tsはGitHubから取得できます。
他にも読み込みたい型定義ファイルがあればlib内にしまってください。
ts-sakura │ plugin.def │ ts-sakura.js │ └─server │ server.ps1 │ start.cmd │ typescriptServices.js │ └─lib lib.d.ts
plugin.def
プラグインの定義ファイルです。
[Plugin] Id=ts-sakura Name=ts-sakura Description=TypeScriptの簡易補完プラグイン Type=wsh Version=0.1 Url=http://old-horizon.hateblo.jp/ Author= [Wsh] UseCache=0 [Plug] Complement=ts-sakura.js Complement.Label=ts-sakura [Option] O[1].Section=Option O[1].Key=Port O[1].Label=サーバーのポート番号 O[1].Type=Int O[1].Default=8081
ts-sakura.js
サクラエディタ上で実行されるスクリプトです。
PowerShellのサーバーにソース全文とキャレット位置を送信し、返された補完候補を表示します。
// サーバーのポート番号 var port = Plugin.GetOption("Option","Port"); // 現在入力中の文字列 var currentWord = Complement.GetCurrentWord(); // ソース全文を取得 Editor.SetDrawSwitch(0); Editor.MoveHistSet(); Editor.SelectAll(); var allLines = Editor.GetSelectedString(0); Editor.MoveHistPrev(); // キャレット位置までの文字数を取得 var lineCount = Editor.ExpandParameter("$y"); var columnCount = Editor.ExpandParameter("$x"); var position = 0; for (var i = 1; i < lineCount; i++) { position += Editor.GetLineStr(i).length; } position += columnCount - 1; // サーバーから補完候補を取得 var xhr = new ActiveXObject("Msxml2.XMLHTTP.6.0"); xhr.onreadystatechange = function() { if (xhr.readyState == 4) { if (xhr.status == 200) { var response = xhr.responseText; if (!response) return; var completions = response.split(","); for (var i = 0, length = completions.length; i < length; i++) { // 先頭一致の候補のみを表示 if(currentWord == "." || completions[i].toLowerCase().lastIndexOf(currentWord.toLowerCase(), 0) == 0) { try{ Complement.AddList(currentWord == "." ? "." + completions[i] : completions[i]); } catch(e) {} } } } else { var shell = new ActiveXObject("WScript.Shell"); shell.Popup("通信に失敗しました。\nサーバーが起動していない可能性があります。", 3, "ts-sakura", 48); } } } try { // リクエストを送信 xhr.Open("POST", "http://localhost:" + port + "/", false); xhr.setRequestHeader("x-caretposition", position); xhr.send(allLines); } catch(e) {}
server.ps1
補完候補を返す簡易HTTPサーバーです。
ScriptControlでWSHエンジンを作成し、その中でAPIを呼び出します。
# ポート番号 $port = $args[0] # カレントディレクトリ $currentDir = [System.IO.Path]::GetDirectoryName($MyInvocation.InvocationName) # フルパスを取得 function getFullPath($name) { return $([System.IO.Path]::Combine($currentDir, $name)).Replace("\","\\") } $jsCode = @" // 指定ファイルのテキストを取得 function readFile(path) { var sr = new ActiveXObject("ADODB.Stream"); sr.Type = 2; sr.charset = "_autodetect_all"; sr.Open(); sr.LoadFromFile(path); var content = sr.ReadText(-1); sr.Close(); sr = null; return content; } // eval時のエラー防止 Array.prototype.indexOf = function() {} // TypeScriptコンパイラを読み込む eval(readFile("$(getFullPath("typescriptServices.js"))")); // インターフェースの実装 var LanguageServiceHostImpl = (function() { function LanguageServiceHostImpl() { this.files = {}; } LanguageServiceHostImpl.prototype.getCompilationSettings = function() { return ts.getDefaultCompilerOptions(); }; LanguageServiceHostImpl.prototype.getScriptFileNames = function() { var fileNames = []; for (var fileName in this.files) { if (this.files.hasOwnProperty(fileName)) { fileNames.push(fileName); } } return fileNames; }; LanguageServiceHostImpl.prototype.getScriptVersion = function(fileName) { return this.files[fileName].version.toString(); }; LanguageServiceHostImpl.prototype.getScriptIsOpen = function(fileName) { return true; }; LanguageServiceHostImpl.prototype.getScriptSnapshot = function(fileName) { return this.files[fileName].snapshot; }; LanguageServiceHostImpl.prototype.getCurrentDirectory = function() { return ""; }; LanguageServiceHostImpl.prototype.getDefaultLibFilename = function(options) { return ""; }; LanguageServiceHostImpl.prototype.log = function(s) { }; LanguageServiceHostImpl.prototype.addFile = function(fileName, body) { var snapshot = ts.ScriptSnapshot.fromString(body); snapshot.getChangeRange = function (oldSnapshot) { return undefined; }; if (this.files[fileName]) { this.files[fileName].version++; this.files[fileName].snapshot = snapshot; } else { this.files[fileName] = { snapshot: snapshot, version: 1 }; } }; return LanguageServiceHostImpl; })(); var fso = new ActiveXObject("Scripting.FileSystemObject"); // libディレクトリのフルパス var libDir = fso.BuildPath("$($currentDir.Replace("\","\\"))", "lib"); var host = new LanguageServiceHostImpl(); var service = ts.createLanguageService(host, ts.createDocumentRegistry()); // 型定義ファイルの読み込み var e = new Enumerator(fso.GetFolder(libDir).Files); for (; !e.atEnd(); e.moveNext()) { var file = e.item(); if (/\.ts$/.test(file.Name)) { host.addFile(file.Name, readFile(file.Path)); } } // タイムスタンプから一時ファイル名を生成 var tempName = new Date().getTime(); while (host.files[tempName + ".ts"]) { // ファイル名が衝突した場合はインクリメント tempName++; } tempName += ".ts"; // 補完候補を取得 function getCompletions(source, position) { host.addFile(tempName, source); var completions = service.getCompletionsAtPosition(tempName, position); var entries = []; if (completions) { for (var i = 0, length = completions.entries.length; i < length; i++) { entries.push(completions.entries[i].name); } } // カンマ区切りで一覧を返す return entries.join(","); } "@ # WSHオブジェクトを生成 $sc = New-Object -Com ScriptControl $sc.Language = "JScript" $sc.AddCode($jsCode) # HTTPサーバーの起動 $listener = New-Object System.Net.HttpListener $listener.Prefixes.add("http://localhost:" + $port + "/") $listener.Start() Write-Host "ポート$($port)で起動しました" while($listener.IsListening) { $context = $listener.GetContext() $request = $context.Request $response = $context.Response if ($request.HasEntityBody) { $body = $request.InputStream $reader = New-Object System.IO.StreamReader($body, $request.ContentEncoding) # ソース全文とキャレット位置から補完候補を取得 $completions = $sc.CodeObject.getCompletions($reader.ReadToEnd(), $request.Headers["x-caretposition"]) # 戻り値を出力して返す $writer = New-Object System.IO.StreamWriter($response.OutputStream, $request.ContentEncoding) $writer.Write($completions) $writer.Close() $response.OutputStream.Close() $response.Close() } } $listener.Stop()
start.cmd
待ち受けポートを指定してserver.ps1を実行するバッチファイルです。
ScriptControlを使う都合上、32bit版のPowerShellを起動させています。
@echo off rem ポート番号を指定 set port=8081 if defined ProgramFiles(x86) ( %SystemRoot%\syswow64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy RemoteSigned -File server.ps1 %port% ) else ( powershell.exe -ExecutionPolicy RemoteSigned -File server.ps1 %port% )
使い方
start.cmdとサクラエディタ側の設定でポート番号を合わせ、サーバーを起動すると補完候補が出るようになります。
ただ依存関係の解決をしていないし、不安定で全然ダメですね…実用するならTSServer使うべきでしょう。
JavaScriptの範囲でも少しは型推論が効きます。