Skip to content

Commit 1995eed

Browse files
committed
Before switch to tower-lsp
1 parent 042b26b commit 1995eed

File tree

8 files changed

+201
-73
lines changed

8 files changed

+201
-73
lines changed

harper-core/benches/spellcheck.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use criterion::{criterion_group, criterion_main, Criterion};
2-
use lt_core::{suggest_correct_spelling_str, Dictionary};
2+
use harper_core::{suggest_correct_spelling_str, Dictionary};
33

44
fn spellcheck(dictionary: &Dictionary) {
55
suggest_correct_spelling_str("hello", 5, 3, dictionary);

harper-core/src/linting/lint.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::fmt::Display;
2+
13
use serde::{Deserialize, Serialize};
24

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

28+
impl Display for Suggestion {
29+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30+
match self {
31+
Suggestion::ReplaceWith(with) => {
32+
write!(f, "Replace with: {}", with.iter().collect::<String>())
33+
}
34+
}
35+
}
36+
}
37+
2638
pub type Linter = fn(document: &Document, dictionary: &Dictionary) -> Vec<Lint>;

harper-core/src/span.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ impl Span {
1616
self.end - self.start
1717
}
1818

19+
pub fn is_empty(&self) -> bool {
20+
self.len() == 0
21+
}
22+
23+
pub fn overlaps_with(&self, other: Self) -> bool {
24+
self.start.max(other.start) <= self.end.min(other.end)
25+
}
26+
1927
pub fn get_content<'a>(&self, source: &'a [char]) -> &'a [char] {
2028
if cfg!(debug_assertions) {
2129
assert!(self.start < self.end);
@@ -39,3 +47,15 @@ impl Span {
3947
cloned
4048
}
4149
}
50+
51+
#[cfg(test)]
52+
mod tests {
53+
use crate::Span;
54+
55+
#[test]
56+
fn overlaps() {
57+
assert!(Span::new(0, 5).overlaps_with(&Span::new(3, 6)));
58+
assert!(Span::new(0, 5).overlaps_with(&Span::new(2, 3)));
59+
assert!(Span::new(0, 5).overlaps_with(&Span::new(4, 5)));
60+
}
61+
}

harper-ls/src/diagnostics.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
use harper_core::{all_linters, Dictionary, Document, Lint, Span, Suggestion};
2+
use lsp_types::{CodeAction, CodeActionKind, Diagnostic, Location, Position, Range, Url};
3+
use std::{fs::read, io::Read};
4+
5+
pub fn generate_diagnostics(file_url: &Url) -> anyhow::Result<Vec<Diagnostic>> {
6+
let file_str = open_url(file_url)?;
7+
let source_chars: Vec<_> = file_str.chars().collect();
8+
let lints = lint_string(&file_str);
9+
10+
let diagnostics = lints
11+
.into_iter()
12+
.map(|lint| lint_to_diagnostic(lint, &source_chars))
13+
.collect();
14+
15+
Ok(diagnostics)
16+
}
17+
18+
pub fn generate_code_actions(url: &Url, range: Range) -> anyhow::Result<Vec<CodeAction>> {
19+
let file_str = open_url(url)?;
20+
let source_chars: Vec<_> = file_str.chars().collect();
21+
let lints = lint_string(&file_str);
22+
23+
// Find lints whose span overlaps with range
24+
let span = range_to_span(&source_chars, range);
25+
26+
let actions = lints
27+
.into_iter()
28+
.filter(|lint| lint.span.overlaps_with(span))
29+
.flat_map(|lint| lint_to_code_actions(&lint).collect::<Vec<_>>())
30+
.collect();
31+
32+
Ok(actions)
33+
}
34+
35+
fn lint_to_code_actions(lint: &Lint) -> impl Iterator<Item = CodeAction> + '_ {
36+
lint.suggestions.iter().map(|suggestion| CodeAction {
37+
title: suggestion.to_string(),
38+
kind: Some(CodeActionKind::QUICKFIX),
39+
diagnostics: None,
40+
edit: None,
41+
command: None,
42+
is_preferred: None,
43+
disabled: None,
44+
data: None,
45+
})
46+
}
47+
48+
fn open_url(url: &Url) -> anyhow::Result<String> {
49+
let file = read(url.path())?;
50+
Ok(String::from_utf8(file)?)
51+
}
52+
53+
fn lint_string(text: &str) -> Vec<Lint> {
54+
let document = Document::new(text);
55+
let dictionary = Dictionary::new();
56+
all_linters(&document, dictionary)
57+
}
58+
59+
fn lint_to_diagnostic(lint: Lint, source: &[char]) -> Diagnostic {
60+
let range = span_to_range(source, lint.span);
61+
62+
Diagnostic {
63+
range,
64+
severity: None,
65+
code: None,
66+
code_description: None,
67+
source: Some("Harper".to_string()),
68+
message: lint.message,
69+
related_information: None,
70+
tags: None,
71+
data: None,
72+
}
73+
}
74+
75+
fn span_to_range(source: &[char], span: Span) -> Range {
76+
let start = index_to_position(source, span.start);
77+
let end = index_to_position(source, span.end);
78+
79+
Range { start, end }
80+
}
81+
82+
fn index_to_position(source: &[char], index: usize) -> Position {
83+
let before = &source[0..index];
84+
let newline_indices: Vec<_> = before
85+
.iter()
86+
.enumerate()
87+
.filter_map(|(idx, c)| if *c == '\n' { Some(idx) } else { None })
88+
.collect();
89+
90+
let lines = newline_indices.len();
91+
let cols = index - newline_indices.last().copied().unwrap_or(1) - 1;
92+
93+
Position {
94+
line: lines as u32,
95+
character: cols as u32,
96+
}
97+
}
98+
99+
fn position_to_index(source: &[char], position: Position) -> usize {
100+
let newline_indices =
101+
source
102+
.iter()
103+
.enumerate()
104+
.filter_map(|(idx, c)| if *c == '\n' { Some(idx) } else { None });
105+
106+
let line_start_idx = newline_indices
107+
.take(position.line as usize)
108+
.next()
109+
.unwrap_or(0);
110+
line_start_idx + position.character as usize
111+
}
112+
113+
fn range_to_span(source: &[char], range: Range) -> Span {
114+
let start = position_to_index(source, range.start);
115+
let end = position_to_index(source, range.end);
116+
117+
Span::new(start, end)
118+
}

