title | description | date | tags | |
---|---|---|---|---|
03. Language server |
nn 개발기 3편 |
2024-10-03 |
|
이번 글은 언어 서버에 대해 설명하려 한다.
다만 예전 구현은 별로 설명할만한 구석이 없어서 현재(2024-10-03) 기준으로 글을 작성하였다.
또한, 구현 중 참고했던 Typescript Language Server (이하 TSServer)코드도 같이 설명했으니 알아두길 바란다.
2024-10-03 aa93904
큰 틀에서의 구조는 TSServer와 크게 다르지 않은데,
export function createLspConnection(options: LspConnectionOptions) {
const connection = createConnection(ProposedFeatures.all);
const client = new LspClient(connection);
const logger = new LspClientLogger(client, options.showMessageLevel);
const documents = new TextDocuments(TextDocument);
const context: Partial<LspContext> = {
logger,
client,
documents,
showMessageLevel: options.showMessageLevel
}
connection.onDidOpenTextDocument((params) => openTextDocument(params, context as LspContext))
connection.onDidCloseTextDocument((params) => onDidCloseTextDocument(params, context as LspContext))
documents.onDidChangeContent((params) => onDidChangeTextDocument(params, context as LspContext))
connection.onInitialize((params) => initialize(params, context))
connection.onCompletion((params, token) => completion(params, context as LspContext, token))
connection.onHover((params, token) => hover(params, context as LspContext, token))
connection.languages.semanticTokens.on((params) => semanticTokens(params, context as LspContext))
documents.listen(connection);
return connection;
}
export function createLspConnection(options: LspConnectionOptions): lsp.Connection {
const connection = lsp.createConnection(lsp.ProposedFeatures.all);
const lspClient = new LspClientImpl(connection);
const logger = new LspClientLogger(lspClient, options.showMessageLevel);
const server: LspServer = new LspServer({
logger,
lspClient,
});
connection.onInitialize(server.initialize.bind(server));
connection.onInitialized(server.initialized.bind(server));
connection.onDidChangeConfiguration(server.didChangeConfiguration.bind(server));
connection.onDidOpenTextDocument(server.didOpenTextDocument.bind(server));
connection.onDidSaveTextDocument(server.didSaveTextDocument.bind(server));
connection.onDidCloseTextDocument(server.didCloseTextDocument.bind(server));
connection.onDidChangeTextDocument(server.didChangeTextDocument.bind(server));
connection.onCodeAction(server.codeAction.bind(server));
connection.onCodeLens(server.codeLens.bind(server));
connection.onCodeLensResolve(server.codeLensResolve.bind(server));
connection.onCompletion(server.completion.bind(server));
connection.onCompletionResolve(server.completionResolve.bind(server));
connection.onDefinition(server.definition.bind(server));
connection.onImplementation(server.implementation.bind(server));
connection.onTypeDefinition(server.typeDefinition.bind(server));
connection.onDocumentFormatting(server.documentFormatting.bind(server));
connection.onDocumentRangeFormatting(server.documentRangeFormatting.bind(server));
connection.onDocumentHighlight(server.documentHighlight.bind(server));
connection.onDocumentSymbol(server.documentSymbol.bind(server));
connection.onExecuteCommand(server.executeCommand.bind(server));
connection.onHover(server.hover.bind(server));
connection.onReferences(server.references.bind(server));
connection.onRenameRequest(server.rename.bind(server));
connection.onPrepareRename(server.prepareRename.bind(server));
connection.onSelectionRanges(server.selectionRanges.bind(server));
connection.onSignatureHelp(server.signatureHelp.bind(server));
connection.onWorkspaceSymbol(server.workspaceSymbol.bind(server));
connection.onFoldingRanges(server.foldingRanges.bind(server));
connection.languages.onLinkedEditingRange(server.linkedEditingRange.bind(server));
connection.languages.callHierarchy.onPrepare(server.prepareCallHierarchy.bind(server));
connection.languages.callHierarchy.onIncomingCalls(server.callHierarchyIncomingCalls.bind(server));
connection.languages.callHierarchy.onOutgoingCalls(server.callHierarchyOutgoingCalls.bind(server));
connection.languages.inlayHint.on(server.inlayHints.bind(server));
connection.languages.semanticTokens.on(server.semanticTokensFull.bind(server));
connection.languages.semanticTokens.onRange(server.semanticTokensRange.bind(server));
connection.workspace.onWillRenameFiles(server.willRenameFiles.bind(server));
return connection;
}
위쪽 코드가 nn의 lsp 메소드별 라우팅, 아래쪽 코드가 TSServer가 구현하고 있는 lsp 메소드별 라우팅이다.
다만 라우팅 하위 코드를 TSServer에서는 class로 구현하고 있는 반면,
export async function hover(params: TextDocumentPositionParams, context: LspContext, token?: CancellationToken): Promise<Hover | null> {
nn 구현에서는 context 객체를 따로 만들어서 인자로 넣어주고 있다. (큰 이유는 없지만 class 문법의 장황함과 indent가 맘에 안들었다.)
그 외 설명할만한 포인트가 하나 있는데,
private triggerDiagnostics(delay: number = 200): void {
this.diagnosticDelayer.trigger(() => {
this.sendPendingDiagnostics();
}, delay);
}
diagnostic을 전달하기 전에 일부러 200ms의 딜레이를 준다.
이유는 사용자의 키 스트로크가 진행되는 중에는 diagnostic이 실시간으로 변하지 않게 하기 위함이다.
(이 기능 때문에 TSServer에서 diagnostic을 전달하는 코드가 엄청 복잡해졌는데, 간단히 설명하자면 이미 발생한 요청을 취소하기 위해 최소 2~3개의 클래스를 더 만들어야 했다.)
해당 기능도 구현하기로 마음을 먹어
export class Delayer<T> {
constructor(
public defaultDelay: number,
private timeout: NodeJS.Timeout | null = null,
private completionPromise: Promise<T> | null = null,
private onSuccess: ((value: T) => void) | null = null,
private task: (() => T) | null = null,
)
{
}
public trigger(task: () => T, delay: number = this.defaultDelay): Promise<T> {
this.task = task;
if (delay >= 0) {
this.cancelTimeout();
}
if (!this.completionPromise) {
this.completionPromise = new Promise<T>((resolve) => {
this.onSuccess = resolve;
}).then(() => {
this.completionPromise = null;
const result = this.task!();
this.task = null;
return result;
});
}
if (delay >= 0 || this.timeout === null) {
this.timeout = setTimeout(() => {
this.timeout = null;
this.onSuccess!(null!);
}, delay >= 0 ? delay : this.defaultDelay);
}
return this.completionPromise;
}
public cancelTimeout(): void {
if (this.timeout !== null) {
clearTimeout(this.timeout);
this.timeout = null;
}
}
}
delayer 코드를 구현하고 적용했다.
아무래도 아직 구현할 게 많기 때문에, 나중에 관련해서 글을 또 써야할 것 같다.
다음 주제는 (아마도) 타입 체커 세부 구현일 것 같다.