use std::path::{Path, PathBuf};
use serde_json::Value;
use super::super::{Diagnostic, OriginalPosition, VirtualFile, VirtualProject};
use crate::corsa_client::LspDiagnostic;
use crate::file_uri::file_uri_to_path;
use vize_carton::{FxHashMap, FxHashSet, String, cstr};
pub(super) fn map_batch_diagnostics(
results: Vec<(String, Vec<LspDiagnostic>)>,
project: &VirtualProject,
) -> Vec<Diagnostic> {
let diagnostic_count = results
.iter()
.fold(0usize, |acc, (_, diagnostics)| acc + diagnostics.len());
let mut diagnostics = Vec::with_capacity(diagnostic_count);
let mut mapper = DiagnosticMapper::new(project);
for (uri, lsp_diagnostics) in results {
let virtual_path = uri_to_path(uri.as_str());
for diagnostic in lsp_diagnostics {
if let Some(diagnostic) = mapper.map_lsp_diagnostic(&virtual_path, diagnostic) {
diagnostics.push(diagnostic);
}
}
}
dedup_diagnostics(diagnostics)
}
type DiagnosticKey = (std::path::PathBuf, u32, u32, Option<u32>, String, u8);
fn diagnostic_key(diagnostic: &Diagnostic) -> DiagnosticKey {
(
diagnostic.file.clone(),
diagnostic.line,
diagnostic.column,
diagnostic.code,
diagnostic.message.clone(),
diagnostic.severity,
)
}
pub(super) fn dedup_diagnostics(diagnostics: Vec<Diagnostic>) -> Vec<Diagnostic> {
let mut seen: FxHashSet<DiagnosticKey> = FxHashSet::default();
let mut deduped = Vec::with_capacity(diagnostics.len());
for diagnostic in diagnostics {
if seen.insert(diagnostic_key(&diagnostic)) {
deduped.push(diagnostic);
}
}
deduped
}
pub(super) struct DiagnosticMapper<'a> {
project: &'a VirtualProject,
preserve_unused_diagnostics: bool,
original_sources: FxHashMap<PathBuf, CachedSource>,
virtual_line_indexes: FxHashMap<PathBuf, LineIndex>,
}
impl<'a> DiagnosticMapper<'a> {
pub(super) fn new(project: &'a VirtualProject) -> Self {
Self {
project,
preserve_unused_diagnostics: project.tsconfig_preserves_unused_diagnostics(),
original_sources: FxHashMap::default(),
virtual_line_indexes: FxHashMap::default(),
}
}
pub(super) fn preserves_unused_diagnostics(&self) -> bool {
self.preserve_unused_diagnostics
}
fn map_lsp_diagnostic(
&mut self,
virtual_path: &Path,
diagnostic: LspDiagnostic,
) -> Option<Diagnostic> {
let code = parse_diagnostic_code(diagnostic.code.as_ref());
if should_skip_diagnostic(code, &diagnostic.message) {
return None;
}
if code == Some(6133) && !self.preserve_unused_diagnostics {
return None;
}
let original = self.map_to_original(
virtual_path,
diagnostic.range.start.line,
diagnostic.range.start.character,
);
if original
.as_ref()
.is_some_and(|original| should_skip_original_diagnostic(code, original))
{
return None;
}
if code == Some(2307)
&& let Some(original) = original.as_ref()
&& relative_module_resolves_on_disk(&diagnostic.message, &original.path)
{
return None;
}
if let Some(original) = original {
return Some(Diagnostic {
file: original.path,
line: original.line,
column: original.column,
message: diagnostic.message,
code,
severity: parse_severity(diagnostic.severity),
block_type: original.block_type,
});
}
None
}
pub(super) fn map_to_original(
&mut self,
virtual_path: &Path,
line: u32,
column: u32,
) -> Option<OriginalPosition> {
let file = self.project.find_by_virtual(virtual_path)?;
let virtual_offset = self.virtual_offset(file, line, column)?;
let (original_offset, _, block_type) =
file.source_map.get_original_position(virtual_offset)?;
let cached = self.original_source(&file.original_path)?;
let (original_line, original_column) = cached
.line_index
.offset_to_line_col(&cached.content, original_offset)?;
Some(OriginalPosition {
path: file.original_path.clone(),
line: original_line,
column: original_column,
block_type,
})
}
fn virtual_offset(&mut self, file: &VirtualFile, line: u32, column: u32) -> Option<u32> {
if !self.virtual_line_indexes.contains_key(&file.virtual_path) {
self.virtual_line_indexes
.insert(file.virtual_path.clone(), LineIndex::new(&file.content));
}
let line_index = self.virtual_line_indexes.get(&file.virtual_path)?;
line_index.line_col_to_offset(&file.content, line, column)
}
fn original_source(&mut self, path: &Path) -> Option<&CachedSource> {
if !self.original_sources.contains_key(path) {
let content: String = std::fs::read_to_string(path).ok()?.into();
let line_index = LineIndex::new(&content);
self.original_sources.insert(
path.to_path_buf(),
CachedSource {
content,
line_index,
},
);
}
self.original_sources.get(path)
}
}
struct CachedSource {
content: String,
line_index: LineIndex,
}
struct LineIndex {
starts: Vec<usize>,
len: usize,
}
impl LineIndex {
fn new(content: &str) -> Self {
let mut starts = vec![0];
for (index, byte) in content.bytes().enumerate() {
if byte == b'\n' {
starts.push(index + 1);
}
}
Self {
starts,
len: content.len(),
}
}
fn line_col_to_offset(&self, content: &str, line: u32, col: u32) -> Option<u32> {
let line = usize::try_from(line).ok()?;
let start = *self.starts.get(line)?;
let end = self.line_end(line);
let mut current_col = 0u32;
let mut offset = start;
if col == 0 {
return u32::try_from(offset).ok();
}
for ch in content[start..end].chars() {
offset += ch.len_utf8();
current_col += ch.len_utf16() as u32;
if current_col >= col {
return u32::try_from(offset).ok();
}
}
if current_col == col {
u32::try_from(offset).ok()
} else {
None
}
}
fn offset_to_line_col(&self, content: &str, offset: u32) -> Option<(u32, u32)> {
let offset = usize::try_from(offset).ok()?;
if offset > self.len {
return None;
}
let line = self.starts.partition_point(|start| *start <= offset);
let line = line.saturating_sub(1);
let start = *self.starts.get(line)?;
let end = self.line_end(line);
let mut col = 0u32;
let mut cursor = start;
for ch in content[start..end].chars() {
if cursor >= offset {
break;
}
col += ch.len_utf16() as u32;
cursor += ch.len_utf8();
}
Some((u32::try_from(line).ok()?, col))
}
fn line_end(&self, line: usize) -> usize {
self.starts
.get(line + 1)
.map(|next_start| next_start.saturating_sub(1))
.unwrap_or(self.len)
}
}
fn uri_to_path(uri: &str) -> PathBuf {
file_uri_to_path(uri).unwrap_or_else(|| PathBuf::from(uri))
}
fn parse_diagnostic_code(code: Option<&Value>) -> Option<u32> {
match code {
Some(Value::Number(value)) => value.as_u64().and_then(|value| u32::try_from(value).ok()),
Some(Value::String(value)) => value
.strip_prefix("TS")
.unwrap_or(value.as_str())
.parse()
.ok(),
_ => None,
}
}
fn parse_severity(severity: Option<i32>) -> u8 {
match severity {
Some(value) if (1..=4).contains(&value) => value as u8,
_ => 1,
}
}
pub(super) fn should_skip_diagnostic(code: Option<u32>, _message: &str) -> bool {
match code {
Some(2666) => true,
_ => false,
}
}
pub(super) fn should_skip_original_diagnostic(
code: Option<u32>,
original: &OriginalPosition,
) -> bool {
code == Some(6133) && original.block_type.is_none() && is_vue_source(&original.path)
}
fn is_vue_source(path: &Path) -> bool {
path.extension().is_some_and(|extension| extension == "vue")
}
fn module_not_found_specifier(message: &str) -> Option<&str> {
let rest = message.split_once("Cannot find module ")?.1;
let open = rest.chars().next()?;
let close = match open {
'\'' => '\'',
'"' => '"',
'\u{2018}' => '\u{2019}',
_ => return None,
};
let after_open = &rest[open.len_utf8()..];
let end = after_open.find(close)?;
Some(&after_open[..end])
}
pub(super) fn relative_module_resolves_on_disk(message: &str, importer: &Path) -> bool {
let Some(specifier) = module_not_found_specifier(message) else {
return false;
};
if !(specifier.starts_with("./") || specifier.starts_with("../")) {
return false;
}
let Some(dir) = importer.parent() else {
return false;
};
relative_specifier_resolves(dir, specifier)
}
const SOURCE_EXTENSIONS: &[&str] = &["ts", "tsx", "d.ts", "mts", "cts", "vue"];
fn relative_specifier_resolves(dir: &Path, specifier: &str) -> bool {
if specifier_has_source_extension(specifier) && dir.join(specifier).is_file() {
return true;
}
if let Some(stem) = specifier.strip_suffix(".vue.ts")
&& dir.join(cstr!("{stem}.vue").as_str()).is_file()
{
return true;
}
for (js, ts) in [(".js", ".ts"), (".mjs", ".mts"), (".cjs", ".cts")] {
if let Some(stem) = specifier.strip_suffix(js)
&& dir.join(cstr!("{stem}{ts}").as_str()).is_file()
{
return true;
}
}
for extension in SOURCE_EXTENSIONS {
if dir
.join(cstr!("{specifier}.{extension}").as_str())
.is_file()
{
return true;
}
}
let base = dir.join(specifier);
for extension in SOURCE_EXTENSIONS {
if base.join(cstr!("index.{extension}").as_str()).is_file() {
return true;
}
}
false
}
fn specifier_has_source_extension(specifier: &str) -> bool {
specifier.ends_with(".vue")
|| specifier.ends_with(".d.ts")
|| specifier.ends_with(".ts")
|| specifier.ends_with(".tsx")
|| specifier.ends_with(".mts")
|| specifier.ends_with(".cts")
}
#[cfg(test)]
mod tests {
use super::{
LineIndex, map_batch_diagnostics, parse_diagnostic_code, parse_severity,
should_skip_diagnostic, should_skip_original_diagnostic, uri_to_path,
};
use crate::batch::{OriginalPosition, SfcBlockType, VirtualProject};
use serde_json::json;
use std::path::PathBuf;
use tempfile::TempDir;
use vize_carton::cstr;
#[test]
fn duplicated_template_diagnostic_is_reported_once() {
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path().canonicalize().unwrap();
let src_dir = project_root.join("src");
std::fs::create_dir_all(&src_dir).unwrap();
let app_path = src_dir.join("App.vue");
std::fs::write(
&app_path,
"<script setup lang=\"ts\">\n</script>\n<template><div>{{ missingThing }}</div></template>\n",
)
.unwrap();
let mut project = VirtualProject::new(&project_root).unwrap();
project.register_path(&app_path).unwrap();
let virtual_file = project.find_by_original(&app_path).unwrap();
let virtual_source = virtual_file.content.as_str();
let message = "Cannot find name 'missingThing'.";
let diagnostics: Vec<_> = virtual_source
.match_indices("void (missingThing)")
.map(|(at, _)| at + "void (".len())
.map(|offset| {
let (line, character) = LineIndex::new(virtual_source)
.offset_to_line_col(virtual_source, offset as u32)
.expect("virtual offset should map to LSP position");
crate::corsa_client::LspDiagnostic {
range: crate::corsa_client::LspRange {
start: crate::corsa_client::LspPosition { line, character },
end: crate::corsa_client::LspPosition {
line,
character: character + "missingThing".len() as u32,
},
},
severity: Some(1),
code: Some(json!("TS2304")),
source: Some("ts".into()),
message: message.into(),
}
})
.collect();
assert_eq!(
diagnostics.len(),
2,
"expected `missingThing` to be generated at two virtual positions"
);
let mapped = map_batch_diagnostics(
vec![(file_uri_for(&virtual_file.virtual_path), diagnostics)],
&project,
);
assert_eq!(
mapped.len(),
1,
"the duplicated template diagnostic must be deduplicated: {mapped:#?}"
);
assert_eq!(mapped[0].file, app_path);
assert_eq!(mapped[0].code, Some(2304));
}
#[test]
fn dedup_preserves_distinct_diagnostics() {
use super::dedup_diagnostics;
use crate::batch::Diagnostic;
let base = Diagnostic {
file: PathBuf::from("/p/App.vue"),
line: 4,
column: 6,
message: "Cannot find name 'x'.".into(),
code: Some(2304),
severity: 1,
block_type: Some(SfcBlockType::Template),
};
let duplicate = base.clone();
let different_code = Diagnostic {
code: Some(2322),
..base.clone()
};
let different_message = Diagnostic {
message: "Cannot find name 'y'.".into(),
..base.clone()
};
let different_severity = Diagnostic {
severity: 4,
..base.clone()
};
let deduped = dedup_diagnostics(vec![
base.clone(),
duplicate,
different_code,
different_message,
different_severity,
]);
assert_eq!(deduped.len(), 4, "{deduped:#?}");
}
#[test]
fn parses_numeric_and_string_diagnostic_codes() {
assert_eq!(parse_diagnostic_code(Some(&json!(2322))), Some(2322));
assert_eq!(parse_diagnostic_code(Some(&json!("TS2304"))), Some(2304));
assert_eq!(parse_diagnostic_code(Some(&json!("2551"))), Some(2551));
assert_eq!(parse_diagnostic_code(Some(&json!(false))), None);
}
#[test]
fn normalizes_lsp_severity() {
assert_eq!(parse_severity(Some(1)), 1);
assert_eq!(parse_severity(Some(2)), 2);
assert_eq!(parse_severity(Some(9)), 1);
assert_eq!(parse_severity(None), 1);
}
#[test]
fn strips_file_uri_scheme() {
assert_eq!(
uri_to_path("file:///workspace/src/App.vue.ts"),
PathBuf::from("/workspace/src/App.vue.ts")
);
}
#[test]
fn decodes_file_uri_path_bytes() {
assert_eq!(
uri_to_path("file:///workspace/pages/%5Bname%5D%20%231.vue.ts"),
PathBuf::from("/workspace/pages/[name] #1.vue.ts")
);
}
#[test]
fn line_index_matches_source_map_boundaries() {
let content = "a\nbeta\n";
let index = LineIndex::new(content);
assert_eq!(index.line_col_to_offset(content, 0, 1), Some(1));
assert_eq!(index.line_col_to_offset(content, 1, 4), Some(6));
assert_eq!(index.line_col_to_offset(content, 2, 0), Some(7));
assert_eq!(index.line_col_to_offset(content, 1, 5), None);
assert_eq!(index.offset_to_line_col(content, 7), Some((2, 0)));
let content = "é\n";
let index = LineIndex::new(content);
assert_eq!(index.offset_to_line_col(content, 1), Some((0, 1)));
}
#[test]
fn line_index_counts_astral_chars_as_two_utf16_units() {
let content = "\u{1F600}x\n";
let index = LineIndex::new(content);
assert_eq!(index.offset_to_line_col(content, 5), Some((0, 3)));
assert_eq!(index.line_col_to_offset(content, 0, 3), Some(5));
}
#[test]
fn ts2307_module_not_found_diagnostics_are_not_skipped_globally() {
let vue_msg = "Cannot find module './app.vue' or its corresponding type declarations.";
let vue_ts_msg =
"Cannot find module './app.vue.ts' or its corresponding type declarations.";
let non_vue_msg = "Cannot find module 'lodash-es' or its corresponding type declarations.";
assert!(!should_skip_diagnostic(Some(2307), vue_msg));
assert!(!should_skip_diagnostic(Some(2307), vue_ts_msg));
assert!(!should_skip_diagnostic(Some(2307), non_vue_msg));
assert!(!should_skip_diagnostic(Some(6133), "any message"));
assert!(should_skip_diagnostic(Some(2666), "any message"));
assert!(!should_skip_diagnostic(Some(7006), "any message"));
assert!(!should_skip_diagnostic(Some(7043), "any message"));
assert!(!should_skip_diagnostic(Some(7044), "any message"));
assert!(!should_skip_diagnostic(Some(2322), "any message"));
assert!(!should_skip_diagnostic(None, "any message"));
}
#[test]
fn ts6133_suppression_is_limited_to_unmapped_vue_generated_code() {
let unmapped_vue = OriginalPosition {
path: PathBuf::from("App.vue"),
line: 0,
column: 0,
block_type: None,
};
let mapped_vue = OriginalPosition {
path: PathBuf::from("App.vue"),
line: 1,
column: 6,
block_type: Some(SfcBlockType::ScriptSetup),
};
let plain_ts = OriginalPosition {
path: PathBuf::from("main.ts"),
line: 0,
column: 6,
block_type: None,
};
assert!(should_skip_original_diagnostic(Some(6133), &unmapped_vue));
assert!(!should_skip_original_diagnostic(Some(6133), &mapped_vue));
assert!(!should_skip_original_diagnostic(Some(6133), &plain_ts));
assert!(!should_skip_original_diagnostic(Some(2322), &unmapped_vue));
}
#[test]
fn ts2307_for_resolvable_relative_siblings_is_suppressed() {
use super::relative_module_resolves_on_disk;
let dir = TempDir::new().unwrap();
let importer = dir.path().join("App.vue");
std::fs::write(&importer, "").unwrap();
std::fs::write(dir.path().join("types.ts"), "").unwrap();
std::fs::write(dir.path().join("Panel.vue"), "<template />").unwrap();
std::fs::create_dir_all(dir.path().join("util")).unwrap();
std::fs::write(dir.path().join("util").join("index.ts"), "").unwrap();
let resolvable_ts = "Cannot find module './types' or its corresponding type declarations.";
let resolvable_vue_ts =
"Cannot find module './Panel.vue.ts' or its corresponding type declarations.";
let resolvable_index =
"Cannot find module './util' or its corresponding type declarations.";
let resolvable_js_to_ts =
"Cannot find module './types.js' or its corresponding type declarations.";
let missing_relative =
"Cannot find module './nope' or its corresponding type declarations.";
let bare_package = "Cannot find module 'lodash-es' or its corresponding type declarations.";
assert!(relative_module_resolves_on_disk(resolvable_ts, &importer));
assert!(relative_module_resolves_on_disk(
resolvable_vue_ts,
&importer
));
assert!(relative_module_resolves_on_disk(
resolvable_index,
&importer
));
assert!(relative_module_resolves_on_disk(
resolvable_js_to_ts,
&importer
));
assert!(!relative_module_resolves_on_disk(
missing_relative,
&importer
));
assert!(!relative_module_resolves_on_disk(bare_package, &importer));
}
#[test]
fn maps_missing_vue_ts2307_back_to_source_file() {
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path().canonicalize().unwrap();
let src_dir = project_root.join("src");
std::fs::create_dir_all(&src_dir).unwrap();
let app_path = src_dir.join("App.vue");
std::fs::write(
&app_path,
r#"<script setup lang="ts">
import MissingPanel from './MissingPanel.vue'
</script>
<template>
<MissingPanel />
</template>
"#,
)
.unwrap();
let mut project = VirtualProject::new(&project_root).unwrap();
project.register_path(&app_path).unwrap();
let virtual_file = project.find_by_original(&app_path).unwrap();
let diagnostic = ts2307_diagnostic_at(
virtual_file.content.as_str(),
"MissingPanel.vue.ts",
"Cannot find module './MissingPanel.vue.ts' or its corresponding type declarations.",
);
let diagnostics = map_batch_diagnostics(
vec![(file_uri_for(&virtual_file.virtual_path), vec![diagnostic])],
&project,
);
assert_eq!(diagnostics.len(), 1);
let diagnostic = &diagnostics[0];
assert_eq!(diagnostic.file, app_path);
assert_eq!(diagnostic.code, Some(2307));
assert_eq!(diagnostic.line, 1);
assert!(
diagnostic.message.contains("MissingPanel.vue.ts"),
"{diagnostic:?}"
);
}
#[test]
fn suppresses_vue_ts2307_when_vue_sibling_exists_on_disk() {
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path().canonicalize().unwrap();
let src_dir = project_root.join("src");
std::fs::create_dir_all(&src_dir).unwrap();
let app_path = src_dir.join("App.vue");
std::fs::write(
&app_path,
r#"<script setup lang="ts">
import ExistingPanel from './ExistingPanel.vue'
</script>
<template>
<ExistingPanel />
</template>
"#,
)
.unwrap();
std::fs::write(
src_dir.join("ExistingPanel.vue"),
r#"<template><section /></template>
"#,
)
.unwrap();
let mut project = VirtualProject::new(&project_root).unwrap();
project.register_path(&app_path).unwrap();
let virtual_file = project.find_by_original(&app_path).unwrap();
let diagnostic = ts2307_diagnostic_at(
virtual_file.content.as_str(),
"ExistingPanel.vue.ts",
"Cannot find module './ExistingPanel.vue.ts' or its corresponding type declarations.",
);
let diagnostics = map_batch_diagnostics(
vec![(file_uri_for(&virtual_file.virtual_path), vec![diagnostic])],
&project,
);
assert!(
diagnostics.is_empty(),
"existing Vue sibling false positive should stay suppressed: {diagnostics:#?}"
);
}
#[test]
fn maps_unmapped_diagnostics_snapshot() {
let temp_dir = TempDir::new().unwrap();
let project = VirtualProject::new(temp_dir.path()).unwrap();
let diagnostics = map_batch_diagnostics(
vec![(
cstr!("file:///workspace/src/App.vue.ts"),
vec![crate::corsa_client::LspDiagnostic {
range: crate::corsa_client::LspRange {
start: crate::corsa_client::LspPosition {
line: 3,
character: 5,
},
end: crate::corsa_client::LspPosition {
line: 3,
character: 12,
},
},
severity: Some(1),
code: Some(json!("TS2322")),
source: Some("ts".into()),
message: "Type 'string' is not assignable to type 'number'.".into(),
}],
)],
&project,
);
insta::assert_debug_snapshot!("maps_unmapped_diagnostics_snapshot", diagnostics);
}
#[test]
fn maps_user_ts6133_but_suppresses_generated_vue_ts6133() {
let temp_dir = TempDir::new().unwrap();
let source = temp_dir.path().join("src").join("App.vue");
std::fs::create_dir_all(source.parent().unwrap()).unwrap();
std::fs::write(
temp_dir.path().join("tsconfig.json"),
r#"{
"compilerOptions": {
"noUnusedLocals": true
},
"include": ["src/**/*"]
}"#,
)
.unwrap();
std::fs::write(
&source,
r#"<script setup lang="ts">
const used = 1
const unusedLocal = 2
</script>
<template>{{ used }}</template>
"#,
)
.unwrap();
let source = source.canonicalize().unwrap();
let mut project = VirtualProject::new(temp_dir.path()).unwrap();
project.register_path(&source).unwrap();
let virtual_file = project.find_by_original(&source).unwrap();
let virtual_uri = file_uri_for(&virtual_file.virtual_path);
let diagnostics = map_batch_diagnostics(
vec![(
virtual_uri,
vec![
ts6133_diagnostic_at(
virtual_file.content.as_str(),
"unusedLocal",
"'unusedLocal' is declared but its value is never read.",
),
ts6133_diagnostic_at(
virtual_file.content.as_str(),
"const defineProps",
"'defineProps' is declared but its value is never read.",
),
],
)],
&project,
);
assert_eq!(diagnostics.len(), 1, "{diagnostics:#?}");
assert_eq!(diagnostics[0].file, source);
assert_eq!(diagnostics[0].line, 2);
assert_eq!(diagnostics[0].column, 6);
assert_eq!(diagnostics[0].code, Some(6133));
assert!(diagnostics[0].message.contains("unusedLocal"));
assert_eq!(diagnostics[0].block_type, Some(SfcBlockType::ScriptSetup));
}
fn ts2307_diagnostic_at(
virtual_source: &str,
needle: &str,
message: &str,
) -> crate::corsa_client::LspDiagnostic {
let (line, character) = virtual_position_for(virtual_source, needle);
crate::corsa_client::LspDiagnostic {
range: crate::corsa_client::LspRange {
start: crate::corsa_client::LspPosition { line, character },
end: crate::corsa_client::LspPosition {
line,
character: character + 1,
},
},
severity: Some(1),
code: Some(json!("TS2307")),
source: Some("ts".into()),
message: message.into(),
}
}
fn ts6133_diagnostic_at(
virtual_source: &str,
needle: &str,
message: &str,
) -> crate::corsa_client::LspDiagnostic {
let (line, character) = virtual_position_for(virtual_source, needle);
crate::corsa_client::LspDiagnostic {
range: crate::corsa_client::LspRange {
start: crate::corsa_client::LspPosition { line, character },
end: crate::corsa_client::LspPosition { line, character },
},
severity: Some(1),
code: Some(json!("TS6133")),
source: Some("ts".into()),
message: message.into(),
}
}
fn virtual_position_for(virtual_source: &str, needle: &str) -> (u32, u32) {
let offset = virtual_source
.find(needle)
.unwrap_or_else(|| panic!("expected virtual source to contain {needle:?}"));
LineIndex::new(virtual_source)
.offset_to_line_col(virtual_source, offset as u32)
.expect("virtual offset should map to LSP position")
}
fn file_uri_for(path: &std::path::Path) -> vize_carton::String {
cstr!("file://{}", path.display())
}
}