Skip to content

Commit

Permalink
Before switch to tower-lsp
Browse files Browse the repository at this point in the history
  • Loading branch information
elijah-potter committed Jan 18, 2024
1 parent 042b26b commit 1995eed
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 73 deletions.
2 changes: 1 addition & 1 deletion harper-core/benches/spellcheck.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use criterion::{criterion_group, criterion_main, Criterion};
use lt_core::{suggest_correct_spelling_str, Dictionary};
use harper_core::{suggest_correct_spelling_str, Dictionary};

fn spellcheck(dictionary: &Dictionary) {
suggest_correct_spelling_str("hello", 5, 3, dictionary);
Expand Down
12 changes: 12 additions & 0 deletions harper-core/src/linting/lint.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::fmt::Display;

use serde::{Deserialize, Serialize};

use crate::{document::Document, span::Span, Dictionary};
Expand All @@ -23,4 +25,14 @@ pub enum Suggestion {
ReplaceWith(Vec<char>),
}

impl Display for Suggestion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Suggestion::ReplaceWith(with) => {
write!(f, "Replace with: {}", with.iter().collect::<String>())
}
}
}
}

pub type Linter = fn(document: &Document, dictionary: &Dictionary) -> Vec<Lint>;
20 changes: 20 additions & 0 deletions harper-core/src/span.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ impl Span {
self.end - self.start
}

pub fn is_empty(&self) -> bool {
self.len() == 0
}

pub fn overlaps_with(&self, other: Self) -> bool {
self.start.max(other.start) <= self.end.min(other.end)
}

pub fn get_content<'a>(&self, source: &'a [char]) -> &'a [char] {
if cfg!(debug_assertions) {
assert!(self.start < self.end);
Expand All @@ -39,3 +47,15 @@ impl Span {
cloned
}
}

#[cfg(test)]
mod tests {
use crate::Span;

#[test]
fn overlaps() {
assert!(Span::new(0, 5).overlaps_with(&Span::new(3, 6)));
assert!(Span::new(0, 5).overlaps_with(&Span::new(2, 3)));
assert!(Span::new(0, 5).overlaps_with(&Span::new(4, 5)));
}
}
118 changes: 118 additions & 0 deletions harper-ls/src/diagnostics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
use harper_core::{all_linters, Dictionary, Document, Lint, Span, Suggestion};
use lsp_types::{CodeAction, CodeActionKind, Diagnostic, Location, Position, Range, Url};
use std::{fs::read, io::Read};

pub fn generate_diagnostics(file_url: &Url) -> anyhow::Result<Vec<Diagnostic>> {
let file_str = open_url(file_url)?;
let source_chars: Vec<_> = file_str.chars().collect();
let lints = lint_string(&file_str);

let diagnostics = lints
.into_iter()
.map(|lint| lint_to_diagnostic(lint, &source_chars))
.collect();

Ok(diagnostics)
}

pub fn generate_code_actions(url: &Url, range: Range) -> anyhow::Result<Vec<CodeAction>> {
let file_str = open_url(url)?;
let source_chars: Vec<_> = file_str.chars().collect();
let lints = lint_string(&file_str);

// 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).collect::<Vec<_>>())
.collect();

Ok(actions)
}

fn lint_to_code_actions(lint: &Lint) -> impl Iterator<Item = CodeAction> + '_ {
lint.suggestions.iter().map(|suggestion| CodeAction {
title: suggestion.to_string(),
kind: Some(CodeActionKind::QUICKFIX),
diagnostics: None,
edit: None,
command: None,
is_preferred: None,
disabled: None,
data: None,
})
}

fn open_url(url: &Url) -> anyhow::Result<String> {
let file = read(url.path())?;
Ok(String::from_utf8(file)?)
}

fn lint_string(text: &str) -> Vec<Lint> {
let document = Document::new(text);
let dictionary = Dictionary::new();
all_linters(&document, dictionary)
}

