diff --git a/src/error.rs b/src/error.rs index c365e81..a772739 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,6 +7,8 @@ pub enum Error { SendError(String), #[error("OutOfBounds at ix {0}")] OutOfBounds(usize), + #[error("NoSuchFile {0}")] + NoSuchFile(String), #[error(transparent)] IO(#[from] std::io::Error), diff --git a/src/main.rs b/src/main.rs index 0355a9a..1250614 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,30 +4,22 @@ mod utils; mod words; use crate::prelude::*; - -use crate::utils::data_to_position::ToPosition; -use crate::utils::find_variant_sublists_from_to::FindVariantSublistsFromTo; -use crate::utils::ropey_get_ix::GetIx; -use crate::utils::ropey_word_at_char::WordAtChar; -use crate::words::{Word, Words}; +use crate::utils::handlers::notification_did_change::handle_did_change_text_document; +use crate::utils::handlers::notification_did_open::handle_did_open_text_document; +use crate::utils::handlers::request_completion::handle_completion; +use crate::utils::handlers::request_goto_definition::handle_goto_definition; +use crate::utils::handlers::request_hover::handle_hover; +use crate::utils::server_capabilities::forth_lsp_capabilities; +use crate::words::Words; use std::collections::HashMap; use std::ffi::OsStr; use std::fs; -use std::mem::discriminant; use std::path::Path; -use forth_lexer::parser::Lexer; -use forth_lexer::token::{Data, Token}; -use lsp_types::request::{Completion, HoverRequest}; -use lsp_types::{ - request::GotoDefinition, GotoDefinitionResponse, InitializeParams, ServerCapabilities, -}; -use lsp_types::{ - CompletionItem, CompletionResponse, Hover, Location, OneOf, Range, TextDocumentSyncKind, Url, -}; +use lsp_server::{Connection, Message}; +use lsp_types::InitializeParams; -use lsp_server::{Connection, Message, Notification, Request, RequestId, Response}; use ropey::Rope; fn main() -> Result<()> { @@ -39,23 +31,7 @@ fn main() -> Result<()> { let (connection, io_threads) = Connection::stdio(); // Run the server and wait for the two threads to end (typically by trigger LSP Exit event). - let server_capabilities = serde_json::to_value(ServerCapabilities { - text_document_sync: Some(lsp_types::TextDocumentSyncCapability::Kind( - TextDocumentSyncKind::INCREMENTAL, - )), - // workspace_symbol_provider - workspace: Some(lsp_types::WorkspaceServerCapabilities { - workspace_folders: Some(lsp_types::WorkspaceFoldersServerCapabilities { - supported: Some(true), - change_notifications: Some(OneOf::Left(false)), - }), - file_operations: None, - }), - hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)), - definition_provider: Some(OneOf::Left(true)), - completion_provider: Some(lsp_types::CompletionOptions::default()), - ..Default::default() - })?; + let server_capabilities = serde_json::to_value(forth_lsp_capabilities())?; let initialization_params = connection.initialize(server_capabilities)?; main_loop(connection, initialization_params)?; io_threads.join()?; @@ -67,8 +43,8 @@ fn main() -> Result<()> { fn main_loop(connection: Connection, params: serde_json::Value) -> Result<()> { eprintln!("Starting main loop"); - let mut files = HashMap::::new(); let init: InitializeParams = serde_json::from_value(params)?; + let mut files = HashMap::::new(); if let Some(roots) = init.workspace_folders { eprintln!("Root: {:?}", roots); for root in roots { @@ -78,258 +54,31 @@ fn main_loop(connection: Connection, params: serde_json::Value) -> Result<()> { let data = Words::default(); for msg in &connection.receiver { match msg { - Message::Request(req) => { - if connection.handle_shutdown(&req)? { + Message::Request(request) => { + if connection.handle_shutdown(&request)? { return Ok(()); } - eprintln!("got request: {:?}", req.method); - match cast::(req.clone()) { - Ok((id, params)) => { - eprintln!("#{id}: {params:?}"); - let rope = files - .get_mut(¶ms.text_document_position.text_document.uri.to_string()) - .expect("Must be able to get rope for lang"); - let mut ix = rope.get_ix(¶ms); - if ix >= rope.len_chars() { - return Err(Error::OutOfBounds(ix)); - } - if let Some(char_at_ix) = rope.get_char(ix) { - if char_at_ix.is_whitespace() && ix > 0 { - ix -= 1; - } - } - let word = rope.word_at_char(ix); - eprintln!("Found word {}", word); - let use_lower = if let Some(chr) = word.get_char(word.len_chars() - 1) { - chr.is_lowercase() - } else { - false - }; - let result = if word.len_chars() > 0 { - let mut ret = vec![]; - //TODO: - let candidates = data.words.iter().filter(|x| { - x.token - .to_lowercase() - .starts_with(word.to_string().to_lowercase().as_str()) - }); - for candidate in candidates { - let label = candidate.token.to_owned(); - let label = if use_lower { - label.to_lowercase() - } else { - label - }; - ret.push(CompletionItem { - label, - detail: Some(candidate.stack.to_owned()), - documentation: Some(lsp_types::Documentation::MarkupContent( - lsp_types::MarkupContent { - kind: lsp_types::MarkupKind::Markdown, - value: candidate.help.to_owned(), - }, - )), - ..Default::default() - }); - } - Some(CompletionResponse::Array(ret)) - } else { - None - }; - let result = serde_json::to_value(&result) - .expect("Must be able to serialize the CompletionResponse"); - let resp = Response { - id, - result: Some(result), - error: None, - }; - connection - .sender - .send(Message::Response(resp)) - .map_err(|err| Error::SendError(err.to_string()))?; - continue; - } - Err(Error::ExtractRequestError(req)) => req, - Err(err) => panic!("{err:?}"), - }; - match cast::(req.clone()) { - Ok((id, params)) => { - eprintln!("#{id}: {params:?}"); - let rope = files - .get_mut( - ¶ms - .text_document_position_params - .text_document - .uri - .to_string(), - ) - .expect("Must be able to get rope for lang"); - let ix = rope.get_ix(¶ms); - if ix >= rope.len_chars() { - return Err(Error::OutOfBounds(ix)); - } - let word = word_on_or_before_cursor(rope, ix); - let result = if !word.is_empty() { - let default_info = &Word::default(); - let info = data - .words - .iter() - .find(|x| { - x.token.to_lowercase() - == (word.to_string().to_lowercase().as_str()) - }) - .unwrap_or(&default_info); - Some(Hover { - contents: lsp_types::HoverContents::Markup( - lsp_types::MarkupContent { - kind: lsp_types::MarkupKind::Markdown, - value: format!( - "# `{}` `{}`\n\n{}", - info.token, info.stack, info.help - ), - }, - ), - range: None, - }) - } else { - None - }; - let result = serde_json::to_value(&result) - .expect("Must be able to serialize the Hover"); - let resp = Response { - id, - result: Some(result), - error: None, - }; - connection - .sender - .send(Message::Response(resp)) - .map_err(|err| Error::SendError(err.to_string()))?; - continue; - } - Err(Error::ExtractRequestError(req)) => req, - Err(err) => panic!("{err:?}"), - // Err(err @ ExtractError::JsonError { .. }) => panic!("{err:?}"), - // Err(ExtractError::MethodMismatch(req)) => req, - }; - match cast::(req) { - Ok((id, params)) => { - eprintln!("#{id}: {params:?}"); - //TODO: recurse parse follow includes /^[iI][nN][cC][lL][uU][dD][eE] +(.*\..*)/ - //TODO: find colon defines /: +(?:.|\n)*;/ - //TODO: generate Vec from line num and col from the matching - let mut ret: Vec = vec![]; - eprintln!( - "{:?}", - ¶ms.text_document_position_params.text_document.uri - ); - let rope = files - .get_mut( - ¶ms - .text_document_position_params - .text_document - .uri - .to_string(), - ) - .expect("Must be able to get rope for lang"); - let ix = rope.get_ix(¶ms); - if ix >= rope.len_chars() { - eprintln!( - "IX OUT OF BOUNDS {} {} {}", - ix, - rope.len_chars(), - rope.len_bytes() - ); - break; - } - let word = word_on_or_before_cursor(rope, ix); - for (file, rope) in files.iter() { - eprintln!("Word: {}", word); - let progn = rope.to_string(); - let mut lexer = Lexer::new(progn.as_str()); - let tokens = lexer.parse(); - - for result in tokens.find_variant_sublists_from_to( - discriminant(&Token::Colon(Data::default())), - discriminant(&Token::Semicolon(Data::default())), - ) { - eprintln!("{:?}", result); - let tok = Token::Illegal(Data::new(0, 0, "")); - let begin = result.first().unwrap_or(&tok).get_data(); - let end = result.last().unwrap_or(&tok).get_data(); - if let Ok(uri) = Url::from_file_path(file) { - ret.push(Location { - uri, - range: Range { - start: begin.to_position_start(rope), - end: end.to_position_end(rope), - }, - }); - } else { - eprintln!("Failed to parse URI for {}", file); - } - } - } - let result = Some(GotoDefinitionResponse::Array(ret)); - let result = serde_json::to_value(&result) - .expect("Must be able to serialize the GotoDefinitionResponse"); - let resp = Response { - id, - result: Some(result), - error: None, - }; - connection - .sender - .send(Message::Response(resp)) - .map_err(|err| Error::SendError(err.to_string()))?; - continue; - } - Err(Error::ExtractRequestError(req)) => req, - Err(err) => panic!("{err:?}"), - // Err(err @ ExtractError::JsonError { .. }) => panic!("{err:?}"), - // Err(ExtractError::MethodMismatch(req)) => req, - }; - // ... + eprintln!("got request: {:?}", request.method); + if handle_hover(&request, &connection, &data, &mut files).is_ok() { + continue; + } + if handle_completion(&request, &connection, &data, &mut files).is_ok() { + continue; + } + if handle_goto_definition(&request, &connection, &data, &mut files).is_ok() { + continue; + } } Message::Response(resp) => { eprintln!("got response: {resp:?}"); } - Message::Notification(not) => { - eprintln!("got notification: {not:?}"); - match cast_notification::(not.clone()) - { - Ok(params) => { - if let std::collections::hash_map::Entry::Vacant(e) = - files.entry(params.text_document.uri.to_string()) - { - let rope = Rope::from_str(params.text_document.text.as_str()); - e.insert(rope); - } - continue; - } - Err(Error::ExtractNotificationError(req)) => req, - Err(err) => panic!("{err:?}"), - // Err(err @ ExtractError::JsonError { .. }) => panic!("{err:?}"), - // Err(ExtractError::MethodMismatch(not)) => not, - }; - match cast_notification::( - not.clone(), - ) { - Ok(params) => { - let rope = files - .get_mut(¶ms.text_document.uri.to_string()) - .expect("Must be able to get rope for lang"); - for change in params.content_changes { - let range = change.range.unwrap_or_default(); - let start = rope.line_to_char(range.start.line as usize) - + range.start.character as usize; - let end = rope.line_to_char(range.end.line as usize) - + range.end.character as usize; - rope.remove(start..end); - rope.insert(start, change.text.as_str()); - } - } - Err(_) => todo!(), + Message::Notification(notification) => { + eprintln!("got notification: {:?}", notification.method); + if handle_did_open_text_document(¬ification, &mut files).is_ok() { + continue; + } + if handle_did_change_text_document(¬ification, &mut files).is_ok() { + continue; } } } @@ -358,43 +107,3 @@ fn load_dir( } Ok(()) } - -fn word_on_or_before_cursor(rope: &Rope, ix: usize) -> String { - let word_on_cursor = rope.word_at_char(ix); - // with helix, you typically end up with having a selected word including the previous space - // this means we should also look for a word behind the cursor - //TODO: make look-behind cleaner - let word_behind_cursor = if ix > 0 { - rope.word_at_char(ix - 1) - } else { - word_on_cursor - }; - eprintln!( - "Word on `{}` before `{}`", - word_on_cursor, word_behind_cursor - ); - let word = if word_on_cursor.len_chars() >= word_behind_cursor.len_chars() { - word_on_cursor - } else { - word_behind_cursor - }; - word.to_string() -} - -fn cast(req: Request) -> Result<(RequestId, R::Params)> -where - R: lsp_types::request::Request, - R::Params: serde::de::DeserializeOwned, -{ - req.extract(R::METHOD) - .map_err(error::Error::ExtractRequestError) -} - -fn cast_notification(req: Notification) -> Result -where - N: lsp_types::notification::Notification, - N::Params: serde::de::DeserializeOwned, -{ - req.extract(N::METHOD) - .map_err(error::Error::ExtractNotificationError) -} diff --git a/src/utils/handlers/mod.rs b/src/utils/handlers/mod.rs new file mode 100644 index 0000000..6665696 --- /dev/null +++ b/src/utils/handlers/mod.rs @@ -0,0 +1,27 @@ +#[allow(unused_imports)] +use crate::prelude::*; + +pub mod notification_did_change; +pub mod notification_did_open; +pub mod request_completion; +pub mod request_goto_definition; +pub mod request_hover; + +use lsp_server::{Notification, Request, RequestId}; + +pub fn cast(req: Request) -> Result<(RequestId, R::Params)> +where + R: lsp_types::request::Request, + R::Params: serde::de::DeserializeOwned, +{ + req.extract(R::METHOD).map_err(Error::ExtractRequestError) +} + +pub fn cast_notification(req: Notification) -> Result +where + N: lsp_types::notification::Notification, + N::Params: serde::de::DeserializeOwned, +{ + req.extract(N::METHOD) + .map_err(Error::ExtractNotificationError) +} diff --git a/src/utils/handlers/notification_did_change.rs b/src/utils/handlers/notification_did_change.rs new file mode 100644 index 0000000..c69c9d7 --- /dev/null +++ b/src/utils/handlers/notification_did_change.rs @@ -0,0 +1,33 @@ +#[allow(unused_imports)] +use crate::prelude::*; + +use std::collections::HashMap; + +use lsp_server::Notification; +use ropey::Rope; + +use super::cast_notification; + +pub fn handle_did_change_text_document( + notification: &Notification, + files: &mut HashMap, +) -> Result<()> { + match cast_notification::(notification.clone()) + { + Ok(params) => { + let rope = files + .get_mut(¶ms.text_document.uri.to_string()) + .expect("Must be able to get rope for lang"); + for change in params.content_changes { + let range = change.range.unwrap_or_default(); + let start = + rope.line_to_char(range.start.line as usize) + range.start.character as usize; + let end = rope.line_to_char(range.end.line as usize) + range.end.character as usize; + rope.remove(start..end); + rope.insert(start, change.text.as_str()); + } + Ok(()) + } + Err(_) => todo!(), + } +} diff --git a/src/utils/handlers/notification_did_open.rs b/src/utils/handlers/notification_did_open.rs new file mode 100644 index 0000000..37e6015 --- /dev/null +++ b/src/utils/handlers/notification_did_open.rs @@ -0,0 +1,28 @@ +#[allow(unused_imports)] +use crate::prelude::*; + +use std::collections::HashMap; + +use lsp_server::Notification; +use ropey::Rope; + +use super::cast_notification; + +pub fn handle_did_open_text_document( + notification: &Notification, + files: &mut HashMap, +) -> Result<()> { + match cast_notification::(notification.clone()) { + Ok(params) => { + if let std::collections::hash_map::Entry::Vacant(e) = + files.entry(params.text_document.uri.to_string()) + { + let rope = Rope::from_str(params.text_document.text.as_str()); + e.insert(rope); + } + Ok(()) + } + Err(Error::ExtractNotificationError(req)) => Err(Error::ExtractNotificationError(req)), + Err(err) => panic!("{err:?}"), + } +} diff --git a/src/utils/handlers/request_completion.rs b/src/utils/handlers/request_completion.rs new file mode 100644 index 0000000..37799b5 --- /dev/null +++ b/src/utils/handlers/request_completion.rs @@ -0,0 +1,94 @@ +#[allow(unused_imports)] +use crate::prelude::*; +use crate::{ + utils::{ + ropey::{get_ix::GetIx, word_at::WordAt, RopeSliceIsLower}, + HashMapGetForLSPParams, + }, + words::Words, +}; + +use std::collections::HashMap; + +use lsp_server::{Connection, Message, Request, Response}; +use lsp_types::{request::Completion, CompletionItem, CompletionResponse}; +use ropey::Rope; + +use super::cast; + +pub fn handle_completion( + req: &Request, + connection: &Connection, + data: &Words, + files: &mut HashMap, +) -> Result<()> { + match cast::(req.clone()) { + Ok((id, params)) => { + eprintln!("#{id}: {params:?}"); + let rope = if let Some(rope) = files.for_position_param(¶ms.text_document_position) + { + rope + } else { + return Err(Error::NoSuchFile( + params.text_document_position.text_document.uri.to_string(), + )); + }; + let mut ix = rope.get_ix(¶ms); + if ix >= rope.len_chars() { + return Err(Error::OutOfBounds(ix)); + } + if let Some(char_at_ix) = rope.get_char(ix) { + if char_at_ix.is_whitespace() && ix > 0 { + ix -= 1; + } + } + let word = rope.word_at(ix); + let result = if word.len_chars() > 0 { + eprintln!("Found word {}", word); + let use_lower = word.is_lowercase(); + let mut ret = vec![]; + let candidates = data.words.iter().filter(|x| { + x.token + .to_lowercase() + .starts_with(word.to_string().to_lowercase().as_str()) + }); + for candidate in candidates { + let label = candidate.token.to_owned(); + let label = if use_lower { + label.to_lowercase() + } else { + label + }; + ret.push(CompletionItem { + label, + detail: Some(candidate.stack.to_owned()), + documentation: Some(lsp_types::Documentation::MarkupContent( + lsp_types::MarkupContent { + kind: lsp_types::MarkupKind::Markdown, + value: candidate.documentation(), + }, + )), + ..Default::default() + }); + } + Some(CompletionResponse::Array(ret)) + } else { + None + }; + let result = serde_json::to_value(result) + .expect("Must be able to serialize the CompletionResponse"); + let resp = Response { + id, + result: Some(result), + error: None, + }; + connection + .sender + .send(Message::Response(resp)) + .map_err(|err| Error::SendError(err.to_string()))?; + Ok(()) + } + Err(Error::ExtractRequestError(req)) => Err(Error::ExtractRequestError(req)), + Err(err) => panic!("{err:?}"), + } +} diff --git a/src/utils/handlers/request_goto_definition.rs b/src/utils/handlers/request_goto_definition.rs new file mode 100644 index 0000000..dc753f9 --- /dev/null +++ b/src/utils/handlers/request_goto_definition.rs @@ -0,0 +1,99 @@ +#[allow(unused_imports)] +use crate::prelude::*; +use crate::{ + utils::{ + data_to_position::ToPosition, + find_variant_sublists_from_to::FindVariantSublistsFromTo, + ropey::{get_ix::GetIx, word_on_or_before::WordOnOrBefore}, + HashMapGetForLSPParams, + }, + words::Words, +}; + +use std::{collections::HashMap, mem::discriminant}; + +use forth_lexer::{ + parser::Lexer, + token::{Data, Token}, +}; +use lsp_server::{Connection, Message, Request, Response}; +use lsp_types::{request::GotoDefinition, GotoDefinitionResponse, Location, Range, Url}; +use ropey::Rope; + +use super::cast; + +pub fn handle_goto_definition( + req: &Request, + connection: &Connection, + _data: &Words, + files: &mut HashMap, +) -> Result<()> { + match cast::(req.clone()) { + Ok((id, params)) => { + eprintln!("#{id}: {params:?}"); + let mut ret: Vec = vec![]; + let rope = if let Some(rope) = + files.for_position_param(¶ms.text_document_position_params) + { + rope + } else { + return Err(Error::NoSuchFile( + params + .text_document_position_params + .text_document + .uri + .to_string(), + )); + }; + let ix = rope.get_ix(¶ms); + if ix >= rope.len_chars() { + return Err(Error::OutOfBounds(ix)); + } + let word = rope.word_on_or_before(ix).to_string(); + for (file, rope) in files.iter() { + eprintln!("Word: {}", word); + let progn = rope.to_string(); + let mut lexer = Lexer::new(progn.as_str()); + let tokens = lexer.parse(); + + for result in tokens.find_variant_sublists_from_to( + discriminant(&Token::Colon(Data::default())), + discriminant(&Token::Semicolon(Data::default())), + ) { + eprintln!("{:?}", result); + let tok = Token::Illegal(Data::new(0, 0, "")); + let begin = result.first().unwrap_or(&tok).get_data(); + let end = result.last().unwrap_or(&tok).get_data(); + if let Ok(uri) = Url::from_file_path(file) { + ret.push(Location { + uri, + range: Range { + start: begin.to_position_start(rope), + end: end.to_position_end(rope), + }, + }); + } else { + eprintln!("Failed to parse URI for {}", file); + } + } + } + let result = Some(GotoDefinitionResponse::Array(ret)); + let result = serde_json::to_value(result) + .expect("Must be able to serialize the GotoDefinitionResponse"); + let resp = Response { + id, + result: Some(result), + error: None, + }; + connection + .sender + .send(Message::Response(resp)) + .map_err(|err| Error::SendError(err.to_string()))?; + Ok(()) + } + Err(Error::ExtractRequestError(req)) => Err(Error::ExtractRequestError(req)), + Err(err) => panic!("{err:?}"), + // Err(err @ ExtractError::JsonError { .. }) => panic!("{err:?}"), + // Err(ExtractError::MethodMismatch(req)) => req, + } +} diff --git a/src/utils/handlers/request_hover.rs b/src/utils/handlers/request_hover.rs new file mode 100644 index 0000000..c97ceab --- /dev/null +++ b/src/utils/handlers/request_hover.rs @@ -0,0 +1,80 @@ +#[allow(unused_imports)] +use crate::prelude::*; +use crate::{ + utils::{ + ropey::{get_ix::GetIx, word_on_or_before::WordOnOrBefore}, + HashMapGetForLSPParams, + }, + words::{Word, Words}, +}; + +use std::collections::HashMap; + +use lsp_server::{Connection, Message, Request, Response}; +use lsp_types::{request::HoverRequest, Hover}; +use ropey::Rope; + +use super::cast; + +pub fn handle_hover( + req: &Request, + connection: &Connection, + data: &Words, + files: &mut HashMap, +) -> Result<()> { + match cast::(req.clone()) { + Ok((id, params)) => { + eprintln!("#{id}: {params:?}"); + let rope = if let Some(rope) = + files.for_position_param(¶ms.text_document_position_params) + { + rope + } else { + return Err(Error::NoSuchFile( + params + .text_document_position_params + .text_document + .uri + .to_string(), + )); + }; + let ix = rope.get_ix(¶ms); + if ix >= rope.len_chars() { + return Err(Error::OutOfBounds(ix)); + } + let word = rope.word_on_or_before(ix); + let result = if !word.len_chars() > 0 { + let default_info = &Word::default(); + let info = data + .words + .iter() + .find(|x| x.token.to_lowercase() == (word.to_string().to_lowercase().as_str())) + .unwrap_or(&default_info); + Some(Hover { + contents: lsp_types::HoverContents::Markup(lsp_types::MarkupContent { + kind: lsp_types::MarkupKind::Markdown, + value: info.documentation(), + }), + range: None, + }) + } else { + None + }; + let result = serde_json::to_value(result).expect("Must be able to serialize the Hover"); + let resp = Response { + id, + result: Some(result), + error: None, + }; + connection + .sender + .send(Message::Response(resp)) + .map_err(|err| Error::SendError(err.to_string()))?; + Ok(()) + } + Err(Error::ExtractRequestError(req)) => Err(Error::ExtractRequestError(req)), + Err(err) => panic!("{err:?}"), + // Err(err @ ExtractError::JsonError { .. }) => panic!("{err:?}"), + // Err(ExtractError::MethodMismatch(req)) => req, + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index d5315cb..69be7fc 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -4,5 +4,19 @@ use crate::prelude::*; pub mod data_to_position; pub mod find_variant_sublists; pub mod find_variant_sublists_from_to; -pub mod ropey_get_ix; -pub mod ropey_word_at_char; +pub mod handlers; +pub mod ropey; +pub mod server_capabilities; + +use lsp_types::TextDocumentPositionParams; +use std::collections::HashMap; + +pub trait HashMapGetForLSPParams { + fn for_position_param(&mut self, params: &TextDocumentPositionParams) -> Option<&mut T>; +} + +impl HashMapGetForLSPParams for HashMap { + fn for_position_param(&mut self, params: &TextDocumentPositionParams) -> Option<&mut T> { + self.get_mut(¶ms.text_document.uri.to_string()) + } +} diff --git a/src/utils/ropey_get_ix.rs b/src/utils/ropey/get_ix.rs similarity index 100% rename from src/utils/ropey_get_ix.rs rename to src/utils/ropey/get_ix.rs diff --git a/src/utils/ropey/mod.rs b/src/utils/ropey/mod.rs new file mode 100644 index 0000000..8dd1e69 --- /dev/null +++ b/src/utils/ropey/mod.rs @@ -0,0 +1,21 @@ +#[allow(unused_imports)] +use crate::prelude::*; + +pub mod get_ix; +pub mod word_at; +pub mod word_on_or_before; + +use ropey::RopeSlice; +pub trait RopeSliceIsLower { + fn is_lowercase(&self) -> bool; +} + +impl<'a> RopeSliceIsLower for RopeSlice<'a> { + fn is_lowercase(&self) -> bool { + if let Some(chr) = self.get_char(self.len_chars() - 1) { + chr.is_lowercase() + } else { + false + } + } +} diff --git a/src/utils/ropey_word_at_char.rs b/src/utils/ropey/word_at.rs similarity index 76% rename from src/utils/ropey_word_at_char.rs rename to src/utils/ropey/word_at.rs index 1614f3a..8a438eb 100644 --- a/src/utils/ropey_word_at_char.rs +++ b/src/utils/ropey/word_at.rs @@ -1,10 +1,10 @@ use ropey::{Rope, RopeSlice}; -pub trait WordAtChar { - fn word_at_char(&self, char: usize) -> RopeSlice; +pub trait WordAt { + fn word_at(&self, char: usize) -> RopeSlice; } -impl WordAtChar for Rope { - fn word_at_char(&self, chix: usize) -> RopeSlice { +impl WordAt for Rope { + fn word_at(&self, chix: usize) -> RopeSlice { if self.char(chix).is_whitespace() { return self.slice(chix..chix); } @@ -19,6 +19,7 @@ impl WordAtChar for Rope { self.slice(min..(max + 1)) } } + #[cfg(test)] mod tests { use ropey::Rope; @@ -28,37 +29,37 @@ mod tests { #[test] fn word_at_zero() { let rope = Rope::from_str("find the first word"); - let word = rope.word_at_char(0); + let word = rope.word_at(0); assert_eq!("find", word); } #[test] fn word_at_center() { let rope = Rope::from_str("Should find this"); - let word = rope.word_at_char(8); + let word = rope.word_at(8); assert_eq!("find", word); } #[test] fn word_at_begin() { let rope = Rope::from_str("Should find this"); - let word = rope.word_at_char(7); + let word = rope.word_at(7); assert_eq!("find", word); } #[test] fn word_at_end() { let rope = Rope::from_str("Should find this"); - let word = rope.word_at_char(10); + let word = rope.word_at(10); assert_eq!("find", word); } #[test] fn word_at_after() { let rope = Rope::from_str("Should find this"); - let word = rope.word_at_char(11); + let word = rope.word_at(11); assert_eq!("", word); } #[test] fn word_at_single() { let rope = Rope::from_str("Should + find this"); - let word = rope.word_at_char(7); + let word = rope.word_at(7); assert_eq!("+", word); } } diff --git a/src/utils/ropey/word_on_or_before.rs b/src/utils/ropey/word_on_or_before.rs new file mode 100644 index 0000000..f52c664 --- /dev/null +++ b/src/utils/ropey/word_on_or_before.rs @@ -0,0 +1,46 @@ +use ropey::{Rope, RopeSlice}; + +use super::word_at::WordAt; + +pub trait WordOnOrBefore { + fn word_on_or_before(&self, char: usize) -> RopeSlice; +} + +impl WordOnOrBefore for Rope { + fn word_on_or_before(&self, ix: usize) -> RopeSlice { + let word_on_cursor = self.word_at(ix); + // with helix, you typically end up with having a selected word including the previous space + // this means we should also look for a word behind the cursor + //TODO: make look-behind cleaner + let word_before_cursor = if ix > 0 { + self.word_at(ix - 1) + } else { + word_on_cursor + }; + if word_on_cursor.len_chars() > 0 { + word_on_cursor + } else { + word_before_cursor + } + } +} + +#[cfg(test)] +mod tests { + use ropey::Rope; + + use super::*; + + #[test] + fn word_at_end() { + let rope = Rope::from_str("Should find this"); + let word = rope.word_on_or_before(10); + assert_eq!("find", word); + } + #[test] + fn word_at_after() { + let rope = Rope::from_str("Should find this"); + let word = rope.word_on_or_before(11); + assert_eq!("find", word); + } +} diff --git a/src/utils/server_capabilities.rs b/src/utils/server_capabilities.rs new file mode 100644 index 0000000..3d0af2b --- /dev/null +++ b/src/utils/server_capabilities.rs @@ -0,0 +1,21 @@ +use lsp_types::{OneOf, ServerCapabilities, TextDocumentSyncKind}; + +pub fn forth_lsp_capabilities() -> ServerCapabilities { + ServerCapabilities { + text_document_sync: Some(lsp_types::TextDocumentSyncCapability::Kind( + TextDocumentSyncKind::INCREMENTAL, + )), + // workspace_symbol_provider + workspace: Some(lsp_types::WorkspaceServerCapabilities { + workspace_folders: Some(lsp_types::WorkspaceFoldersServerCapabilities { + supported: Some(true), + change_notifications: Some(OneOf::Left(false)), + }), + file_operations: None, + }), + hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)), + definition_provider: Some(OneOf::Left(true)), + completion_provider: Some(lsp_types::CompletionOptions::default()), + ..Default::default() + } +} diff --git a/src/words.rs b/src/words.rs index c11ae82..152eeac 100644 --- a/src/words.rs +++ b/src/words.rs @@ -10,6 +10,12 @@ pub struct Words<'a> { pub words: Vec<&'a Word<'a>>, } +impl<'a> Word<'a> { + pub fn documentation(&self) -> String { + format!("# `{}` `{}`\n\n{}", self.token, self.stack, self.help) + } +} + impl Default for Words<'_> { fn default() -> Words<'static> { Words {