1use crate::report::{Fix, Severity, Violation};
10
11pub fn lsp_severity(sev: Severity) -> u8 {
12 match sev {
13 Severity::Error => 1,
14 Severity::Warning => 2,
15 Severity::Info => 3,
16 }
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct Position {
21 pub line: u32,
22 pub character: u32,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct Range {
27 pub start: Position,
28 pub end: Position,
29}
30
31#[derive(Debug, Clone)]
32pub struct Diagnostic {
33 pub range: Range,
34 pub severity: u8,
35 pub code: String,
36 pub source: String,
37 pub message: String,
38 pub code_description_href: Option<String>,
39}
40
41#[derive(Debug, Clone)]
42pub struct TextEdit {
43 pub range: Range,
44 pub new_text: String,
45}
46
47#[derive(Debug, Clone)]
48pub struct CodeAction {
49 pub title: String,
50 pub kind: String,
51 pub uri: String,
52 pub edit: TextEdit,
53}
54
55fn token_end(chars: &[char], col: usize) -> usize {
61 if col >= chars.len() || chars[col].is_whitespace() {
62 return chars.len();
63 }
64 let mut i = col;
65 while i < chars.len() && !chars[i].is_whitespace() {
66 i += 1;
67 }
68 i
69}
70
71pub fn violation_to_diagnostic(
80 v: &Violation,
81 guide_url: Option<&str>,
82 source: Option<&str>,
83) -> Diagnostic {
84 let line = v.line.saturating_sub(1);
85 let mut col = v.column.unwrap_or(1).saturating_sub(1) as usize;
86 let mut end_col = col;
87
88 if let Some(source) = source {
89 let lines: Vec<&str> = source.lines().collect();
90 if (line as usize) < lines.len() {
91 let text: Vec<char> = lines[line as usize].trim_end().chars().collect();
92 if v.column.is_none() {
93 col = 0;
94 end_col = text.len();
95 } else {
96 col = col.min(text.len());
97 end_col = token_end(&text, col);
98 }
99 }
100 }
101
102 Diagnostic {
103 range: Range {
104 start: Position {
105 line,
106 character: col as u32,
107 },
108 end: Position {
109 line,
110 character: end_col as u32,
111 },
112 },
113 severity: lsp_severity(v.severity),
114 code: v.rule_id.clone(),
115 source: "jss-lint".to_string(),
116 message: v.message.clone(),
117 code_description_href: guide_url.map(|s| s.to_string()),
118 }
119}
120
121fn offset_to_lsp_position(text: &str, offset: usize) -> Position {
128 let chunk: Vec<char> = text.chars().take(offset).collect();
129 let line = chunk.iter().filter(|&&c| c == '\n').count() as u32;
130 let character = match chunk.iter().rposition(|&c| c == '\n') {
131 Some(idx) => (chunk.len() - idx - 1) as u32,
132 None => chunk.len() as u32,
133 };
134 Position { line, character }
135}
136
137pub fn fix_to_text_edit(fix: &Fix, source: &str) -> TextEdit {
139 TextEdit {
140 range: Range {
141 start: offset_to_lsp_position(source, fix.start),
142 end: offset_to_lsp_position(source, fix.end),
143 },
144 new_text: fix.replacement.clone(),
145 }
146}
147
148pub fn violation_to_code_action(v: &Violation, source: &str, uri: &str) -> Option<CodeAction> {
152 let fix = v.fix.as_ref()?;
153 Some(CodeAction {
154 title: fix.description.clone(),
155 kind: "quickfix".to_string(),
156 uri: uri.to_string(),
157 edit: fix_to_text_edit(fix, source),
158 })
159}