From 6039755d7333f18a159ac62c000137d3948b9665 Mon Sep 17 00:00:00 2001 From: Elijah Potter Date: Tue, 23 Jan 2024 10:29:53 -0700 Subject: [PATCH] `harper-ls` now runs diagnostics on change --- harper-core/src/document.rs | 4 + harper-core/src/linting/spell_check.rs | 2 +- harper-core/src/parsing/lexer.rs | 5 +- harper-core/src/parsing/token.rs | 2 + harper-ls/src/backend.rs | 109 +++++++++++++++----- harper-ls/src/diagnostics.rs | 132 ++----------------------- harper-ls/src/main.rs | 1 + harper-ls/src/pos_conv.rs | 99 +++++++++++++++++++ 8 files changed, 200 insertions(+), 154 deletions(-) create mode 100644 harper-ls/src/pos_conv.rs diff --git a/harper-core/src/document.rs b/harper-core/src/document.rs index aa64c2de..ec5c570e 100644 --- a/harper-core/src/document.rs +++ b/harper-core/src/document.rs @@ -244,6 +244,10 @@ impl Document { }) } + pub fn get_full_content(&self) -> &[char] { + &self.source + } + pub fn apply_suggestion(&mut self, suggestion: &Suggestion, span: Span) { match suggestion { Suggestion::ReplaceWith(chars) => { diff --git a/harper-core/src/linting/spell_check.rs b/harper-core/src/linting/spell_check.rs index 97bebff8..b4dc8b9f 100644 --- a/harper-core/src/linting/spell_check.rs +++ b/harper-core/src/linting/spell_check.rs @@ -63,7 +63,7 @@ impl Linter for SpellCheck { } // The error is likely by omission - if key_dist > 4 { + if key_dist > 2 { usize::MAX - v.len() } // The error is likely by replacement diff --git a/harper-core/src/parsing/lexer.rs b/harper-core/src/parsing/lexer.rs index 237d636b..0481024d 100644 --- a/harper-core/src/parsing/lexer.rs +++ b/harper-core/src/parsing/lexer.rs @@ -30,12 +30,8 @@ pub fn lex_to_end_md(source: &[char]) -> Vec { traversed_chars += source_str[traversed_bytes..range.start].chars().count(); traversed_bytes = range.start; - dbg!(text.to_string()); - let mut new_tokens = lex_to_end_str(text); - dbg!(&new_tokens); - new_tokens .iter_mut() .for_each(|token| token.span.offset(traversed_chars)); @@ -196,6 +192,7 @@ fn lex_punctuation(source: &[char]) -> Option { use Punctuation::*; let punct = match c { + '%' => Percent, '’' => Apostrophe, '\'' => Apostrophe, '.' => Period, diff --git a/harper-core/src/parsing/token.rs b/harper-core/src/parsing/token.rs index 9979baa2..3afe191b 100644 --- a/harper-core/src/parsing/token.rs +++ b/harper-core/src/parsing/token.rs @@ -86,6 +86,8 @@ pub enum Punctuation { Hash, /// ' Apostrophe, + /// % + Percent, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] diff --git a/harper-ls/src/backend.rs b/harper-ls/src/backend.rs index d17bf94f..6e82ad92 100644 --- a/harper-ls/src/backend.rs +++ b/harper-ls/src/backend.rs @@ -1,39 +1,91 @@ -use std::ops::DerefMut; +use std::{collections::HashMap, fs, ops::DerefMut}; -use harper_core::{Dictionary, LintSet}; +use harper_core::{Dictionary, Document, LintSet, Linter}; use tokio::{sync::Mutex, time::Instant}; use tower_lsp::{ jsonrpc::Result, lsp_types::{ - notification::PublishDiagnostics, CodeActionOrCommand, CodeActionParams, - CodeActionProviderCapability, CodeActionResponse, DidChangeTextDocumentParams, + notification::PublishDiagnostics, CodeAction, CodeActionOrCommand, CodeActionParams, + CodeActionProviderCapability, CodeActionResponse, Diagnostic, DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, InitializeParams, InitializeResult, InitializedParams, MessageType, - PublishDiagnosticsParams, ServerCapabilities, Url, + PublishDiagnosticsParams, Range, ServerCapabilities, TextDocumentSyncCapability, + TextDocumentSyncKind, TextDocumentSyncOptions, TextDocumentSyncSaveOptions, Url, }, Client, LanguageServer, }; -use crate::diagnostics::{generate_code_actions, generate_diagnostics}; +use crate::{ + diagnostics::{lint_to_code_actions, lints_to_diagnostics}, + pos_conv::range_to_span, +}; pub struct Backend { client: Client, linter: Mutex, + files: Mutex>, } impl Backend { + async fn update_document_from_file(&self, url: &Url) { + let content = fs::read_to_string(url.path()).unwrap(); + self.update_document(url, &content).await; + } + + async fn update_document(&self, url: &Url, text: &str) { + let doc = Document::new(text, true); + let mut files = self.files.lock().await; + files.insert(url.clone(), doc); + } + + async fn generate_code_actions(&self, url: &Url, range: Range) -> Result> { + let files = self.files.lock().await; + let Some(document) = files.get(url) else { + return Ok(vec![]); + }; + + let mut linter = self.linter.lock().await; + let lints = linter.lint(document); + let source_chars = document.get_full_content(); + + // Find lints whose span overlaps with range + let span = range_to_span(source_chars, range); + + let actions = lints + .into_iter() + .filter(|lint| lint.span.overlaps_with(span)) + .flat_map(|lint| lint_to_code_actions(&lint, url, source_chars).collect::>()) + .collect(); + + Ok(actions) + } + pub fn new(client: Client) -> Self { let dictionary = Dictionary::new(); let linter = Mutex::new(LintSet::new().with_standard(dictionary)); - Self { client, linter } + Self { + client, + linter, + files: Mutex::new(HashMap::new()), + } } - async fn publish_diagnostics(&self, url: &Url) { - let start_time = Instant::now(); + async fn generate_diagnostics(&self, url: &Url) -> Vec { + let files = self.files.lock().await; + + let Some(document) = files.get(url) else { + return vec![]; + }; + let mut linter = self.linter.lock().await; + let lints = linter.lint(document); + + lints_to_diagnostics(document.get_full_content(), &lints) + } - let diagnostics = generate_diagnostics(url, linter.deref_mut()).unwrap(); + async fn publish_diagnostics(&self, url: &Url) { + let diagnostics = self.generate_diagnostics(url).await; let result = PublishDiagnosticsParams { uri: url.clone(), @@ -44,20 +96,6 @@ impl Backend { self.client .send_notification::(result) .await; - - let end_time = Instant::now(); - - let duration = end_time - start_time; - - self.client - .log_message( - MessageType::LOG, - format!( - "Took {} ms to generate and publish diagnostics.", - duration.as_millis() - ), - ) - .await; } } @@ -68,6 +106,15 @@ impl LanguageServer for Backend { server_info: None, capabilities: ServerCapabilities { code_action_provider: Some(CodeActionProviderCapability::Simple(true)), + text_document_sync: Some(TextDocumentSyncCapability::Options( + TextDocumentSyncOptions { + open_close: Some(true), + change: Some(TextDocumentSyncKind::FULL), + will_save: None, + will_save_wait_until: None, + save: Some(TextDocumentSyncSaveOptions::Supported(true)), + }, + )), ..Default::default() }, }) @@ -88,6 +135,8 @@ impl LanguageServer for Backend { .log_message(MessageType::INFO, "File saved!") .await; + self.update_document_from_file(¶ms.text_document.uri) + .await; self.publish_diagnostics(¶ms.text_document.uri).await; } @@ -100,10 +149,16 @@ impl LanguageServer for Backend { } async fn did_change(&self, params: DidChangeTextDocumentParams) { + let Some(last) = params.content_changes.last() else { + return; + }; + self.client .log_message(MessageType::INFO, "File changed!") .await; + self.update_document(¶ms.text_document.uri, &last.text) + .await; self.publish_diagnostics(¶ms.text_document.uri).await; } @@ -114,9 +169,9 @@ impl LanguageServer for Backend { } async fn code_action(&self, params: CodeActionParams) -> Result> { - let mut linter = self.linter.lock().await; - let actions = - generate_code_actions(¶ms.text_document.uri, params.range, linter.deref_mut())?; + let actions = self + .generate_code_actions(¶ms.text_document.uri, params.range) + .await?; self.client .log_message(MessageType::INFO, format!("{:?}", actions)) diff --git a/harper-ls/src/diagnostics.rs b/harper-ls/src/diagnostics.rs index 9932c590..80e35173 100644 --- a/harper-ls/src/diagnostics.rs +++ b/harper-ls/src/diagnostics.rs @@ -1,49 +1,19 @@ -use harper_core::{Document, Lint, LintSet, Linter, Span, Suggestion}; +use harper_core::{Lint, Span, Suggestion}; use std::collections::HashMap; -use std::fs::read; -use tower_lsp::jsonrpc::{ErrorCode, Result}; use tower_lsp::lsp_types::{ CodeAction, CodeActionKind, Diagnostic, Position, Range, TextEdit, Url, WorkspaceEdit, }; -fn lint_file(file_url: &Url, linter: &mut impl Linter) -> Result<(Vec, Vec)> { - let file_str = open_url(file_url)?; - let source_chars: Vec<_> = file_str.chars().collect(); - let document = Document::new(&file_str, true); - Ok((linter.lint(&document), source_chars)) -} - -pub fn generate_diagnostics(file_url: &Url, linter: &mut impl Linter) -> Result> { - let (lints, source_chars) = lint_file(file_url, linter)?; - - let diagnostics = lints - .into_iter() - .map(|lint| lint_to_diagnostic(lint, &source_chars)) - .collect(); - - Ok(diagnostics) -} +use crate::pos_conv::span_to_range; -pub fn generate_code_actions( - url: &Url, - range: Range, - linter: &mut impl Linter, -) -> Result> { - let (lints, source_chars) = lint_file(url, linter)?; - - // Find lints whose span overlaps with range - let span = range_to_span(&source_chars, range); - - let actions = lints - .into_iter() - .filter(|lint| lint.span.overlaps_with(span)) - .flat_map(|lint| lint_to_code_actions(&lint, url, &source_chars).collect::>()) - .collect(); - - Ok(actions) +pub fn lints_to_diagnostics(source: &[char], lints: &[Lint]) -> Vec { + lints + .iter() + .map(|lint| lint_to_diagnostic(lint, source)) + .collect() } -fn lint_to_code_actions<'a>( +pub fn lint_to_code_actions<'a>( lint: &'a Lint, url: &'a Url, source: &'a [char], @@ -76,13 +46,7 @@ fn lint_to_code_actions<'a>( }) } -fn open_url(url: &Url) -> Result { - let file = read(url.path()) - .map_err(|_err| tower_lsp::jsonrpc::Error::new(ErrorCode::InternalError))?; - Ok(String::from_utf8(file).unwrap()) -} - -fn lint_to_diagnostic(lint: Lint, source: &[char]) -> Diagnostic { +fn lint_to_diagnostic(lint: &Lint, source: &[char]) -> Diagnostic { let range = span_to_range(source, lint.span); Diagnostic { @@ -91,85 +55,9 @@ fn lint_to_diagnostic(lint: Lint, source: &[char]) -> Diagnostic { code: None, code_description: None, source: Some("Harper".to_string()), - message: lint.message, + message: lint.message.clone(), related_information: None, tags: None, data: None, } } - -fn span_to_range(source: &[char], span: Span) -> Range { - let start = index_to_position(source, span.start); - let end = index_to_position(source, span.end); - - Range { start, end } -} - -fn index_to_position(source: &[char], index: usize) -> Position { - let before = &source[0..index]; - let newline_indices: Vec<_> = before - .iter() - .enumerate() - .filter_map(|(idx, c)| if *c == '\n' { Some(idx) } else { None }) - .collect(); - - let lines = newline_indices.len(); - let cols = index - newline_indices.last().copied().unwrap_or(1) - 1; - - Position { - line: lines as u32, - character: cols as u32, - } -} - -fn position_to_index(source: &[char], position: Position) -> usize { - let newline_indices = - source - .iter() - .enumerate() - .filter_map(|(idx, c)| if *c == '\n' { Some(idx) } else { None }); - - let line_start_idx = newline_indices - .take(position.line as usize) - .last() - .unwrap_or(0); - - line_start_idx + position.character as usize + 1 -} - -fn range_to_span(source: &[char], range: Range) -> Span { - let start = position_to_index(source, range.start); - let end = position_to_index(source, range.end); - - Span::new(start, end) -} - -#[cfg(test)] -mod tests { - use tower_lsp::lsp_types::Position; - - use super::{index_to_position, position_to_index}; - - #[test] - fn reversible_position_conv() { - let source: Vec<_> = "There was a man,\n his voice had timbre,\n unlike a boy." - .chars() - .collect(); - - let a = Position { - line: 2, - character: 3, - }; - - let b = position_to_index(&source, a); - - assert_eq!(b, 43); - - let c = index_to_position(&source, b); - - let d = position_to_index(&source, a); - - assert_eq!(a, c); - assert_eq!(b, d); - } -} diff --git a/harper-ls/src/main.rs b/harper-ls/src/main.rs index e4fa7724..2e0870a2 100644 --- a/harper-ls/src/main.rs +++ b/harper-ls/src/main.rs @@ -1,6 +1,7 @@ use tokio::net::TcpListener; mod backend; mod diagnostics; +mod pos_conv; use backend::Backend; use clap::Parser; diff --git a/harper-ls/src/pos_conv.rs b/harper-ls/src/pos_conv.rs new file mode 100644 index 00000000..1699e115 --- /dev/null +++ b/harper-ls/src/pos_conv.rs @@ -0,0 +1,99 @@ +use harper_core::Span; +use tower_lsp::lsp_types::{Position, Range}; + +/// This module includes various conversions from the index-based [`Span`]s that Harper uses, and +/// the Ranges that the LSP uses. + +pub fn span_to_range(source: &[char], span: Span) -> Range { + let start = index_to_position(source, span.start); + let end = index_to_position(source, span.end); + + Range { start, end } +} + +fn index_to_position(source: &[char], index: usize) -> Position { + let before = &source[0..index]; + let newline_indices: Vec<_> = before + .iter() + .enumerate() + .filter_map(|(idx, c)| if *c == '\n' { Some(idx + 1) } else { None }) + .collect(); + + let lines = newline_indices.len(); + let cols = index - newline_indices.last().copied().unwrap_or(0); + + Position { + line: lines as u32, + character: cols as u32, + } +} + +fn position_to_index(source: &[char], position: Position) -> usize { + let newline_indices = + source + .iter() + .enumerate() + .filter_map(|(idx, c)| if *c == '\n' { Some(idx + 1) } else { None }); + + let line_start_idx = newline_indices + .take(position.line as usize) + .last() + .unwrap_or(0); + + line_start_idx + position.character as usize +} + +pub fn range_to_span(source: &[char], range: Range) -> Span { + let start = position_to_index(source, range.start); + let end = position_to_index(source, range.end); + + Span::new(start, end) +} + +#[cfg(test)] +mod tests { + use tower_lsp::lsp_types::Position; + + use super::{index_to_position, position_to_index}; + + #[test] + fn first_line_correct() { + let source: Vec<_> = "Hello there.".chars().collect(); + + let start = Position { + line: 0, + character: 4, + }; + + let i = position_to_index(&source, start); + + assert_eq!(i, 4); + + let p = index_to_position(&source, i); + + assert_eq!(p, start) + } + + #[test] + fn reversible_position_conv() { + let source: Vec<_> = "There was a man,\n his voice had timbre,\n unlike a boy." + .chars() + .collect(); + + let a = Position { + line: 1, + character: 2, + }; + + let b = position_to_index(&source, a); + + assert_eq!(b, 19); + + let c = index_to_position(&source, b); + + let d = position_to_index(&source, a); + + assert_eq!(a, c); + assert_eq!(b, d); + } +}