DenoでLSPサーバを実装しているお話
#Deno #LSP #tsserver #アイデア
概要
DenoでLSPサーバを実装しています
https://github.com/uki00a/deno-lsp
追記
deno@v1.6.0にてDeno本体にdeno lspコマンドが実装されました😇
今後はそちらをお使いください (vscode-denoやcoc-denoなどから利用できます)
モチベーション
Node.jsやnpmに依存せず、Denoのみで動作するLSPサーバが欲しい🦕
Import mapやリモートURL(https://deno.land/std@0.76.0/http/server.ts等)のサポートが欲しい
アイデア
TypeScriptのLanguage Service APIを使えば、DenoだけでもLSPのサーバを実装できそうな気がする
DenoからTypeScriptのLanguage Service APIを使用する方法
まず、依存関係はdeps.tsで管理することにします。
jspm.devからtypescriptパッケージを、UNPKGから型定義ファイルを読み込んでみます。
code:deps.ts
// @deno-types="https://unpkg.com/typescript@4.0.3/lib/typescript.d.ts"
import {
default as _ts,
LanguageServiceHost as _LanguageServiceHost,
LanguageService as _LanguageService,
CompilerOptions as _CompilerOptions,
} from "https://jspm.dev/typescript@4.0.3/lib/typescript.js";
export type LanguageServiceHost = _LanguageServiceHost;
export type LanguageService = _LanguageService;
export type CompilerOptions = _CompilerOptions;
export const ts = _ts;
TypeScriptのLanguage Service APIを使うためには、LanguageServiceHostの実装が必要なので、用意します。
code:host.ts
import { ts } from "./deps.ts";
import type { CompilerOptions, LanguageServiceHost } from "./deps.ts";
import { directoryExistsSync, existsSync, fileExistsSync } from "./fs.ts";
export function createServiceHost(
rootFileNames: string[],
options: CompilerOptions,
): LanguageServiceHost {
const files = rootFileNames.reduce((files, rootFileName) => {
filesrootFileName = { version: 0 };
return files;
}, {} as { fileName: string: { version: number } });
return {
getScriptFileNames: () => rootFileNames,
getScriptVersion(fileName: string): string {
return filesfileName && filesfileName.version.toString();
},
getScriptSnapshot(fileName: string) {
if (!existsSync(fileName)) {
return undefined;
}
return ts.ScriptSnapshot.fromString(Deno.readTextFileSync(fileName));
},
getCurrentDirectory: () => Deno.cwd(),
getCompilationSettings: () => options,
getDefaultLibFileName(options: CompilerOptions) {
return ts.getDefaultLibFilePath(options);
},
fileExists: fileExistsSync,
readFile(path) {
console.log(path);
return Deno.readTextFileSync(path);
},
readDirectory(path, extensions, exclude, include, depth) {
console.log(path, extensions, exclude, include, depth);
throw new Error("readDirectory(): unimplemented");
},
directoryExists: directoryExistsSync,
getDirectories(path: string): string[] {
const directories = [];
for (const entry of Deno.readDirSync(path)) {
// TODO isSymlink
if (entry.isDirectory) {
directories.push(entry.name);
}
}
return directories;
},
};
}
あとは、上記のLanguageServiceHostを使ってts.createLanguageServiceを呼べば実現できるはず🤔
code:language_service.ts
import { ts } from "./deps.ts";
import type { LanguageService, CompilerOptions } from "./deps.ts";
import { createServiceHost } from "./host.ts";
export async function createLanguageService(rootFileNames: string[], tsConfig: CompilerOptions): Promise<LanguageService> {
const host = createServiceHost(rootFileNames, tsConfig);
const languageService = ts.createLanguageService(host, ts.createDocumentRegistry());
return languageService;
}
参考情報
lsp_spec_ja
Language Server Protocol Specification - 3.15の日本語訳
language server protocolについて (前編)
typescript-language-server
https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API#incremental-build-support-using-the-language-services