From 59dc1ab7262cde444c1e94569ecb7208864ac2ab Mon Sep 17 00:00:00 2001 From: Ashar Date: Sun, 16 Jun 2024 23:30:29 +0530 Subject: [PATCH] feat: Add support for go to definition and hover and diagnostics on single file (#2) In a single file mode, the definition and hover support have been added. --- .gitignore | 1 + Cargo.lock | 139 ++++++++++++++++++++++++++++++++ Cargo.toml | 3 + README.md | 15 +++- src/lsp.rs | 176 ++++++++++++++++++++++++++++++----------- src/main.rs | 15 +++- src/parser.rs | 200 +++++++++++++++++++++++++++++++++++++++++++++++ src/server.rs | 17 ++-- src/simple.proto | 20 +++++ src/utils.rs | 16 ++++ 10 files changed, 545 insertions(+), 57 deletions(-) create mode 100644 src/parser.rs create mode 100644 src/utils.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..791af9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/logs diff --git a/Cargo.lock b/Cargo.lock index 1438f29..79ee3f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "async-lsp" version = "0.2.0" @@ -88,6 +97,30 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "errno" version = "0.3.9" @@ -307,6 +340,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num_cpus" version = "1.16.0" @@ -379,6 +418,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.85" @@ -398,7 +443,10 @@ dependencies = [ "tokio-util", "tower", "tracing", + "tracing-appender", "tracing-subscriber", + "tree-sitter", + "tree-sitter-proto", ] [[package]] @@ -419,6 +467,35 @@ dependencies = [ "bitflags 2.5.0", ] +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -576,6 +653,37 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -670,6 +778,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.27" @@ -716,6 +836,25 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tree-sitter" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7cc499ceadd4dcdf7ec6d4cbc34ece92c3fa07821e287aedecd4416c516dca" +dependencies = [ + "cc", + "regex", +] + +[[package]] +name = "tree-sitter-proto" +version = "0.0.1" +source = "git+https://github.com/coder3101/tree-sitter-proto?branch=main#9f702d2b9544662bf2f9ff0035fc71c0172c6b64" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "unicode-bidi" version = "0.3.15" diff --git a/Cargo.toml b/Cargo.toml index d07aa26..5235f79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,6 @@ tokio-util = { version = "0.7.11", features = ["compat"] } tower = "0.4.13" tracing = "0.1.40" tracing-subscriber = "0.3.18" +tree-sitter-proto = { git = "https://github.com/coder3101/tree-sitter-proto", branch = "main" } +tree-sitter = "0.22.6" +tracing-appender = "0.2.3" diff --git a/README.md b/README.md index 2254ebd..328a1e4 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,20 @@ # protols -Language server for proto files +A Language Server for **proto3** files. It only uses tree-sitter parser for all operations and always runs in **single file mode**. + +## Features +- [x] Hover +- [x] Go to definition +- [x] Diagnostics + + +## Installation and testing + +Clone the repository and run `cargo install --path .` to install locally in your `~/.cargo/bin` and the below to your `init.lua` until we start shipping this via Mason. -## Testing with neovim ```lua local client = vim.lsp.start_client({ name = "protols", - cmd = { "" }, + cmd = { vim.fn.expand("$HOME/.cargo/bin/protols") }, }) if not client then diff --git a/src/lsp.rs b/src/lsp.rs index 1ca126c..f52ed42 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -1,15 +1,13 @@ -use async_lsp::LanguageClient; use std::ops::ControlFlow; -use std::time::Duration; use tracing::{debug, info}; use async_lsp::lsp_types::{ - DidChangeConfigurationParams, DidChangeTextDocumentParams, DidOpenTextDocumentParams, - DidSaveTextDocumentParams, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, - HoverParams, HoverProviderCapability, InitializeParams, InitializeResult, MarkedString, - MessageType, OneOf, ServerCapabilities, ServerInfo, ShowMessageParams, + DidChangeTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, + GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, + HoverProviderCapability, InitializeParams, InitializeResult, OneOf, ServerCapabilities, + ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind, }; -use async_lsp::{LanguageServer, ResponseError}; +use async_lsp::{ErrorCode, LanguageClient, LanguageServer, ResponseError}; use futures::future::BoxFuture; use crate::server::ServerState; @@ -33,56 +31,140 @@ impl LanguageServer for ServerState { info!("Connected with client {cname} {cversion}"); debug!("Initialize with {params:?}"); - Box::pin(async move { - Ok(InitializeResult { - capabilities: ServerCapabilities { - hover_provider: Some(HoverProviderCapability::Simple(true)), - ..ServerCapabilities::default() - }, - server_info: Some(ServerInfo { - name: env!("CARGO_PKG_NAME").to_string(), - version: Some(env!("CARGO_PKG_VERSION").to_string()), - }), - }) - }) + let response = InitializeResult { + capabilities: ServerCapabilities { + // todo(): We might prefer incremental sync at some later stage + text_document_sync: Some(TextDocumentSyncCapability::Kind( + TextDocumentSyncKind::FULL, + )), + definition_provider: Some(OneOf::Left(true)), + hover_provider: Some(HoverProviderCapability::Simple(true)), + ..ServerCapabilities::default() + }, + server_info: Some(ServerInfo { + name: env!("CARGO_PKG_NAME").to_string(), + version: Some(env!("CARGO_PKG_VERSION").to_string()), + }), + }; + + Box::pin(async move { Ok(response) }) } - fn hover(&mut self, _: HoverParams) -> BoxFuture<'static, Result, Self::Error>> { - let mut client = self.client.clone(); - let counter = self.counter; - Box::pin(async move { - tokio::time::sleep(Duration::from_secs(1)).await; - client - .show_message(ShowMessageParams { - typ: MessageType::INFO, - message: "Hello LSP".into(), - }) - .unwrap(); - Ok(Some(Hover { - contents: HoverContents::Scalar(MarkedString::String(format!( - "I am a hover text {counter}!" - ))), + fn hover( + &mut self, + param: HoverParams, + ) -> BoxFuture<'static, Result, Self::Error>> { + let uri = param.text_document_position_params.text_document.uri; + let pos = param.text_document_position_params.position; + + let Some(contents) = self.documents.get(&uri) else { + return Box::pin(async move { + Err(ResponseError::new( + ErrorCode::INVALID_REQUEST, + "uri was never opened", + )) + }); + }; + + let Some(parsed) = self.parser.parse(contents.as_bytes()) else { + return Box::pin(async move { + Err(ResponseError::new( + ErrorCode::REQUEST_FAILED, + "ts failed to parse contents", + )) + }); + }; + + let comments = parsed.hover(&pos, contents.as_bytes()); + info!("Found {} node comments in the document", comments.len()); + let response = match comments.len() { + 0 => None, + 1 => Some(Hover { + contents: HoverContents::Scalar(comments[0].clone()), range: None, - })) - }) + }), + 2.. => Some(Hover { + contents: HoverContents::Array(comments), + range: None, + }), + }; + + Box::pin(async move { Ok(response) }) } - // fn definition( - // &mut self, - // _: GotoDefinitionParams, - // ) -> BoxFuture<'static, Result, ResponseError>> { - // unimplemented!("Not yet implemented!"); - // } + fn definition( + &mut self, + param: GotoDefinitionParams, + ) -> BoxFuture<'static, Result, ResponseError>> { + let uri = param.text_document_position_params.text_document.uri; + let pos = param.text_document_position_params.position; + + let Some(contents) = self.documents.get(&uri) else { + return Box::pin(async move { + Err(ResponseError::new( + ErrorCode::INVALID_REQUEST, + "uri was never opened", + )) + }); + }; + + let Some(parsed) = self.parser.parse(contents.as_bytes()) else { + return Box::pin(async move { + Err(ResponseError::new( + ErrorCode::REQUEST_FAILED, + "ts failed to parse contents", + )) + }); + }; + + let locations = parsed.definition(&pos, &uri, contents.as_bytes()); + info!("Found {} matching nodes in the document", locations.len()); + + let response = match locations.len() { + 0 => None, + 1 => Some(GotoDefinitionResponse::Scalar(locations[0].clone())), + 2.. => Some(GotoDefinitionResponse::Array(locations)), + }; + + Box::pin(async move { Ok(response) }) + } fn did_save(&mut self, _: DidSaveTextDocumentParams) -> Self::NotifyResult { - todo!("to implement") + ControlFlow::Continue(()) } - fn did_open(&mut self, _: DidOpenTextDocumentParams) -> Self::NotifyResult { - todo!("to implement") + fn did_open(&mut self, params: DidOpenTextDocumentParams) -> Self::NotifyResult { + let uri = params.text_document.uri; + let contents = params.text_document.text; + info!("Opened file at: {:}", uri); + self.documents.insert(uri.clone(), contents.clone()); + + let Some(parsed) = self.parser.parse(contents.as_bytes()) else { + tracing::error!("failed to parse content"); + return ControlFlow::Continue(()); + }; + + let diagnostics = parsed.collect_parse_errors(&uri); + if let Err(e) = self.client.publish_diagnostics(diagnostics) { + tracing::error!("failed to publish diagnostics. {:?}", e) + } + ControlFlow::Continue(()) } - fn did_change(&mut self, _: DidChangeTextDocumentParams) -> Self::NotifyResult { - todo!("to implement") + fn did_change(&mut self, params: DidChangeTextDocumentParams) -> Self::NotifyResult { + let uri = params.text_document.uri; + let contents = params.content_changes[0].text.clone(); + self.documents.insert(uri.clone(), contents.clone()); + + let Some(parsed) = self.parser.parse(contents.as_bytes()) else { + tracing::error!("failed to parse content"); + return ControlFlow::Continue(()); + }; + + let diagnostics = parsed.collect_parse_errors(&uri); + if let Err(e) = self.client.publish_diagnostics(diagnostics) { + tracing::error!("failed to publish diagnostics. {:?}", e) + } + ControlFlow::Continue(()) } } diff --git a/src/main.rs b/src/main.rs index edd901b..7f9b132 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,10 +7,12 @@ use async_lsp::server::LifecycleLayer; use async_lsp::tracing::TracingLayer; use server::{ServerState, TickEvent}; use tower::ServiceBuilder; -use tracing::Level; +use tracing::{info, Level}; mod lsp; +mod parser; mod server; +mod utils; #[tokio::main(flavor = "current_thread")] async fn main() { @@ -37,10 +39,19 @@ async fn main() { .service(ServerState::new_router(client)) }); + let mut dir = std::env::temp_dir(); + dir.push("protols.log"); + + eprintln!("Logs are being written to {:?}", dir); + + let file_appender = + tracing_appender::rolling::daily(std::env::temp_dir().as_path(), "protols.log"); + let (non_blocking, _gaurd) = tracing_appender::non_blocking(file_appender); + tracing_subscriber::fmt() .with_max_level(Level::INFO) .with_ansi(false) - .with_writer(std::io::stderr) + .with_writer(non_blocking) .init(); // Prefer truly asynchronous piped stdin/stdout without blocking tasks. diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..4c133ad --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,200 @@ +use async_lsp::lsp_types::{ + Diagnostic, DiagnosticSeverity, Location, MarkedString, Position, PublishDiagnosticsParams, + Range, Url, +}; +use tracing::info; +use tree_sitter::{Node, Tree, TreeCursor}; + +use crate::utils::{lsp_to_ts_point, ts_to_lsp_position}; + +pub struct ProtoParser { + parser: tree_sitter::Parser, +} + +pub struct ParsedTree { + tree: Tree, +} + +impl ProtoParser { + pub fn new() -> Self { + let mut parser = tree_sitter::Parser::new(); + if let Err(e) = parser.set_language(&tree_sitter_proto::language()) { + panic!("failed to set ts language parser {:?}", e); + } + Self { parser } + } + + pub fn parse(&mut self, contents: impl AsRef<[u8]>) -> Option { + self.parser + .parse(contents, None) + .map(|t| ParsedTree { tree: t }) + } +} + +impl ParsedTree { + fn walk_and_collect_kinds<'a>( + &self, + cursor: &mut TreeCursor<'a>, + kinds: &[&str], + ) -> Vec> { + let mut v = vec![]; + + loop { + let node = cursor.node(); + + if kinds.contains(&node.kind()) { + v.push(node) + } + + if cursor.goto_first_child() { + v.extend(self.walk_and_collect_kinds(cursor, kinds)); + cursor.goto_parent(); + } + + if !cursor.goto_next_sibling() { + break; + } + } + + v + } + + fn advance_cursor_to<'a>(&self, cursor: &mut TreeCursor<'a>, nid: usize) -> bool { + loop { + let node = cursor.node(); + if node.id() == nid { + return true; + } + if cursor.goto_first_child() { + if self.advance_cursor_to(cursor, nid) { + return true; + } + cursor.goto_parent(); + } + if !cursor.goto_next_sibling() { + return false; + } + } + } + + fn find_preceeding_comments(&self, nid: usize, content: impl AsRef<[u8]>) -> Option { + let root = self.tree.root_node(); + let mut cursor = root.walk(); + + info!("Looking for node with id: {nid}"); + + self.advance_cursor_to(&mut cursor, nid); + if !cursor.goto_parent() { + return None; + } + + if !cursor.goto_previous_sibling() { + return None; + } + + let mut comments = vec![]; + while cursor.node().kind() == "comment" { + let node = cursor.node(); + let text = node + .utf8_text(content.as_ref()) + .expect("utf-8 parser error") + .trim() + .trim_start_matches("//") + .trim(); + + comments.push(text); + + if !cursor.goto_previous_sibling() { + break; + } + } + return if comments.len() != 0 { + comments.reverse(); + Some(comments.join("\n")) + } else { + None + }; + } +} + +impl ParsedTree { + pub fn get_node_text_at_position<'a>( + &'a self, + pos: &Position, + content: &'a [u8], + ) -> Option<&'a str> { + let pos = lsp_to_ts_point(pos); + self.tree + .root_node() + .descendant_for_point_range(pos, pos) + .map(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error")) + } + + pub fn find_childrens_by_kinds(&self, kinds: &[&str]) -> Vec { + let mut cursor = self.tree.root_node().walk(); + self.walk_and_collect_kinds(&mut cursor, kinds) + } + + pub fn definition( + &self, + pos: &Position, + uri: &Url, + content: impl AsRef<[u8]>, + ) -> Vec { + let text = self.get_node_text_at_position(pos, content.as_ref()); + info!("Looking for definition of: {:?}", text); + + match text { + Some(text) => self + .find_childrens_by_kinds(&["message_name", "enum_name"]) + .into_iter() + .filter(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error") == text) + .map(|n| Location { + uri: uri.clone(), + range: Range { + start: ts_to_lsp_position(&n.start_position()), + end: ts_to_lsp_position(&n.end_position()), + }, + }) + .collect(), + None => vec![], + } + } + + pub fn hover(&self, pos: &Position, content: impl AsRef<[u8]>) -> Vec { + let text = self.get_node_text_at_position(pos, content.as_ref()); + info!("Looking for hover response on: {:?}", text); + match text { + Some(text) => self + .find_childrens_by_kinds(&["message_name", "enum_name", "service_name", "rpc_name"]) + .into_iter() + .filter(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error") == text) + .filter_map(|n| self.find_preceeding_comments(n.id(), content.as_ref())) + .map(|s| MarkedString::String(s)) + .collect(), + None => vec![], + } + } + + pub fn collect_parse_errors(&self, uri: &Url) -> PublishDiagnosticsParams { + let diagnostics = self + .find_childrens_by_kinds(&["ERROR"]) + .into_iter() + .map(|n| Diagnostic { + range: Range { + start: ts_to_lsp_position(&n.start_position()), + end: ts_to_lsp_position(&n.end_position()), + }, + severity: Some(DiagnosticSeverity::ERROR), + source: Some("protols".to_string()), + message: "Syntax error".to_string(), + ..Default::default() + }) + .collect(); + PublishDiagnosticsParams { + uri: uri.clone(), + diagnostics, + version: None, + } + } +} diff --git a/src/server.rs b/src/server.rs index 28b76a7..252fd6e 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,22 +1,29 @@ -use async_lsp::{router::Router, ClientSocket}; -use std::ops::ControlFlow; -use tracing::info; +use async_lsp::{lsp_types::Url, router::Router, ClientSocket}; +use std::{collections::HashMap, ops::ControlFlow}; + +use crate::parser::ProtoParser; pub struct TickEvent; pub struct ServerState { pub client: ClientSocket, pub counter: i32, + pub documents: HashMap, + pub parser: ProtoParser, } impl ServerState { pub fn new_router(client: ClientSocket) -> Router { - let mut router = Router::from_language_server(Self { client, counter: 0 }); + let mut router = Router::from_language_server(Self { + client, + counter: 0, + documents: Default::default(), + parser: ProtoParser::new(), + }); router.event(Self::on_tick); router } fn on_tick(&mut self, _: TickEvent) -> ControlFlow> { - info!("tick"); self.counter += 1; ControlFlow::Continue(()) } diff --git a/src/simple.proto b/src/simple.proto index fc647b0..a896698 100644 --- a/src/simple.proto +++ b/src/simple.proto @@ -3,20 +3,33 @@ syntax = "proto3"; package com.book; message Book { + // This is a multi line comment on the field name + // Of a message called Book int64 isbn = 1; string title = 2; string author = 3; } +// This is a comment on message message GetBookRequest { + + // This is a sigle line comment on the field of a message int64 isbn = 1; } +message GotoBookRequest { + bool flag = 1; +} + message GetBookViaAuthor { string author = 1; } + +// It is a BookService Implementation service BookService { + // This is GetBook RPC that takes a book request + // and returns a Book, simple and sweet rpc GetBook (GetBookRequest) returns (Book) {} rpc GetBooksViaAuthor (GetBookViaAuthor) returns (stream Book) {} rpc GetGreatestBook (stream GetBookRequest) returns (Book) {} @@ -24,10 +37,17 @@ service BookService { } message BookStore { + reserved 1; + Book book = 0; string name = 1; map books = 2; + EnumSample sample = 3; } +// These are enum options representing some operation in the proto +// these are meant to be ony called from one place, + +// Note: Please set only to started or running enum EnumSample { option allow_alias = true; UNKNOWN = 0; diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..4b26ccb --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,16 @@ +use async_lsp::lsp_types::Position; +use tree_sitter::Point; + +pub fn ts_to_lsp_position(p: &Point) -> Position { + Position { + line: p.row as u32, + character: p.column as u32, + } +} + +pub fn lsp_to_ts_point(p: &Position) -> Point { + Point { + row: p.line as usize, + column: p.character as usize, + } +}