fn lint_to_diagnostic(lint: Lint, source: &[char]) -> Diagnostic {
let range = span_to_range(source, lint.span);

Diagnostic {
range,
severity: None,
code: None,
code_description: None,
source: Some("Harper".to_string()),
message: lint.message,
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)
.next()
.unwrap_or(0);
line_start_idx + position.character as usize
}

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)
}
61 changes: 0 additions & 61 deletions harper-ls/src/generate_diagnostics.rs

This file was deleted.

2 changes: 1 addition & 1 deletion harper-ls/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
mod generate_diagnostics;
mod diagnostics;
mod server;
use clap::Parser;
use lsp_server::Connection;
Expand Down
55 changes: 46 additions & 9 deletions harper-ls/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@ use lsp_server::{
};
use lsp_types::{
notification::{
DidChangeTextDocument, DidOpenTextDocument, DidSaveTextDocument,
Notification as NotificationTrait, PublishDiagnostics,
DidOpenTextDocument, DidSaveTextDocument, Notification as NotificationTrait,
PublishDiagnostics,
},
request::GotoDefinition,
CodeActionProviderCapability, Diagnostic, DiagnosticOptions, GotoDefinitionResponse,
InitializedParams, Location, Position, PublishDiagnosticsParams, Range, ServerCapabilities,
Url,
request::{CodeActionRequest, GotoDefinition},
CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionProviderCapability,
CodeActionResponse, Diagnostic, DiagnosticOptions, GotoDefinitionResponse, InitializedParams,
Location, OneOf, Position, PublishDiagnosticsParams, Range, ServerCapabilities, Url,
WorkDoneProgressOptions,
};
use serde::Serialize;
use tracing::{error, info};

use crate::generate_diagnostics::generate_diagnostics;
use crate::diagnostics::{generate_code_actions, generate_diagnostics};

pub struct Server {
connection: Connection,
Expand All @@ -30,7 +32,14 @@ impl Server {
diagnostic_provider: Some(lsp_types::DiagnosticServerCapabilities::Options(
DiagnosticOptions::default(),
)),
code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
definition_provider: Some(OneOf::Left(true)),
code_action_provider: Some(CodeActionProviderCapability::Options(
lsp_types::CodeActionOptions {
code_action_kinds: Some(vec![CodeActionKind::QUICKFIX]),
work_done_progress_options: WorkDoneProgressOptions::default(),
resolve_provider: None,
},
)),
..Default::default()
})
.unwrap();
Expand All @@ -53,9 +62,11 @@ impl Server {
if self.connection.handle_shutdown(&req)? {
return Ok(());
}

info!("Got request: {req:?}");

let handlers: [RequestHandler; 1] = [Self::handle_goto];
let handlers: [RequestHandler; 2] =
[Self::handle_goto, Self::handle_code_action];

for handler in handlers {
let res = handler(self, &req);
Expand Down Expand Up @@ -144,12 +155,38 @@ impl Server {
},
},
}]));

self.send_response(result, id)?;

Ok(())
}

fn handle_code_action(&self, req: &Request) -> anyhow::Result<()> {
let (id, params) = cast_request::<CodeActionRequest>(req.clone())?;

info!("Got code action request request #{id}: {params:?}");

let actions = generate_code_actions(&params.text_document.uri, params.range)?;
let response: CodeActionResponse = actions
.into_iter()
.map(CodeActionOrCommand::CodeAction)
.collect();

let result = Some(response);

self.send_response(result, id)?;

Ok(())
}

fn send_response<V: Serialize>(&self, result: V, id: RequestId) -> anyhow::Result<()> {
let result = serde_json::to_value(result).unwrap();
let resp = Response {
id,
result: Some(result),
error: None,
};

self.connection.sender.send(Message::Response(resp))?;

Ok(())
Expand Down
4 changes: 3 additions & 1 deletion nvim.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

vim.lsp.start({
name = "example",
cmd = { "harper-ls" },
cmd = vim.lsp.rpc.connect("127.0.0.1", 4000),
root_dir = "."
})


0 comments on commit 1995eed

Please sign in to comment.