Skip to content

Commit

Permalink
harper-ls now runs diagnostics on change
Browse files Browse the repository at this point in the history
  • Loading branch information
elijah-potter committed Jan 23, 2024
1 parent 6dfaa62 commit 6039755
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 154 deletions.
4 changes: 4 additions & 0 deletions harper-core/src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
2 changes: 1 addition & 1 deletion harper-core/src/linting/spell_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 1 addition & 4 deletions harper-core/src/parsing/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,8 @@ pub fn lex_to_end_md(source: &[char]) -> Vec<Token> {
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));
Expand Down Expand Up @@ -196,6 +192,7 @@ fn lex_punctuation(source: &[char]) -> Option<FoundToken> {
use Punctuation::*;

let punct = match c {
'%' => Percent,
'’' => Apostrophe,
'\'' => Apostrophe,
'.' => Period,
Expand Down
2 changes: 2 additions & 0 deletions harper-core/src/parsing/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ pub enum Punctuation {
Hash,
/// '
Apostrophe,
/// %
Percent,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
Expand Down
109 changes: 82 additions & 27 deletions harper-ls/src/backend.rs
Original file line number Diff line number Diff line change
@@ -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<LintSet>,
files: Mutex<HashMap<Url, Document>>,
}

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<Vec<CodeAction>> {
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::<Vec<_>>())
.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<Diagnostic> {
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(),
Expand All @@ -44,20 +96,6 @@ impl Backend {
self.client
.send_notification::<PublishDiagnostics>(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;
}
}

Expand All @@ -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()
},
})
Expand All @@ -88,6 +135,8 @@ impl LanguageServer for Backend {
.log_message(MessageType::INFO, "File saved!")
.await;

self.update_document_from_file(&params.text_document.uri)
.await;
self.publish_diagnostics(&params.text_document.uri).await;
}

Expand All @@ -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(&params.text_document.uri, &last.text)
.await;
self.publish_diagnostics(&params.text_document.uri).await;
}

Expand All @@ -114,9 +169,9 @@ impl LanguageServer for Backend {
}

async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
let mut linter = self.linter.lock().await;
let actions =
generate_code_actions(&params.text_document.uri, params.range, linter.deref_mut())?;
let actions = self
.generate_code_actions(&params.text_document.uri, params.range)
.await?;

self.client
.log_message(MessageType::INFO, format!("{:?}", actions))
Expand Down
132 changes: 10 additions & 122 deletions harper-ls/src/diagnostics.rs
Original file line number Diff line number Diff line change
@@ -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<Lint>, Vec<char>)> {
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<Vec<Diagnostic>> {
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<Vec<CodeAction>> {
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::<Vec<_>>())
.collect();

Ok(actions)
pub fn lints_to_diagnostics(source: &[char], lints: &[Lint]) -> Vec<Diagnostic> {
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],
Expand Down Expand Up @@ -76,13 +46,7 @@ fn lint_to_code_actions<'a>(
})
}

fn open_url(url: &Url) -> Result<String> {
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 {
Expand All @@ -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);
}
}
1 change: 1 addition & 0 deletions harper-ls/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use tokio::net::TcpListener;
mod backend;
mod diagnostics;
mod pos_conv;

use backend::Backend;
use clap::Parser;
Expand Down
Loading

0 comments on commit 6039755

Please sign in to comment.