harper-ls/src/generate_diagnostics.rs

Lines changed: 0 additions & 61 deletions
This file was deleted.

harper-ls/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
mod generate_diagnostics;
1+
mod diagnostics;
22
mod server;
33
use clap::Parser;
44
use lsp_server::Connection;

harper-ls/src/server.rs

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,19 @@ use lsp_server::{
33
};
44
use lsp_types::{
55
notification::{
6-
DidChangeTextDocument, DidOpenTextDocument, DidSaveTextDocument,
7-
Notification as NotificationTrait, PublishDiagnostics,
6+
DidOpenTextDocument, DidSaveTextDocument, Notification as NotificationTrait,
7+
PublishDiagnostics,
88
},
9-
request::GotoDefinition,
10-
CodeActionProviderCapability, Diagnostic, DiagnosticOptions, GotoDefinitionResponse,
11-
InitializedParams, Location, Position, PublishDiagnosticsParams, Range, ServerCapabilities,
12-
Url,
9+
request::{CodeActionRequest, GotoDefinition},
10+
CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionProviderCapability,
11+
CodeActionResponse, Diagnostic, DiagnosticOptions, GotoDefinitionResponse, InitializedParams,
12+
Location, OneOf, Position, PublishDiagnosticsParams, Range, ServerCapabilities, Url,
13+
WorkDoneProgressOptions,
1314
};
15+
use serde::Serialize;
1416
use tracing::{error, info};
1517

16-
use crate::generate_diagnostics::generate_diagnostics;
18+
use crate::diagnostics::{generate_code_actions, generate_diagnostics};
1719

1820
pub struct Server {
1921
connection: Connection,
@@ -30,7 +32,14 @@ impl Server {
3032
diagnostic_provider: Some(lsp_types::DiagnosticServerCapabilities::Options(
3133
DiagnosticOptions::default(),
3234
)),
33-
code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
35+
definition_provider: Some(OneOf::Left(true)),
36+
code_action_provider: Some(CodeActionProviderCapability::Options(
37+
lsp_types::CodeActionOptions {
38+
code_action_kinds: Some(vec![CodeActionKind::QUICKFIX]),
39+
work_done_progress_options: WorkDoneProgressOptions::default(),
40+
resolve_provider: None,
41+
},
42+
)),
3443
..Default::default()
3544
})
3645
.unwrap();
@@ -53,9 +62,11 @@ impl Server {
5362
if self.connection.handle_shutdown(&req)? {
5463
return Ok(());
5564
}
65+
5666
info!("Got request: {req:?}");
5767

58-
let handlers: [RequestHandler; 1] = [Self::handle_goto];
68+
let handlers: [RequestHandler; 2] =
69+
[Self::handle_goto, Self::handle_code_action];
5970

6071
for handler in handlers {
6172
let res = handler(self, &req);
@@ -144,12 +155,38 @@ impl Server {
144155
},
145156
},
146157
}]));
158+
159+
self.send_response(result, id)?;
160+
161+
Ok(())
162+
}
163+
164+
fn handle_code_action(&self, req: &Request) -> anyhow::Result<()> {
165+
let (id, params) = cast_request::<CodeActionRequest>(req.clone())?;
166+
167+
info!("Got code action request request #{id}: {params:?}");
168+
169+
let actions = generate_code_actions(&params.text_document.uri, params.range)?;
170+
let response: CodeActionResponse = actions
171+
.into_iter()
172+
.map(CodeActionOrCommand::CodeAction)
173+
.collect();
174+
175+
let result = Some(response);
176+
177+
self.send_response(result, id)?;
178+
179+
Ok(())
180+
}
181+
182+
fn send_response<V: Serialize>(&self, result: V, id: RequestId) -> anyhow::Result<()> {
147183
let result = serde_json::to_value(result).unwrap();
148184
let resp = Response {
149185
id,
150186
result: Some(result),
151187
error: None,
152188
};
189+
153190
self.connection.sender.send(Message::Response(resp))?;
154191

155192
Ok(())

nvim.lua

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
vim.lsp.start({
44
name = "example",
5-
cmd = { "harper-ls" },
5+
cmd = vim.lsp.rpc.connect("127.0.0.1", 4000),
66
root_dir = "."
77
})
8+
9+

0 commit comments

Comments
 (0)