mirror of
https://github.com/denoland/deno.git
synced 2024-12-24 08:09:08 -05:00
fix(lsp): handle mbc documents properly (#9151)
Co-authored-by: Ryan Dahl <ry@tinyclouds.org>
This commit is contained in:
parent
ffa920e4b9
commit
1a9209d1e3
18 changed files with 1499 additions and 870 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -453,6 +453,7 @@ dependencies = [
|
|||
"tempfile",
|
||||
"termcolor",
|
||||
"test_util",
|
||||
"text-size",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-test",
|
||||
|
@ -2948,6 +2949,12 @@ dependencies = [
|
|||
"tokio-tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "text-size"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "288cb548dbe72b652243ea797201f3d481a0609a967980fcc5b2315ea811560a"
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.11.0"
|
||||
|
|
|
@ -74,6 +74,7 @@ swc_common = { version = "0.10.8", features = ["sourcemap"] }
|
|||
swc_ecmascript = { version = "0.17.1", features = ["codegen", "dep_graph", "parser", "proposal", "react", "transforms", "typescript", "visit"] }
|
||||
tempfile = "3.1.0"
|
||||
termcolor = "1.1.2"
|
||||
text-size = "1.1.0"
|
||||
tokio = { version = "1.0.1", features = ["full"] }
|
||||
tokio-rustls = "0.22.0"
|
||||
uuid = { version = "0.8.2", features = ["v4"] }
|
||||
|
|
|
@ -4,7 +4,6 @@ use super::analysis::get_lint_references;
|
|||
use super::analysis::references_to_diagnostics;
|
||||
use super::analysis::ResolvedDependency;
|
||||
use super::language_server::StateSnapshot;
|
||||
use super::memory_cache::FileId;
|
||||
use super::tsc;
|
||||
|
||||
use crate::diagnostics;
|
||||
|
@ -13,7 +12,7 @@ use crate::media_type::MediaType;
|
|||
use deno_core::error::custom_error;
|
||||
use deno_core::error::AnyError;
|
||||
use deno_core::serde_json;
|
||||
use deno_core::serde_json::Value;
|
||||
use deno_core::ModuleSpecifier;
|
||||
use lspower::lsp_types;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
@ -28,43 +27,47 @@ pub enum DiagnosticSource {
|
|||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct DiagnosticCollection {
|
||||
map: HashMap<(FileId, DiagnosticSource), Vec<lsp_types::Diagnostic>>,
|
||||
versions: HashMap<FileId, i32>,
|
||||
changes: HashSet<FileId>,
|
||||
map: HashMap<(ModuleSpecifier, DiagnosticSource), Vec<lsp_types::Diagnostic>>,
|
||||
versions: HashMap<ModuleSpecifier, i32>,
|
||||
changes: HashSet<ModuleSpecifier>,
|
||||
}
|
||||
|
||||
impl DiagnosticCollection {
|
||||
pub fn set(
|
||||
&mut self,
|
||||
file_id: FileId,
|
||||
specifier: ModuleSpecifier,
|
||||
source: DiagnosticSource,
|
||||
version: Option<i32>,
|
||||
diagnostics: Vec<lsp_types::Diagnostic>,
|
||||
) {
|
||||
self.map.insert((file_id, source), diagnostics);
|
||||
self.map.insert((specifier.clone(), source), diagnostics);
|
||||
if let Some(version) = version {
|
||||
self.versions.insert(file_id, version);
|
||||
self.versions.insert(specifier.clone(), version);
|
||||
}
|
||||
self.changes.insert(file_id);
|
||||
self.changes.insert(specifier);
|
||||
}
|
||||
|
||||
pub fn diagnostics_for(
|
||||
&self,
|
||||
file_id: FileId,
|
||||
source: DiagnosticSource,
|
||||
specifier: &ModuleSpecifier,
|
||||
source: &DiagnosticSource,
|
||||
) -> impl Iterator<Item = &lsp_types::Diagnostic> {
|
||||
self.map.get(&(file_id, source)).into_iter().flatten()
|
||||
self
|
||||
.map
|
||||
.get(&(specifier.clone(), source.clone()))
|
||||
.into_iter()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub fn get_version(&self, file_id: &FileId) -> Option<i32> {
|
||||
self.versions.get(file_id).cloned()
|
||||
pub fn get_version(&self, specifier: &ModuleSpecifier) -> Option<i32> {
|
||||
self.versions.get(specifier).cloned()
|
||||
}
|
||||
|
||||
pub fn invalidate(&mut self, file_id: &FileId) {
|
||||
self.versions.remove(file_id);
|
||||
pub fn invalidate(&mut self, specifier: &ModuleSpecifier) {
|
||||
self.versions.remove(specifier);
|
||||
}
|
||||
|
||||
pub fn take_changes(&mut self) -> Option<HashSet<FileId>> {
|
||||
pub fn take_changes(&mut self) -> Option<HashSet<ModuleSpecifier>> {
|
||||
if self.changes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
@ -72,7 +75,8 @@ impl DiagnosticCollection {
|
|||
}
|
||||
}
|
||||
|
||||
pub type DiagnosticVec = Vec<(FileId, Option<i32>, Vec<lsp_types::Diagnostic>)>;
|
||||
pub type DiagnosticVec =
|
||||
Vec<(ModuleSpecifier, Option<i32>, Vec<lsp_types::Diagnostic>)>;
|
||||
|
||||
pub async fn generate_lint_diagnostics(
|
||||
state_snapshot: StateSnapshot,
|
||||
|
@ -81,25 +85,24 @@ pub async fn generate_lint_diagnostics(
|
|||
tokio::task::spawn_blocking(move || {
|
||||
let mut diagnostic_list = Vec::new();
|
||||
|
||||
let file_cache = state_snapshot.file_cache.lock().unwrap();
|
||||
for (specifier, doc_data) in state_snapshot.doc_data.iter() {
|
||||
let file_id = file_cache.lookup(specifier).unwrap();
|
||||
let version = doc_data.version;
|
||||
let current_version = diagnostic_collection.get_version(&file_id);
|
||||
let documents = state_snapshot.documents.lock().unwrap();
|
||||
for specifier in documents.open_specifiers() {
|
||||
let version = documents.version(specifier);
|
||||
let current_version = diagnostic_collection.get_version(specifier);
|
||||
if version != current_version {
|
||||
let media_type = MediaType::from(specifier);
|
||||
if let Ok(source_code) = file_cache.get_contents(file_id) {
|
||||
if let Ok(Some(source_code)) = documents.content(specifier) {
|
||||
if let Ok(references) =
|
||||
get_lint_references(specifier, &media_type, &source_code)
|
||||
{
|
||||
if !references.is_empty() {
|
||||
diagnostic_list.push((
|
||||
file_id,
|
||||
specifier.clone(),
|
||||
version,
|
||||
references_to_diagnostics(references),
|
||||
));
|
||||
} else {
|
||||
diagnostic_list.push((file_id, version, Vec::new()));
|
||||
diagnostic_list.push((specifier.clone(), version, Vec::new()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -154,7 +157,7 @@ fn to_lsp_range(
|
|||
}
|
||||
}
|
||||
|
||||
type TsDiagnostics = Vec<diagnostics::Diagnostic>;
|
||||
type TsDiagnostics = HashMap<String, Vec<diagnostics::Diagnostic>>;
|
||||
|
||||
fn get_diagnostic_message(diagnostic: &diagnostics::Diagnostic) -> String {
|
||||
if let Some(message) = diagnostic.message_text.clone() {
|
||||
|
@ -197,65 +200,70 @@ fn to_lsp_related_information(
|
|||
}
|
||||
|
||||
fn ts_json_to_diagnostics(
|
||||
value: Value,
|
||||
) -> Result<Vec<lsp_types::Diagnostic>, AnyError> {
|
||||
let ts_diagnostics: TsDiagnostics = serde_json::from_value(value)?;
|
||||
Ok(
|
||||
ts_diagnostics
|
||||
.iter()
|
||||
.filter_map(|d| {
|
||||
if let (Some(start), Some(end)) = (&d.start, &d.end) {
|
||||
Some(lsp_types::Diagnostic {
|
||||
range: to_lsp_range(start, end),
|
||||
severity: Some((&d.category).into()),
|
||||
code: Some(lsp_types::NumberOrString::Number(d.code as i32)),
|
||||
code_description: None,
|
||||
source: Some("deno-ts".to_string()),
|
||||
message: get_diagnostic_message(d),
|
||||
related_information: to_lsp_related_information(
|
||||
&d.related_information,
|
||||
),
|
||||
tags: match d.code {
|
||||
// These are codes that indicate the variable is unused.
|
||||
6133 | 6192 | 6196 => {
|
||||
Some(vec![lsp_types::DiagnosticTag::Unnecessary])
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
data: None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
diagnostics: &[diagnostics::Diagnostic],
|
||||
) -> Vec<lsp_types::Diagnostic> {
|
||||
diagnostics
|
||||
.iter()
|
||||
.filter_map(|d| {
|
||||
if let (Some(start), Some(end)) = (&d.start, &d.end) {
|
||||
Some(lsp_types::Diagnostic {
|
||||
range: to_lsp_range(start, end),
|
||||
severity: Some((&d.category).into()),
|
||||
code: Some(lsp_types::NumberOrString::Number(d.code as i32)),
|
||||
code_description: None,
|
||||
source: Some("deno-ts".to_string()),
|
||||
message: get_diagnostic_message(d),
|
||||
related_information: to_lsp_related_information(
|
||||
&d.related_information,
|
||||
),
|
||||
tags: match d.code {
|
||||
// These are codes that indicate the variable is unused.
|
||||
6133 | 6192 | 6196 => {
|
||||
Some(vec![lsp_types::DiagnosticTag::Unnecessary])
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
data: None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn generate_ts_diagnostics(
|
||||
ts_server: &tsc::TsServer,
|
||||
diagnostic_collection: &DiagnosticCollection,
|
||||
state_snapshot: StateSnapshot,
|
||||
diagnostic_collection: DiagnosticCollection,
|
||||
ts_server: &tsc::TsServer,
|
||||
) -> Result<DiagnosticVec, AnyError> {
|
||||
let mut diagnostics = Vec::new();
|
||||
let state_snapshot_ = state_snapshot.clone();
|
||||
for (specifier, doc_data) in state_snapshot_.doc_data.iter() {
|
||||
let file_id = {
|
||||
// TODO(lucacasonato): this is highly inefficient
|
||||
let file_cache = state_snapshot_.file_cache.lock().unwrap();
|
||||
file_cache.lookup(specifier).unwrap()
|
||||
};
|
||||
let version = doc_data.version;
|
||||
let current_version = diagnostic_collection.get_version(&file_id);
|
||||
if version != current_version {
|
||||
let req = tsc::RequestMethod::GetDiagnostics(specifier.clone());
|
||||
let ts_diagnostics = ts_json_to_diagnostics(
|
||||
ts_server.request(state_snapshot.clone(), req).await?,
|
||||
)?;
|
||||
diagnostics.push((file_id, version, ts_diagnostics));
|
||||
let mut specifiers = Vec::new();
|
||||
{
|
||||
let documents = state_snapshot.documents.lock().unwrap();
|
||||
for specifier in documents.open_specifiers() {
|
||||
let version = documents.version(specifier);
|
||||
let current_version = diagnostic_collection.get_version(specifier);
|
||||
if version != current_version {
|
||||
specifiers.push(specifier.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
if !specifiers.is_empty() {
|
||||
let req = tsc::RequestMethod::GetDiagnostics(specifiers);
|
||||
let res = ts_server.request(state_snapshot.clone(), req).await?;
|
||||
let ts_diagnostic_map: TsDiagnostics = serde_json::from_value(res)?;
|
||||
for (specifier_str, ts_diagnostics) in ts_diagnostic_map.iter() {
|
||||
let specifier = ModuleSpecifier::resolve_url(specifier_str)?;
|
||||
let version =
|
||||
state_snapshot.documents.lock().unwrap().version(&specifier);
|
||||
diagnostics.push((
|
||||
specifier,
|
||||
version,
|
||||
ts_json_to_diagnostics(ts_diagnostics),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(diagnostics)
|
||||
}
|
||||
|
||||
|
@ -266,19 +274,18 @@ pub async fn generate_dependency_diagnostics(
|
|||
tokio::task::spawn_blocking(move || {
|
||||
let mut diagnostics = Vec::new();
|
||||
|
||||
let file_cache = state_snapshot.file_cache.lock().unwrap();
|
||||
let mut sources = if let Ok(sources) = state_snapshot.sources.lock() {
|
||||
sources
|
||||
} else {
|
||||
return Err(custom_error("Deadlock", "deadlock locking sources"));
|
||||
};
|
||||
for (specifier, doc_data) in state_snapshot.doc_data.iter() {
|
||||
let file_id = file_cache.lookup(specifier).unwrap();
|
||||
let version = doc_data.version;
|
||||
let current_version = diagnostic_collection.get_version(&file_id);
|
||||
let documents = state_snapshot.documents.lock().unwrap();
|
||||
for specifier in documents.open_specifiers() {
|
||||
let version = documents.version(specifier);
|
||||
let current_version = diagnostic_collection.get_version(specifier);
|
||||
if version != current_version {
|
||||
let mut diagnostic_list = Vec::new();
|
||||
if let Some(dependencies) = &doc_data.dependencies {
|
||||
if let Some(dependencies) = documents.dependencies(specifier) {
|
||||
for (_, dependency) in dependencies.iter() {
|
||||
if let (Some(code), Some(range)) = (
|
||||
&dependency.maybe_code,
|
||||
|
@ -299,7 +306,7 @@ pub async fn generate_dependency_diagnostics(
|
|||
})
|
||||
}
|
||||
ResolvedDependency::Resolved(specifier) => {
|
||||
if !(state_snapshot.doc_data.contains_key(&specifier) || sources.contains(&specifier)) {
|
||||
if !(documents.contains(&specifier) || sources.contains(&specifier)) {
|
||||
let is_local = specifier.as_url().scheme() == "file";
|
||||
diagnostic_list.push(lsp_types::Diagnostic {
|
||||
range: *range,
|
||||
|
@ -322,7 +329,7 @@ pub async fn generate_dependency_diagnostics(
|
|||
}
|
||||
}
|
||||
}
|
||||
diagnostics.push((file_id, version, diagnostic_list))
|
||||
diagnostics.push((specifier.clone(), version, diagnostic_list))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
315
cli/lsp/documents.rs
Normal file
315
cli/lsp/documents.rs
Normal file
|
@ -0,0 +1,315 @@
|
|||
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
use super::analysis;
|
||||
use super::text::LineIndex;
|
||||
|
||||
use crate::import_map::ImportMap;
|
||||
use crate::media_type::MediaType;
|
||||
|
||||
use deno_core::error::custom_error;
|
||||
use deno_core::error::AnyError;
|
||||
use deno_core::error::Context;
|
||||
use deno_core::ModuleSpecifier;
|
||||
use lspower::lsp_types::TextDocumentContentChangeEvent;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Range;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum IndexValid {
|
||||
All,
|
||||
UpTo(u32),
|
||||
}
|
||||
|
||||
impl IndexValid {
|
||||
fn covers(&self, line: u32) -> bool {
|
||||
match *self {
|
||||
IndexValid::UpTo(to) => to > line,
|
||||
IndexValid::All => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DocumentData {
|
||||
bytes: Option<Vec<u8>>,
|
||||
line_index: Option<LineIndex>,
|
||||
dependencies: Option<HashMap<String, analysis::Dependency>>,
|
||||
version: Option<i32>,
|
||||
}
|
||||
|
||||
impl DocumentData {
|
||||
pub fn apply_content_changes(
|
||||
&mut self,
|
||||
content_changes: Vec<TextDocumentContentChangeEvent>,
|
||||
) -> Result<(), AnyError> {
|
||||
if self.bytes.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let content = &mut String::from_utf8(self.bytes.clone().unwrap())
|
||||
.context("unable to parse bytes to string")?;
|
||||
let mut line_index = if let Some(line_index) = &self.line_index {
|
||||
line_index.clone()
|
||||
} else {
|
||||
LineIndex::new(&content)
|
||||
};
|
||||
let mut index_valid = IndexValid::All;
|
||||
for change in content_changes {
|
||||
if let Some(range) = change.range {
|
||||
if !index_valid.covers(range.start.line) {
|
||||
line_index = LineIndex::new(&content);
|
||||
}
|
||||
index_valid = IndexValid::UpTo(range.start.line);
|
||||
let range = line_index.get_text_range(range)?;
|
||||
content.replace_range(Range::<usize>::from(range), &change.text);
|
||||
} else {
|
||||
*content = change.text;
|
||||
index_valid = IndexValid::UpTo(0);
|
||||
}
|
||||
}
|
||||
self.bytes = Some(content.as_bytes().to_owned());
|
||||
self.line_index = if index_valid == IndexValid::All {
|
||||
Some(line_index)
|
||||
} else {
|
||||
Some(LineIndex::new(&content))
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn content(&self) -> Result<Option<String>, AnyError> {
|
||||
if let Some(bytes) = self.bytes.clone() {
|
||||
Ok(Some(
|
||||
String::from_utf8(bytes).context("cannot decode bytes to string")?,
|
||||
))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DocumentCache {
|
||||
docs: HashMap<ModuleSpecifier, DocumentData>,
|
||||
}
|
||||
|
||||
impl DocumentCache {
|
||||
pub fn analyze_dependencies(
|
||||
&mut self,
|
||||
specifier: &ModuleSpecifier,
|
||||
maybe_import_map: &Option<ImportMap>,
|
||||
) -> Result<(), AnyError> {
|
||||
if !self.contains(specifier) {
|
||||
return Err(custom_error(
|
||||
"NotFound",
|
||||
format!(
|
||||
"The specifier (\"{}\") does not exist in the document cache.",
|
||||
specifier
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let doc = self.docs.get_mut(specifier).unwrap();
|
||||
if let Some(source) = &doc.content()? {
|
||||
if let Some((dependencies, _)) = analysis::analyze_dependencies(
|
||||
specifier,
|
||||
source,
|
||||
&MediaType::from(specifier),
|
||||
maybe_import_map,
|
||||
) {
|
||||
doc.dependencies = Some(dependencies);
|
||||
} else {
|
||||
doc.dependencies = None;
|
||||
}
|
||||
} else {
|
||||
doc.dependencies = None;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn change(
|
||||
&mut self,
|
||||
specifier: &ModuleSpecifier,
|
||||
version: i32,
|
||||
content_changes: Vec<TextDocumentContentChangeEvent>,
|
||||
) -> Result<(), AnyError> {
|
||||
if !self.contains(specifier) {
|
||||
return Err(custom_error(
|
||||
"NotFound",
|
||||
format!(
|
||||
"The specifier (\"{}\") does not exist in the document cache.",
|
||||
specifier
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let doc = self.docs.get_mut(specifier).unwrap();
|
||||
doc.apply_content_changes(content_changes)?;
|
||||
doc.version = Some(version);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn close(&mut self, specifier: &ModuleSpecifier) {
|
||||
if let Some(mut doc) = self.docs.get_mut(specifier) {
|
||||
doc.version = None;
|
||||
doc.dependencies = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains(&self, specifier: &ModuleSpecifier) -> bool {
|
||||
self.docs.contains_key(specifier)
|
||||
}
|
||||
|
||||
pub fn content(
|
||||
&self,
|
||||
specifier: &ModuleSpecifier,
|
||||
) -> Result<Option<String>, AnyError> {
|
||||
if let Some(doc) = self.docs.get(specifier) {
|
||||
doc.content()
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dependencies(
|
||||
&self,
|
||||
specifier: &ModuleSpecifier,
|
||||
) -> Option<HashMap<String, analysis::Dependency>> {
|
||||
let doc = self.docs.get(specifier)?;
|
||||
doc.dependencies.clone()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.docs.iter().count()
|
||||
}
|
||||
|
||||
pub fn line_index(&self, specifier: &ModuleSpecifier) -> Option<LineIndex> {
|
||||
let doc = self.docs.get(specifier)?;
|
||||
doc.line_index.clone()
|
||||
}
|
||||
|
||||
pub fn open(
|
||||
&mut self,
|
||||
specifier: ModuleSpecifier,
|
||||
version: i32,
|
||||
text: String,
|
||||
) {
|
||||
self.docs.insert(
|
||||
specifier,
|
||||
DocumentData {
|
||||
bytes: Some(text.as_bytes().to_owned()),
|
||||
version: Some(version),
|
||||
line_index: Some(LineIndex::new(&text)),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn open_specifiers(&self) -> Vec<&ModuleSpecifier> {
|
||||
self
|
||||
.docs
|
||||
.iter()
|
||||
.filter_map(|(key, data)| {
|
||||
if data.version.is_some() {
|
||||
Some(key)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn version(&self, specifier: &ModuleSpecifier) -> Option<i32> {
|
||||
self.docs.get(specifier).and_then(|doc| doc.version)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use lspower::lsp_types;
|
||||
|
||||
#[test]
|
||||
fn test_document_cache_contains() {
|
||||
let mut document_cache = DocumentCache::default();
|
||||
let specifier = ModuleSpecifier::resolve_url("file:///a/b.ts").unwrap();
|
||||
let missing_specifier =
|
||||
ModuleSpecifier::resolve_url("file:///a/c.ts").unwrap();
|
||||
document_cache.open(
|
||||
specifier.clone(),
|
||||
1,
|
||||
"console.log(\"Hello Deno\");\n".to_owned(),
|
||||
);
|
||||
assert!(document_cache.contains(&specifier));
|
||||
assert!(!document_cache.contains(&missing_specifier));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_document_cache_change() {
|
||||
let mut document_cache = DocumentCache::default();
|
||||
let specifier = ModuleSpecifier::resolve_url("file:///a/b.ts").unwrap();
|
||||
document_cache.open(
|
||||
specifier.clone(),
|
||||
1,
|
||||
"console.log(\"Hello deno\");\n".to_owned(),
|
||||
);
|
||||
document_cache
|
||||
.change(
|
||||
&specifier,
|
||||
2,
|
||||
vec![lsp_types::TextDocumentContentChangeEvent {
|
||||
range: Some(lsp_types::Range {
|
||||
start: lsp_types::Position {
|
||||
line: 0,
|
||||
character: 19,
|
||||
},
|
||||
end: lsp_types::Position {
|
||||
line: 0,
|
||||
character: 20,
|
||||
},
|
||||
}),
|
||||
range_length: Some(1),
|
||||
text: "D".to_string(),
|
||||
}],
|
||||
)
|
||||
.expect("failed to make changes");
|
||||
let actual = document_cache
|
||||
.content(&specifier)
|
||||
.expect("failed to get content");
|
||||
assert_eq!(actual, Some("console.log(\"Hello Deno\");\n".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_document_cache_change_utf16() {
|
||||
let mut document_cache = DocumentCache::default();
|
||||
let specifier = ModuleSpecifier::resolve_url("file:///a/b.ts").unwrap();
|
||||
document_cache.open(
|
||||
specifier.clone(),
|
||||
1,
|
||||
"console.log(\"Hello 🦕\");\n".to_owned(),
|
||||
);
|
||||
document_cache
|
||||
.change(
|
||||
&specifier,
|
||||
2,
|
||||
vec![lsp_types::TextDocumentContentChangeEvent {
|
||||
range: Some(lsp_types::Range {
|
||||
start: lsp_types::Position {
|
||||
line: 0,
|
||||
character: 19,
|
||||
},
|
||||
end: lsp_types::Position {
|
||||
line: 0,
|
||||
character: 21,
|
||||
},
|
||||
}),
|
||||
range_length: Some(2),
|
||||
text: "Deno".to_string(),
|
||||
}],
|
||||
)
|
||||
.expect("failed to make changes");
|
||||
let actual = document_cache
|
||||
.content(&specifier)
|
||||
.expect("failed to get content");
|
||||
assert_eq!(actual, Some("console.log(\"Hello Deno\");\n".to_string()));
|
||||
}
|
||||
}
|
|
@ -23,33 +23,31 @@ use tokio::fs;
|
|||
|
||||
use crate::deno_dir;
|
||||
use crate::import_map::ImportMap;
|
||||
use crate::media_type::MediaType;
|
||||
use crate::tsc_config::parse_config;
|
||||
use crate::tsc_config::TsConfig;
|
||||
|
||||
use super::analysis;
|
||||
use super::capabilities;
|
||||
use super::config::Config;
|
||||
use super::diagnostics;
|
||||
use super::diagnostics::DiagnosticCollection;
|
||||
use super::diagnostics::DiagnosticSource;
|
||||
use super::memory_cache::MemoryCache;
|
||||
use super::documents::DocumentCache;
|
||||
use super::sources;
|
||||
use super::sources::Sources;
|
||||
use super::text;
|
||||
use super::text::apply_content_changes;
|
||||
use super::text::LineIndex;
|
||||
use super::tsc;
|
||||
use super::tsc::AssetDocument;
|
||||
use super::tsc::TsServer;
|
||||
use super::utils;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LanguageServer {
|
||||
assets: Arc<Mutex<HashMap<ModuleSpecifier, Option<String>>>>,
|
||||
assets: Arc<Mutex<HashMap<ModuleSpecifier, Option<AssetDocument>>>>,
|
||||
client: Client,
|
||||
ts_server: TsServer,
|
||||
config: Arc<Mutex<Config>>,
|
||||
doc_data: Arc<Mutex<HashMap<ModuleSpecifier, DocumentData>>>,
|
||||
file_cache: Arc<Mutex<MemoryCache>>,
|
||||
documents: Arc<Mutex<DocumentCache>>,
|
||||
sources: Arc<Mutex<Sources>>,
|
||||
diagnostics: Arc<Mutex<DiagnosticCollection>>,
|
||||
maybe_config_uri: Arc<Mutex<Option<Url>>>,
|
||||
|
@ -59,9 +57,8 @@ pub struct LanguageServer {
|
|||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct StateSnapshot {
|
||||
pub assets: Arc<Mutex<HashMap<ModuleSpecifier, Option<String>>>>,
|
||||
pub doc_data: HashMap<ModuleSpecifier, DocumentData>,
|
||||
pub file_cache: Arc<Mutex<MemoryCache>>,
|
||||
pub assets: Arc<Mutex<HashMap<ModuleSpecifier, Option<AssetDocument>>>>,
|
||||
pub documents: Arc<Mutex<DocumentCache>>,
|
||||
pub sources: Arc<Mutex<Sources>>,
|
||||
}
|
||||
|
||||
|
@ -78,8 +75,7 @@ impl LanguageServer {
|
|||
client,
|
||||
ts_server: TsServer::new(),
|
||||
config: Default::default(),
|
||||
doc_data: Default::default(),
|
||||
file_cache: Default::default(),
|
||||
documents: Default::default(),
|
||||
sources,
|
||||
diagnostics: Default::default(),
|
||||
maybe_config_uri: Default::default(),
|
||||
|
@ -93,34 +89,65 @@ impl LanguageServer {
|
|||
config.settings.enable
|
||||
}
|
||||
|
||||
/// Searches assets, open documents and external sources for a line_index,
|
||||
/// which might be performed asynchronously, hydrating in memory caches for
|
||||
/// subsequent requests.
|
||||
pub async fn get_line_index(
|
||||
&self,
|
||||
specifier: ModuleSpecifier,
|
||||
) -> Result<Vec<u32>, AnyError> {
|
||||
let line_index = if specifier.as_url().scheme() == "asset" {
|
||||
let state_snapshot = self.snapshot();
|
||||
if let Some(source) =
|
||||
tsc::get_asset(&specifier, &self.ts_server, &state_snapshot).await?
|
||||
{
|
||||
text::index_lines(&source)
|
||||
} else {
|
||||
return Err(anyhow!("asset source missing: {}", specifier));
|
||||
}
|
||||
} else {
|
||||
let file_cache = self.file_cache.lock().unwrap();
|
||||
if let Some(file_id) = file_cache.lookup(&specifier) {
|
||||
let file_text = file_cache.get_contents(file_id)?;
|
||||
text::index_lines(&file_text)
|
||||
} else {
|
||||
let mut sources = self.sources.lock().unwrap();
|
||||
if let Some(line_index) = sources.get_line_index(&specifier) {
|
||||
line_index
|
||||
) -> Result<LineIndex, AnyError> {
|
||||
if specifier.as_url().scheme() == "asset" {
|
||||
let maybe_asset =
|
||||
{ self.assets.lock().unwrap().get(&specifier).cloned() };
|
||||
if let Some(maybe_asset) = maybe_asset {
|
||||
if let Some(asset) = maybe_asset {
|
||||
Ok(asset.line_index)
|
||||
} else {
|
||||
return Err(anyhow!("source for specifier not found: {}", specifier));
|
||||
Err(anyhow!("asset is missing: {}", specifier))
|
||||
}
|
||||
} else {
|
||||
let state_snapshot = self.snapshot();
|
||||
if let Some(asset) =
|
||||
tsc::get_asset(&specifier, &self.ts_server, &state_snapshot).await?
|
||||
{
|
||||
Ok(asset.line_index)
|
||||
} else {
|
||||
Err(anyhow!("asset is missing: {}", specifier))
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(line_index)
|
||||
} else if let Some(line_index) =
|
||||
self.documents.lock().unwrap().line_index(&specifier)
|
||||
{
|
||||
Ok(line_index)
|
||||
} else if let Some(line_index) =
|
||||
self.sources.lock().unwrap().get_line_index(&specifier)
|
||||
{
|
||||
Ok(line_index)
|
||||
} else {
|
||||
Err(anyhow!("Unable to find line index for: {}", specifier))
|
||||
}
|
||||
}
|
||||
|
||||
/// Only searches already cached assets and documents for a line index. If
|
||||
/// the line index cannot be found, `None` is returned.
|
||||
pub fn get_line_index_sync(
|
||||
&self,
|
||||
specifier: &ModuleSpecifier,
|
||||
) -> Option<LineIndex> {
|
||||
if specifier.as_url().scheme() == "asset" {
|
||||
if let Some(Some(asset)) = self.assets.lock().unwrap().get(specifier) {
|
||||
Some(asset.line_index.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
let documents = self.documents.lock().unwrap();
|
||||
if documents.contains(specifier) {
|
||||
documents.line_index(specifier)
|
||||
} else {
|
||||
self.sources.lock().unwrap().get_line_index(specifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn prepare_diagnostics(&self) -> Result<(), AnyError> {
|
||||
|
@ -130,6 +157,7 @@ impl LanguageServer {
|
|||
};
|
||||
|
||||
let lint = async {
|
||||
let mut disturbed = false;
|
||||
if lint_enabled {
|
||||
let diagnostic_collection = self.diagnostics.lock().unwrap().clone();
|
||||
let diagnostics = diagnostics::generate_lint_diagnostics(
|
||||
|
@ -137,59 +165,50 @@ impl LanguageServer {
|
|||
diagnostic_collection,
|
||||
)
|
||||
.await;
|
||||
disturbed = !diagnostics.is_empty();
|
||||
{
|
||||
let mut diagnostics_collection = self.diagnostics.lock().unwrap();
|
||||
for (file_id, version, diagnostics) in diagnostics {
|
||||
for (specifier, version, diagnostics) in diagnostics {
|
||||
diagnostics_collection.set(
|
||||
file_id,
|
||||
specifier,
|
||||
DiagnosticSource::Lint,
|
||||
version,
|
||||
diagnostics,
|
||||
);
|
||||
}
|
||||
}
|
||||
self.publish_diagnostics().await?
|
||||
};
|
||||
|
||||
Ok::<(), AnyError>(())
|
||||
Ok::<bool, AnyError>(disturbed)
|
||||
};
|
||||
|
||||
let ts = async {
|
||||
let mut disturbed = false;
|
||||
if enabled {
|
||||
let diagnostics = {
|
||||
let diagnostic_collection = self.diagnostics.lock().unwrap().clone();
|
||||
match diagnostics::generate_ts_diagnostics(
|
||||
&self.ts_server,
|
||||
&diagnostic_collection,
|
||||
self.snapshot(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(diagnostics) => diagnostics,
|
||||
Err(err) => {
|
||||
error!("Error processing TypeScript diagnostics:\n{}", err);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
};
|
||||
let diagnostics_collection = self.diagnostics.lock().unwrap().clone();
|
||||
let diagnostics = diagnostics::generate_ts_diagnostics(
|
||||
self.snapshot(),
|
||||
diagnostics_collection,
|
||||
&self.ts_server,
|
||||
)
|
||||
.await?;
|
||||
disturbed = !diagnostics.is_empty();
|
||||
{
|
||||
let mut diagnostics_collection = self.diagnostics.lock().unwrap();
|
||||
for (file_id, version, diagnostics) in diagnostics {
|
||||
for (specifier, version, diagnostics) in diagnostics {
|
||||
diagnostics_collection.set(
|
||||
file_id,
|
||||
specifier,
|
||||
DiagnosticSource::TypeScript,
|
||||
version,
|
||||
diagnostics,
|
||||
);
|
||||
}
|
||||
};
|
||||
self.publish_diagnostics().await?
|
||||
}
|
||||
|
||||
Ok::<(), AnyError>(())
|
||||
}
|
||||
};
|
||||
Ok::<bool, AnyError>(disturbed)
|
||||
};
|
||||
|
||||
let deps = async {
|
||||
let mut disturbed = false;
|
||||
if enabled {
|
||||
let diagnostics_collection = self.diagnostics.lock().unwrap().clone();
|
||||
let diagnostics = diagnostics::generate_dependency_diagnostics(
|
||||
|
@ -197,27 +216,26 @@ impl LanguageServer {
|
|||
diagnostics_collection,
|
||||
)
|
||||
.await?;
|
||||
disturbed = !diagnostics.is_empty();
|
||||
{
|
||||
let mut diagnostics_collection = self.diagnostics.lock().unwrap();
|
||||
for (file_id, version, diagnostics) in diagnostics {
|
||||
for (specifier, version, diagnostics) in diagnostics {
|
||||
diagnostics_collection.set(
|
||||
file_id,
|
||||
specifier,
|
||||
DiagnosticSource::Deno,
|
||||
version,
|
||||
diagnostics,
|
||||
);
|
||||
}
|
||||
}
|
||||
self.publish_diagnostics().await?
|
||||
};
|
||||
|
||||
Ok::<(), AnyError>(())
|
||||
Ok::<bool, AnyError>(disturbed)
|
||||
};
|
||||
|
||||
let (lint_res, ts_res, deps_res) = tokio::join!(lint, ts, deps);
|
||||
lint_res?;
|
||||
ts_res?;
|
||||
deps_res?;
|
||||
if lint_res? || ts_res? || deps_res? {
|
||||
self.publish_diagnostics().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -230,7 +248,7 @@ impl LanguageServer {
|
|||
};
|
||||
if let Some(diagnostic_changes) = maybe_changes {
|
||||
let settings = self.config.lock().unwrap().settings.clone();
|
||||
for file_id in diagnostic_changes {
|
||||
for specifier in diagnostic_changes {
|
||||
// TODO(@kitsonk) not totally happy with the way we collect and store
|
||||
// different types of diagnostics and offer them up to the client, we
|
||||
// do need to send "empty" vectors though when a particular feature is
|
||||
|
@ -238,7 +256,7 @@ impl LanguageServer {
|
|||
// diagnostics
|
||||
let mut diagnostics: Vec<Diagnostic> = if settings.lint {
|
||||
diagnostics_collection
|
||||
.diagnostics_for(file_id, DiagnosticSource::Lint)
|
||||
.diagnostics_for(&specifier, &DiagnosticSource::Lint)
|
||||
.cloned()
|
||||
.collect()
|
||||
} else {
|
||||
|
@ -247,27 +265,17 @@ impl LanguageServer {
|
|||
if self.enabled() {
|
||||
diagnostics.extend(
|
||||
diagnostics_collection
|
||||
.diagnostics_for(file_id, DiagnosticSource::TypeScript)
|
||||
.diagnostics_for(&specifier, &DiagnosticSource::TypeScript)
|
||||
.cloned(),
|
||||
);
|
||||
diagnostics.extend(
|
||||
diagnostics_collection
|
||||
.diagnostics_for(file_id, DiagnosticSource::Deno)
|
||||
.diagnostics_for(&specifier, &DiagnosticSource::Deno)
|
||||
.cloned(),
|
||||
);
|
||||
}
|
||||
let specifier = {
|
||||
let file_cache = self.file_cache.lock().unwrap();
|
||||
file_cache.get_specifier(file_id).clone()
|
||||
};
|
||||
let uri = specifier.as_url().clone();
|
||||
let version = if let Some(doc_data) =
|
||||
self.doc_data.lock().unwrap().get(&specifier)
|
||||
{
|
||||
doc_data.version
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let version = self.documents.lock().unwrap().version(&specifier);
|
||||
self
|
||||
.client
|
||||
.publish_diagnostics(uri, diagnostics, version)
|
||||
|
@ -281,8 +289,7 @@ impl LanguageServer {
|
|||
pub fn snapshot(&self) -> StateSnapshot {
|
||||
StateSnapshot {
|
||||
assets: self.assets.clone(),
|
||||
doc_data: self.doc_data.lock().unwrap().clone(),
|
||||
file_cache: self.file_cache.clone(),
|
||||
documents: self.documents.clone(),
|
||||
sources: self.sources.clone(),
|
||||
}
|
||||
}
|
||||
|
@ -507,61 +514,48 @@ impl lspower::LanguageServer for LanguageServer {
|
|||
return;
|
||||
}
|
||||
let specifier = utils::normalize_url(params.text_document.uri);
|
||||
let maybe_import_map = self.maybe_import_map.lock().unwrap().clone();
|
||||
if self
|
||||
.doc_data
|
||||
self.documents.lock().unwrap().open(
|
||||
specifier.clone(),
|
||||
params.text_document.version,
|
||||
params.text_document.text,
|
||||
);
|
||||
if let Err(err) = self
|
||||
.documents
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(
|
||||
specifier.clone(),
|
||||
DocumentData::new(
|
||||
specifier.clone(),
|
||||
params.text_document.version,
|
||||
¶ms.text_document.text,
|
||||
maybe_import_map,
|
||||
),
|
||||
)
|
||||
.is_some()
|
||||
.analyze_dependencies(&specifier, &self.maybe_import_map.lock().unwrap())
|
||||
{
|
||||
error!("duplicate DidOpenTextDocument: {}", specifier);
|
||||
error!("{}", err);
|
||||
}
|
||||
|
||||
self
|
||||
.file_cache
|
||||
.lock()
|
||||
.unwrap()
|
||||
.set_contents(specifier, Some(params.text_document.text.into_bytes()));
|
||||
// TODO(@lucacasonato): error handling
|
||||
self.prepare_diagnostics().await.unwrap();
|
||||
// TODO(@kitsonk): how to better lazily do this?
|
||||
if let Err(err) = self.prepare_diagnostics().await {
|
||||
error!("{}", err);
|
||||
}
|
||||
}
|
||||
|
||||
async fn did_change(&self, params: DidChangeTextDocumentParams) {
|
||||
let specifier = utils::normalize_url(params.text_document.uri);
|
||||
let mut content = {
|
||||
let file_cache = self.file_cache.lock().unwrap();
|
||||
let file_id = file_cache.lookup(&specifier).unwrap();
|
||||
file_cache.get_contents(file_id).unwrap()
|
||||
};
|
||||
apply_content_changes(&mut content, params.content_changes);
|
||||
{
|
||||
let mut doc_data = self.doc_data.lock().unwrap();
|
||||
let doc_data = doc_data.get_mut(&specifier).unwrap();
|
||||
let maybe_import_map = self.maybe_import_map.lock().unwrap();
|
||||
doc_data.update(
|
||||
params.text_document.version,
|
||||
&content,
|
||||
&maybe_import_map,
|
||||
);
|
||||
if let Err(err) = self.documents.lock().unwrap().change(
|
||||
&specifier,
|
||||
params.text_document.version,
|
||||
params.content_changes,
|
||||
) {
|
||||
error!("{}", err);
|
||||
}
|
||||
|
||||
self
|
||||
.file_cache
|
||||
if let Err(err) = self
|
||||
.documents
|
||||
.lock()
|
||||
.unwrap()
|
||||
.set_contents(specifier, Some(content.into_bytes()));
|
||||
.analyze_dependencies(&specifier, &self.maybe_import_map.lock().unwrap())
|
||||
{
|
||||
error!("{}", err);
|
||||
}
|
||||
|
||||
// TODO(@lucacasonato): error handling
|
||||
self.prepare_diagnostics().await.unwrap();
|
||||
// TODO(@kitsonk): how to better lazily do this?
|
||||
if let Err(err) = self.prepare_diagnostics().await {
|
||||
error!("{}", err);
|
||||
}
|
||||
}
|
||||
|
||||
async fn did_close(&self, params: DidCloseTextDocumentParams) {
|
||||
|
@ -572,12 +566,12 @@ impl lspower::LanguageServer for LanguageServer {
|
|||
return;
|
||||
}
|
||||
let specifier = utils::normalize_url(params.text_document.uri);
|
||||
if self.doc_data.lock().unwrap().remove(&specifier).is_none() {
|
||||
error!("orphaned document: {}", specifier);
|
||||
self.documents.lock().unwrap().close(&specifier);
|
||||
|
||||
// TODO(@kitsonk): how to better lazily do this?
|
||||
if let Err(err) = self.prepare_diagnostics().await {
|
||||
error!("{}", err);
|
||||
}
|
||||
// TODO(@kitsonk) should we do garbage collection on the diagnostics?
|
||||
// TODO(@lucacasonato): error handling
|
||||
self.prepare_diagnostics().await.unwrap();
|
||||
}
|
||||
|
||||
async fn did_save(&self, _params: DidSaveTextDocumentParams) {
|
||||
|
@ -673,12 +667,17 @@ impl lspower::LanguageServer for LanguageServer {
|
|||
params: DocumentFormattingParams,
|
||||
) -> LspResult<Option<Vec<TextEdit>>> {
|
||||
let specifier = utils::normalize_url(params.text_document.uri.clone());
|
||||
let file_text = {
|
||||
let file_cache = self.file_cache.lock().unwrap();
|
||||
let file_id = file_cache.lookup(&specifier).unwrap();
|
||||
// TODO(lucacasonato): handle error properly
|
||||
file_cache.get_contents(file_id).unwrap()
|
||||
};
|
||||
let file_text = self
|
||||
.documents
|
||||
.lock()
|
||||
.unwrap()
|
||||
.content(&specifier)
|
||||
.map_err(|_| {
|
||||
LspError::invalid_params(
|
||||
"The specified file could not be found in memory.",
|
||||
)
|
||||
})?
|
||||
.unwrap();
|
||||
|
||||
let file_path =
|
||||
if let Ok(file_path) = params.text_document.uri.to_file_path() {
|
||||
|
@ -723,14 +722,18 @@ impl lspower::LanguageServer for LanguageServer {
|
|||
let specifier = utils::normalize_url(
|
||||
params.text_document_position_params.text_document.uri,
|
||||
);
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let line_index = self.get_line_index(specifier.clone()).await.unwrap();
|
||||
let line_index =
|
||||
if let Some(line_index) = self.get_line_index_sync(&specifier) {
|
||||
line_index
|
||||
} else {
|
||||
return Err(LspError::invalid_params(format!(
|
||||
"An unexpected specifier ({}) was provided.",
|
||||
specifier
|
||||
)));
|
||||
};
|
||||
let req = tsc::RequestMethod::GetQuickInfo((
|
||||
specifier,
|
||||
text::to_char_pos(
|
||||
&line_index,
|
||||
params.text_document_position_params.position,
|
||||
),
|
||||
line_index.offset_tsc(params.text_document_position_params.position)?,
|
||||
));
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let res = self.ts_server.request(self.snapshot(), req).await.unwrap();
|
||||
|
@ -738,7 +741,8 @@ impl lspower::LanguageServer for LanguageServer {
|
|||
let maybe_quick_info: Option<tsc::QuickInfo> =
|
||||
serde_json::from_value(res).unwrap();
|
||||
if let Some(quick_info) = maybe_quick_info {
|
||||
Ok(Some(quick_info.to_hover(&line_index)))
|
||||
let hover = quick_info.to_hover(&line_index);
|
||||
Ok(Some(hover))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
|
@ -754,15 +758,19 @@ impl lspower::LanguageServer for LanguageServer {
|
|||
let specifier = utils::normalize_url(
|
||||
params.text_document_position_params.text_document.uri,
|
||||
);
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let line_index = self.get_line_index(specifier.clone()).await.unwrap();
|
||||
let line_index =
|
||||
if let Some(line_index) = self.get_line_index_sync(&specifier) {
|
||||
line_index
|
||||
} else {
|
||||
return Err(LspError::invalid_params(format!(
|
||||
"An unexpected specifier ({}) was provided.",
|
||||
specifier
|
||||
)));
|
||||
};
|
||||
let files_to_search = vec![specifier.clone()];
|
||||
let req = tsc::RequestMethod::GetDocumentHighlights((
|
||||
specifier,
|
||||
text::to_char_pos(
|
||||
&line_index,
|
||||
params.text_document_position_params.position,
|
||||
),
|
||||
line_index.offset_tsc(params.text_document_position_params.position)?,
|
||||
files_to_search,
|
||||
));
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
|
@ -793,11 +801,18 @@ impl lspower::LanguageServer for LanguageServer {
|
|||
}
|
||||
let specifier =
|
||||
utils::normalize_url(params.text_document_position.text_document.uri);
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let line_index = self.get_line_index(specifier.clone()).await.unwrap();
|
||||
let line_index =
|
||||
if let Some(line_index) = self.get_line_index_sync(&specifier) {
|
||||
line_index
|
||||
} else {
|
||||
return Err(LspError::invalid_params(format!(
|
||||
"An unexpected specifier ({}) was provided.",
|
||||
specifier
|
||||
)));
|
||||
};
|
||||
let req = tsc::RequestMethod::GetReferences((
|
||||
specifier,
|
||||
text::to_char_pos(&line_index, params.text_document_position.position),
|
||||
line_index.offset_tsc(params.text_document_position.position)?,
|
||||
));
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let res = self.ts_server.request(self.snapshot(), req).await.unwrap();
|
||||
|
@ -836,14 +851,18 @@ impl lspower::LanguageServer for LanguageServer {
|
|||
let specifier = utils::normalize_url(
|
||||
params.text_document_position_params.text_document.uri,
|
||||
);
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let line_index = self.get_line_index(specifier.clone()).await.unwrap();
|
||||
let line_index =
|
||||
if let Some(line_index) = self.get_line_index_sync(&specifier) {
|
||||
line_index
|
||||
} else {
|
||||
return Err(LspError::invalid_params(format!(
|
||||
"An unexpected specifier ({}) was provided.",
|
||||
specifier
|
||||
)));
|
||||
};
|
||||
let req = tsc::RequestMethod::GetDefinition((
|
||||
specifier,
|
||||
text::to_char_pos(
|
||||
&line_index,
|
||||
params.text_document_position_params.position,
|
||||
),
|
||||
line_index.offset_tsc(params.text_document_position_params.position)?,
|
||||
));
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let res = self.ts_server.request(self.snapshot(), req).await.unwrap();
|
||||
|
@ -872,10 +891,18 @@ impl lspower::LanguageServer for LanguageServer {
|
|||
let specifier =
|
||||
utils::normalize_url(params.text_document_position.text_document.uri);
|
||||
// TODO(lucacasonato): handle error correctly
|
||||
let line_index = self.get_line_index(specifier.clone()).await.unwrap();
|
||||
let line_index =
|
||||
if let Some(line_index) = self.get_line_index_sync(&specifier) {
|
||||
line_index
|
||||
} else {
|
||||
return Err(LspError::invalid_params(format!(
|
||||
"An unexpected specifier ({}) was provided.",
|
||||
specifier
|
||||
)));
|
||||
};
|
||||
let req = tsc::RequestMethod::GetCompletions((
|
||||
specifier,
|
||||
text::to_char_pos(&line_index, params.text_document_position.position),
|
||||
line_index.offset_tsc(params.text_document_position.position)?,
|
||||
tsc::UserPreferences {
|
||||
// TODO(lucacasonato): enable this. see https://github.com/denoland/deno/pull/8651
|
||||
include_completions_with_insert_text: Some(false),
|
||||
|
@ -906,20 +933,18 @@ impl lspower::LanguageServer for LanguageServer {
|
|||
params.text_document_position_params.text_document.uri,
|
||||
);
|
||||
let line_index =
|
||||
self
|
||||
.get_line_index(specifier.clone())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to get line_index {:#?}", err);
|
||||
LspError::internal_error()
|
||||
})?;
|
||||
if let Some(line_index) = self.get_line_index_sync(&specifier) {
|
||||
line_index
|
||||
} else {
|
||||
return Err(LspError::invalid_params(format!(
|
||||
"An unexpected specifier ({}) was provided.",
|
||||
specifier
|
||||
)));
|
||||
};
|
||||
|
||||
let req = tsc::RequestMethod::GetImplementation((
|
||||
specifier,
|
||||
text::to_char_pos(
|
||||
&line_index,
|
||||
params.text_document_position_params.position,
|
||||
),
|
||||
line_index.offset_tsc(params.text_document_position_params.position)?,
|
||||
));
|
||||
let res =
|
||||
self
|
||||
|
@ -965,36 +990,36 @@ impl lspower::LanguageServer for LanguageServer {
|
|||
if !self.enabled() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let snapshot = self.snapshot();
|
||||
let specifier =
|
||||
utils::normalize_url(params.text_document_position.text_document.uri);
|
||||
|
||||
let line_index =
|
||||
self
|
||||
.get_line_index(specifier.clone())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to get line_index {:#?}", err);
|
||||
LspError::internal_error()
|
||||
})?;
|
||||
if let Some(line_index) = self.get_line_index_sync(&specifier) {
|
||||
line_index
|
||||
} else {
|
||||
return Err(LspError::invalid_params(format!(
|
||||
"An unexpected specifier ({}) was provided.",
|
||||
specifier
|
||||
)));
|
||||
};
|
||||
|
||||
let req = tsc::RequestMethod::FindRenameLocations((
|
||||
specifier,
|
||||
text::to_char_pos(&line_index, params.text_document_position.position),
|
||||
line_index.offset_tsc(params.text_document_position.position)?,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
));
|
||||
|
||||
let res = self
|
||||
.ts_server
|
||||
.request(snapshot.clone(), req)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to request to tsserver {:#?}", err);
|
||||
LspError::invalid_request()
|
||||
})?;
|
||||
let res =
|
||||
self
|
||||
.ts_server
|
||||
.request(self.snapshot(), req)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to request to tsserver {:#?}", err);
|
||||
LspError::invalid_request()
|
||||
})?;
|
||||
|
||||
let maybe_locations = serde_json::from_value::<
|
||||
Option<Vec<tsc::RenameLocation>>,
|
||||
|
@ -1007,26 +1032,22 @@ impl lspower::LanguageServer for LanguageServer {
|
|||
LspError::internal_error()
|
||||
})?;
|
||||
|
||||
match maybe_locations {
|
||||
Some(locations) => {
|
||||
let rename_locations = tsc::RenameLocations { locations };
|
||||
let workpace_edits = rename_locations
|
||||
.into_workspace_edit(
|
||||
snapshot,
|
||||
|s| self.get_line_index(s),
|
||||
¶ms.new_name,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!(
|
||||
"Failed to convert tsc::RenameLocations to WorkspaceEdit {:#?}",
|
||||
err
|
||||
);
|
||||
LspError::internal_error()
|
||||
})?;
|
||||
Ok(Some(workpace_edits))
|
||||
}
|
||||
None => Ok(None),
|
||||
if let Some(locations) = maybe_locations {
|
||||
let rename_locations = tsc::RenameLocations { locations };
|
||||
let workspace_edits = rename_locations
|
||||
.into_workspace_edit(
|
||||
¶ms.new_name,
|
||||
|s| self.get_line_index(s),
|
||||
|s| self.documents.lock().unwrap().version(&s),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to get workspace edits: {:#?}", err);
|
||||
LspError::internal_error()
|
||||
})?;
|
||||
Ok(Some(workspace_edits))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1090,12 +1111,8 @@ impl LanguageServer {
|
|||
error!("{}", err);
|
||||
LspError::internal_error()
|
||||
})?;
|
||||
{
|
||||
let file_cache = self.file_cache.lock().unwrap();
|
||||
if let Some(file_id) = file_cache.lookup(&specifier) {
|
||||
let mut diagnostics_collection = self.diagnostics.lock().unwrap();
|
||||
diagnostics_collection.invalidate(&file_id);
|
||||
}
|
||||
if self.documents.lock().unwrap().contains(&specifier) {
|
||||
self.diagnostics.lock().unwrap().invalidate(&specifier);
|
||||
}
|
||||
self.prepare_diagnostics().await.map_err(|err| {
|
||||
error!("{}", err);
|
||||
|
@ -1111,28 +1128,38 @@ impl LanguageServer {
|
|||
let specifier = utils::normalize_url(params.text_document.uri);
|
||||
let url = specifier.as_url();
|
||||
let contents = if url.as_str() == "deno:/status.md" {
|
||||
let file_cache = self.file_cache.lock().unwrap();
|
||||
let documents = self.documents.lock().unwrap();
|
||||
Some(format!(
|
||||
r#"# Deno Language Server Status
|
||||
|
||||
- Documents in memory: {}
|
||||
|
||||
"#,
|
||||
file_cache.len()
|
||||
documents.len()
|
||||
))
|
||||
} else {
|
||||
match url.scheme() {
|
||||
"asset" => {
|
||||
let state_snapshot = self.snapshot();
|
||||
if let Some(text) =
|
||||
tsc::get_asset(&specifier, &self.ts_server, &state_snapshot)
|
||||
.await
|
||||
.map_err(|_| LspError::internal_error())?
|
||||
{
|
||||
Some(text)
|
||||
let maybe_asset =
|
||||
{ self.assets.lock().unwrap().get(&specifier).cloned() };
|
||||
if let Some(maybe_asset) = maybe_asset {
|
||||
if let Some(asset) = maybe_asset {
|
||||
Some(asset.text)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
error!("Missing asset: {}", specifier);
|
||||
None
|
||||
let state_snapshot = self.snapshot();
|
||||
if let Some(asset) =
|
||||
tsc::get_asset(&specifier, &self.ts_server, &state_snapshot)
|
||||
.await
|
||||
.map_err(|_| LspError::internal_error())?
|
||||
{
|
||||
Some(asset.text)
|
||||
} else {
|
||||
error!("Missing asset: {}", specifier);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
|
@ -1150,59 +1177,6 @@ impl LanguageServer {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DocumentData {
|
||||
pub dependencies: Option<HashMap<String, analysis::Dependency>>,
|
||||
pub version: Option<i32>,
|
||||
specifier: ModuleSpecifier,
|
||||
}
|
||||
|
||||
impl DocumentData {
|
||||
pub fn new(
|
||||
specifier: ModuleSpecifier,
|
||||
version: i32,
|
||||
source: &str,
|
||||
maybe_import_map: Option<ImportMap>,
|
||||
) -> Self {
|
||||
let dependencies = if let Some((dependencies, _)) =
|
||||
analysis::analyze_dependencies(
|
||||
&specifier,
|
||||
source,
|
||||
&MediaType::from(&specifier),
|
||||
&maybe_import_map,
|
||||
) {
|
||||
Some(dependencies)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Self {
|
||||
dependencies,
|
||||
version: Some(version),
|
||||
specifier,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
version: i32,
|
||||
source: &str,
|
||||
maybe_import_map: &Option<ImportMap>,
|
||||
) {
|
||||
self.dependencies = if let Some((dependencies, _)) =
|
||||
analysis::analyze_dependencies(
|
||||
&self.specifier,
|
||||
source,
|
||||
&MediaType::from(&self.specifier),
|
||||
maybe_import_map,
|
||||
) {
|
||||
Some(dependencies)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.version = Some(version)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -1211,6 +1185,7 @@ mod tests {
|
|||
use lspower::LspService;
|
||||
use std::fs;
|
||||
use std::task::Poll;
|
||||
use std::time::Instant;
|
||||
use tower_test::mock::Spawn;
|
||||
|
||||
enum LspResponse {
|
||||
|
@ -1410,6 +1385,69 @@ mod tests {
|
|||
]);
|
||||
harness.run().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_hover_change_mbc() {
|
||||
let mut harness = LspTestHarness::new(vec![
|
||||
("initialize_request.json", LspResponse::RequestAny),
|
||||
("initialized_notification.json", LspResponse::None),
|
||||
("did_open_notification_mbc.json", LspResponse::None),
|
||||
("did_change_notification_mbc.json", LspResponse::None),
|
||||
(
|
||||
"hover_request_mbc.json",
|
||||
LspResponse::Request(
|
||||
2,
|
||||
json!({
|
||||
"contents": [
|
||||
{
|
||||
"language": "typescript",
|
||||
"value": "const b: \"😃\"",
|
||||
},
|
||||
"",
|
||||
],
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 2,
|
||||
"character": 13,
|
||||
},
|
||||
"end": {
|
||||
"line": 2,
|
||||
"character": 14,
|
||||
},
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
(
|
||||
"shutdown_request.json",
|
||||
LspResponse::Request(3, json!(null)),
|
||||
),
|
||||
("exit_notification.json", LspResponse::None),
|
||||
]);
|
||||
harness.run().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_large_doc_change() {
|
||||
let mut harness = LspTestHarness::new(vec![
|
||||
("initialize_request.json", LspResponse::RequestAny),
|
||||
("initialized_notification.json", LspResponse::None),
|
||||
("did_open_notification_large.json", LspResponse::None),
|
||||
("did_change_notification_large.json", LspResponse::None),
|
||||
(
|
||||
"shutdown_request.json",
|
||||
LspResponse::Request(3, json!(null)),
|
||||
),
|
||||
("exit_notification.json", LspResponse::None),
|
||||
]);
|
||||
let time = Instant::now();
|
||||
harness.run().await;
|
||||
assert!(
|
||||
time.elapsed().as_millis() <= 10000,
|
||||
"the execution time exceeded 10000ms"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_rename() {
|
||||
let mut harness = LspTestHarness::new(vec![
|
||||
|
|
|
@ -1,121 +0,0 @@
|
|||
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
use deno_core::error::AnyError;
|
||||
use deno_core::ModuleSpecifier;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
|
||||
pub struct FileId(pub u32);
|
||||
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Debug)]
|
||||
pub enum ChangeKind {
|
||||
Create,
|
||||
Modify,
|
||||
Delete,
|
||||
}
|
||||
|
||||
pub struct ChangedFile {
|
||||
pub change_kind: ChangeKind,
|
||||
pub file_id: FileId,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct SpecifierInterner {
|
||||
map: HashMap<ModuleSpecifier, FileId>,
|
||||
vec: Vec<ModuleSpecifier>,
|
||||
}
|
||||
|
||||
impl SpecifierInterner {
|
||||
pub fn get(&self, specifier: &ModuleSpecifier) -> Option<FileId> {
|
||||
self.map.get(specifier).copied()
|
||||
}
|
||||
|
||||
pub fn intern(&mut self, specifier: ModuleSpecifier) -> FileId {
|
||||
if let Some(id) = self.get(&specifier) {
|
||||
return id;
|
||||
}
|
||||
let id = FileId(self.vec.len() as u32);
|
||||
self.map.insert(specifier.clone(), id);
|
||||
self.vec.push(specifier);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn lookup(&self, id: FileId) -> &ModuleSpecifier {
|
||||
&self.vec[id.0 as usize]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MemoryCache {
|
||||
data: Vec<Option<Vec<u8>>>,
|
||||
interner: SpecifierInterner,
|
||||
changes: Vec<ChangedFile>,
|
||||
}
|
||||
|
||||
impl MemoryCache {
|
||||
fn alloc_file_id(&mut self, specifier: ModuleSpecifier) -> FileId {
|
||||
let file_id = self.interner.intern(specifier);
|
||||
let idx = file_id.0 as usize;
|
||||
let len = self.data.len().max(idx + 1);
|
||||
self.data.resize_with(len, || None);
|
||||
file_id
|
||||
}
|
||||
|
||||
fn get(&self, file_id: FileId) -> &Option<Vec<u8>> {
|
||||
&self.data[file_id.0 as usize]
|
||||
}
|
||||
|
||||
pub fn get_contents(&self, file_id: FileId) -> Result<String, AnyError> {
|
||||
String::from_utf8(self.get(file_id).as_deref().unwrap().to_vec())
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_mut(&mut self, file_id: FileId) -> &mut Option<Vec<u8>> {
|
||||
&mut self.data[file_id.0 as usize]
|
||||
}
|
||||
|
||||
pub fn get_specifier(&self, file_id: FileId) -> &ModuleSpecifier {
|
||||
self.interner.lookup(file_id)
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.data.len()
|
||||
}
|
||||
|
||||
pub fn lookup(&self, specifier: &ModuleSpecifier) -> Option<FileId> {
|
||||
self
|
||||
.interner
|
||||
.get(specifier)
|
||||
.filter(|&it| self.get(it).is_some())
|
||||
}
|
||||
|
||||
pub fn set_contents(
|
||||
&mut self,
|
||||
specifier: ModuleSpecifier,
|
||||
contents: Option<Vec<u8>>,
|
||||
) {
|
||||
let file_id = self.alloc_file_id(specifier);
|
||||
let change_kind = match (self.get(file_id), &contents) {
|
||||
(None, None) => return,
|
||||
(None, Some(_)) => ChangeKind::Create,
|
||||
(Some(_), None) => ChangeKind::Delete,
|
||||
(Some(old), Some(new)) if old == new => return,
|
||||
(Some(_), Some(_)) => ChangeKind::Modify,
|
||||
};
|
||||
|
||||
*self.get_mut(file_id) = contents;
|
||||
self.changes.push(ChangedFile {
|
||||
file_id,
|
||||
change_kind,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for MemoryCache {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.debug_struct("MemoryCache")
|
||||
.field("no_files", &self.data.len())
|
||||
.finish()
|
||||
}
|
||||
}
|
|
@ -7,8 +7,8 @@ mod analysis;
|
|||
mod capabilities;
|
||||
mod config;
|
||||
mod diagnostics;
|
||||
mod documents;
|
||||
mod language_server;
|
||||
mod memory_cache;
|
||||
mod sources;
|
||||
mod text;
|
||||
mod tsc;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
use super::analysis;
|
||||
use super::text;
|
||||
use super::text::LineIndex;
|
||||
|
||||
use crate::file_fetcher::get_source_from_bytes;
|
||||
use crate::file_fetcher::map_content_type;
|
||||
|
@ -43,6 +43,7 @@ pub async fn cache(
|
|||
#[derive(Debug, Clone, Default)]
|
||||
struct Metadata {
|
||||
dependencies: Option<HashMap<String, analysis::Dependency>>,
|
||||
line_index: LineIndex,
|
||||
maybe_types: Option<analysis::ResolvedDependency>,
|
||||
media_type: MediaType,
|
||||
source: String,
|
||||
|
@ -75,19 +76,26 @@ impl Sources {
|
|||
false
|
||||
}
|
||||
|
||||
pub fn get_length(&mut self, specifier: &ModuleSpecifier) -> Option<usize> {
|
||||
/// Provides the length of the source content, calculated in a way that should
|
||||
/// match the behavior of JavaScript, where strings are stored effectively as
|
||||
/// `&[u16]` and when counting "chars" we need to represent the string as a
|
||||
/// UTF-16 string in Rust.
|
||||
pub fn get_length_utf16(
|
||||
&mut self,
|
||||
specifier: &ModuleSpecifier,
|
||||
) -> Option<usize> {
|
||||
let specifier = self.resolve_specifier(specifier)?;
|
||||
let metadata = self.get_metadata(&specifier)?;
|
||||
Some(metadata.source.chars().count())
|
||||
Some(metadata.source.encode_utf16().count())
|
||||
}
|
||||
|
||||
pub fn get_line_index(
|
||||
&mut self,
|
||||
specifier: &ModuleSpecifier,
|
||||
) -> Option<Vec<u32>> {
|
||||
) -> Option<LineIndex> {
|
||||
let specifier = self.resolve_specifier(specifier)?;
|
||||
let metadata = self.get_metadata(&specifier)?;
|
||||
Some(text::index_lines(&metadata.source))
|
||||
Some(metadata.line_index)
|
||||
}
|
||||
|
||||
pub fn get_media_type(
|
||||
|
@ -127,8 +135,10 @@ impl Sources {
|
|||
} else {
|
||||
None
|
||||
};
|
||||
let line_index = LineIndex::new(&source);
|
||||
let metadata = Metadata {
|
||||
dependencies,
|
||||
line_index,
|
||||
maybe_types,
|
||||
media_type,
|
||||
source,
|
||||
|
@ -169,8 +179,10 @@ impl Sources {
|
|||
} else {
|
||||
None
|
||||
};
|
||||
let line_index = LineIndex::new(&source);
|
||||
let metadata = Metadata {
|
||||
dependencies,
|
||||
line_index,
|
||||
maybe_types,
|
||||
media_type,
|
||||
source,
|
||||
|
@ -388,7 +400,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_sources_get_length() {
|
||||
fn test_sources_get_length_utf16() {
|
||||
let (mut sources, _) = setup();
|
||||
let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||
let tests = c.join("tests");
|
||||
|
@ -396,7 +408,7 @@ mod tests {
|
|||
&tests.join("001_hello.js").to_string_lossy(),
|
||||
)
|
||||
.unwrap();
|
||||
let actual = sources.get_length(&specifier);
|
||||
let actual = sources.get_length_utf16(&specifier);
|
||||
assert!(actual.is_some());
|
||||
let actual = actual.unwrap();
|
||||
assert_eq!(actual, 28);
|
||||
|
|
655
cli/lsp/text.rs
655
cli/lsp/text.rs
|
@ -1,123 +1,233 @@
|
|||
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
use deno_core::error::custom_error;
|
||||
use deno_core::error::AnyError;
|
||||
use deno_core::serde_json::json;
|
||||
use deno_core::serde_json::Value;
|
||||
use dissimilar::diff;
|
||||
use dissimilar::Chunk;
|
||||
use lspower::jsonrpc;
|
||||
use lspower::lsp_types;
|
||||
use lspower::lsp_types::TextEdit;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Bound;
|
||||
use std::ops::Range;
|
||||
use std::ops::RangeBounds;
|
||||
use text_size::TextRange;
|
||||
use text_size::TextSize;
|
||||
|
||||
// TODO(@kitson) in general all of these text handling routines don't handle
|
||||
// JavaScript encoding in the same way and likely cause issues when trying to
|
||||
// arbitrate between chars and Unicode graphemes. There be dragons.
|
||||
fn partition_point<T, P>(slice: &[T], mut predicate: P) -> usize
|
||||
where
|
||||
P: FnMut(&T) -> bool,
|
||||
{
|
||||
let mut left = 0;
|
||||
let mut right = slice.len();
|
||||
|
||||
/// Generate a character position for the start of each line. For example:
|
||||
///
|
||||
/// ```rust
|
||||
/// let actual = index_lines("a\nb\n");
|
||||
/// assert_eq!(actual, vec![0, 2, 4]);
|
||||
/// ```
|
||||
///
|
||||
pub fn index_lines(text: &str) -> Vec<u32> {
|
||||
let mut indexes = vec![0_u32];
|
||||
for (i, c) in text.chars().enumerate() {
|
||||
if c == '\n' {
|
||||
indexes.push((i + 1) as u32);
|
||||
}
|
||||
}
|
||||
indexes
|
||||
}
|
||||
|
||||
enum IndexValid {
|
||||
All,
|
||||
UpTo(u32),
|
||||
}
|
||||
|
||||
impl IndexValid {
|
||||
fn covers(&self, line: u32) -> bool {
|
||||
match *self {
|
||||
IndexValid::UpTo(to) => to > line,
|
||||
IndexValid::All => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_range(line_index: &[u32], range: lsp_types::Range) -> Range<usize> {
|
||||
let start =
|
||||
(line_index[range.start.line as usize] + range.start.character) as usize;
|
||||
let end =
|
||||
(line_index[range.end.line as usize] + range.end.character) as usize;
|
||||
Range { start, end }
|
||||
}
|
||||
|
||||
pub fn to_position(line_index: &[u32], char_pos: u32) -> lsp_types::Position {
|
||||
let mut line = 0_usize;
|
||||
let mut line_start = 0_u32;
|
||||
for (pos, v) in line_index.iter().enumerate() {
|
||||
if char_pos < *v {
|
||||
break;
|
||||
}
|
||||
line_start = *v;
|
||||
line = pos;
|
||||
}
|
||||
|
||||
lsp_types::Position {
|
||||
line: line as u32,
|
||||
character: char_pos - line_start,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_char_pos(line_index: &[u32], position: lsp_types::Position) -> u32 {
|
||||
if let Some(line_start) = line_index.get(position.line as usize) {
|
||||
line_start + position.character
|
||||
} else {
|
||||
0_u32
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a vector of document changes to the supplied string.
|
||||
pub fn apply_content_changes(
|
||||
content: &mut String,
|
||||
content_changes: Vec<lsp_types::TextDocumentContentChangeEvent>,
|
||||
) {
|
||||
let mut line_index = index_lines(&content);
|
||||
let mut index_valid = IndexValid::All;
|
||||
for change in content_changes {
|
||||
if let Some(range) = change.range {
|
||||
if !index_valid.covers(range.start.line) {
|
||||
line_index = index_lines(&content);
|
||||
}
|
||||
let range = to_range(&line_index, range);
|
||||
content.replace_range(range, &change.text);
|
||||
while left != right {
|
||||
let mid = left + (right - left) / 2;
|
||||
// SAFETY:
|
||||
// When left < right, left <= mid < right.
|
||||
// Therefore left always increases and right always decreases,
|
||||
// and either of them is selected.
|
||||
// In both cases left <= right is satisfied.
|
||||
// Therefore if left < right in a step,
|
||||
// left <= right is satisfied in the next step.
|
||||
// Therefore as long as left != right, 0 <= left < right <= len is satisfied
|
||||
// and if this case 0 <= mid < len is satisfied too.
|
||||
let value = unsafe { slice.get_unchecked(mid) };
|
||||
if predicate(value) {
|
||||
left = mid + 1;
|
||||
} else {
|
||||
*content = change.text;
|
||||
index_valid = IndexValid::UpTo(0);
|
||||
right = mid;
|
||||
}
|
||||
}
|
||||
|
||||
left
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Utf16Char {
|
||||
pub start: TextSize,
|
||||
pub end: TextSize,
|
||||
}
|
||||
|
||||
impl Utf16Char {
|
||||
fn len(&self) -> TextSize {
|
||||
self.end - self.start
|
||||
}
|
||||
|
||||
fn len_utf16(&self) -> usize {
|
||||
if self.len() == TextSize::from(4) {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Eq, PartialEq)]
|
||||
pub struct LineIndex {
|
||||
utf8_offsets: Vec<TextSize>,
|
||||
utf16_lines: HashMap<u32, Vec<Utf16Char>>,
|
||||
utf16_offsets: Vec<TextSize>,
|
||||
}
|
||||
|
||||
impl LineIndex {
|
||||
pub fn new(text: &str) -> LineIndex {
|
||||
let mut utf16_lines = HashMap::new();
|
||||
let mut utf16_chars = Vec::new();
|
||||
|
||||
let mut utf8_offsets = vec![0.into()];
|
||||
let mut utf16_offsets = vec![0.into()];
|
||||
let mut curr_row = 0.into();
|
||||
let mut curr_col = 0.into();
|
||||
let mut curr_offset_u16 = 0.into();
|
||||
let mut line = 0;
|
||||
for c in text.chars() {
|
||||
let c_len = TextSize::of(c);
|
||||
curr_row += c_len;
|
||||
curr_offset_u16 += TextSize::from(c.len_utf16() as u32);
|
||||
if c == '\n' {
|
||||
utf8_offsets.push(curr_row);
|
||||
utf16_offsets.push(curr_offset_u16);
|
||||
|
||||
if !utf16_chars.is_empty() {
|
||||
utf16_lines.insert(line, utf16_chars);
|
||||
utf16_chars = Vec::new();
|
||||
}
|
||||
|
||||
curr_col = 0.into();
|
||||
line += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if !c.is_ascii() {
|
||||
utf16_chars.push(Utf16Char {
|
||||
start: curr_col,
|
||||
end: curr_col + c_len,
|
||||
});
|
||||
}
|
||||
curr_col += c_len;
|
||||
}
|
||||
|
||||
if !utf16_chars.is_empty() {
|
||||
utf16_lines.insert(line, utf16_chars);
|
||||
}
|
||||
|
||||
LineIndex {
|
||||
utf8_offsets,
|
||||
utf16_lines,
|
||||
utf16_offsets,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a u16 based range to a u8 TextRange.
|
||||
pub fn get_text_range(
|
||||
&self,
|
||||
range: lsp_types::Range,
|
||||
) -> Result<TextRange, AnyError> {
|
||||
let start = self.offset(range.start)?;
|
||||
let end = self.offset(range.end)?;
|
||||
Ok(TextRange::new(start, end))
|
||||
}
|
||||
|
||||
/// Return a u8 offset based on a u16 position.
|
||||
pub fn offset(
|
||||
&self,
|
||||
position: lsp_types::Position,
|
||||
) -> Result<TextSize, AnyError> {
|
||||
let col = self.utf16_to_utf8_col(position.line, position.character);
|
||||
if let Some(line_offset) = self.utf8_offsets.get(position.line as usize) {
|
||||
Ok(line_offset + col)
|
||||
} else {
|
||||
Err(custom_error("OutOfRange", "The position is out of range."))
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an lsp Position into a tsc/TypeScript "position", which is really
|
||||
/// an u16 byte offset from the start of the string represented as an u32.
|
||||
pub fn offset_tsc(
|
||||
&self,
|
||||
position: lsp_types::Position,
|
||||
) -> jsonrpc::Result<u32> {
|
||||
self
|
||||
.offset_utf16(position)
|
||||
.map(|ts| ts.into())
|
||||
.map_err(|err| jsonrpc::Error::invalid_params(err.to_string()))
|
||||
}
|
||||
|
||||
fn offset_utf16(
|
||||
&self,
|
||||
position: lsp_types::Position,
|
||||
) -> Result<TextSize, AnyError> {
|
||||
if let Some(line_offset) = self.utf16_offsets.get(position.line as usize) {
|
||||
Ok(line_offset + TextSize::from(position.character))
|
||||
} else {
|
||||
Err(custom_error("OutOfRange", "The position is out of range."))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a u16 position based on a u16 offset, which TypeScript offsets are
|
||||
/// returned as u16.
|
||||
pub fn position_tsc(&self, offset: TextSize) -> lsp_types::Position {
|
||||
let line = partition_point(&self.utf16_offsets, |&it| it <= offset) - 1;
|
||||
let line_start_offset = self.utf16_offsets[line];
|
||||
let col = offset - line_start_offset;
|
||||
|
||||
lsp_types::Position {
|
||||
line: line as u32,
|
||||
character: col.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a u16 position based on a u8 offset.
|
||||
pub fn position_utf16(&self, offset: TextSize) -> lsp_types::Position {
|
||||
let line = partition_point(&self.utf8_offsets, |&it| it <= offset) - 1;
|
||||
let line_start_offset = self.utf8_offsets[line];
|
||||
let col = offset - line_start_offset;
|
||||
|
||||
lsp_types::Position {
|
||||
line: line as u32,
|
||||
character: col.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn utf16_to_utf8_col(&self, line: u32, mut col: u32) -> TextSize {
|
||||
if let Some(utf16_chars) = self.utf16_lines.get(&line) {
|
||||
for c in utf16_chars {
|
||||
if col > u32::from(c.start) {
|
||||
col += u32::from(c.len()) - c.len_utf16() as u32;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
col.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare two strings and return a vector of text edit records which are
|
||||
/// supported by the Language Server Protocol.
|
||||
pub fn get_edits(a: &str, b: &str) -> Vec<TextEdit> {
|
||||
if a == b {
|
||||
return vec![];
|
||||
}
|
||||
let chunks = diff(a, b);
|
||||
let mut text_edits = Vec::<TextEdit>::new();
|
||||
let line_index = index_lines(a);
|
||||
let line_index = LineIndex::new(a);
|
||||
let mut iter = chunks.iter().peekable();
|
||||
let mut a_pos = 0_u32;
|
||||
let mut a_pos = TextSize::from(0);
|
||||
loop {
|
||||
let chunk = iter.next();
|
||||
match chunk {
|
||||
None => break,
|
||||
Some(Chunk::Equal(e)) => {
|
||||
a_pos += e.chars().count() as u32;
|
||||
a_pos += TextSize::from(e.encode_utf16().count() as u32);
|
||||
}
|
||||
Some(Chunk::Delete(d)) => {
|
||||
let start = to_position(&line_index, a_pos);
|
||||
a_pos += d.chars().count() as u32;
|
||||
let end = to_position(&line_index, a_pos);
|
||||
let start = line_index.position_utf16(a_pos);
|
||||
a_pos += TextSize::from(d.encode_utf16().count() as u32);
|
||||
let end = line_index.position_utf16(a_pos);
|
||||
let range = lsp_types::Range { start, end };
|
||||
match iter.peek() {
|
||||
Some(Chunk::Insert(i)) => {
|
||||
|
@ -134,7 +244,7 @@ pub fn get_edits(a: &str, b: &str) -> Vec<TextEdit> {
|
|||
}
|
||||
}
|
||||
Some(Chunk::Insert(i)) => {
|
||||
let pos = to_position(&line_index, a_pos);
|
||||
let pos = line_index.position_utf16(a_pos);
|
||||
let range = lsp_types::Range {
|
||||
start: pos,
|
||||
end: pos,
|
||||
|
@ -153,6 +263,9 @@ pub fn get_edits(a: &str, b: &str) -> Vec<TextEdit> {
|
|||
/// Convert a difference between two strings into a change range used by the
|
||||
/// TypeScript Language Service.
|
||||
pub fn get_range_change(a: &str, b: &str) -> Value {
|
||||
if a == b {
|
||||
return json!(null);
|
||||
}
|
||||
let chunks = diff(a, b);
|
||||
let mut iter = chunks.iter().peekable();
|
||||
let mut started = false;
|
||||
|
@ -162,12 +275,12 @@ pub fn get_range_change(a: &str, b: &str) -> Value {
|
|||
let mut equal = 0;
|
||||
let mut a_pos = 0;
|
||||
loop {
|
||||
let chunk = iter.next();
|
||||
match chunk {
|
||||
let diff = iter.next();
|
||||
match diff {
|
||||
None => break,
|
||||
Some(Chunk::Equal(e)) => {
|
||||
a_pos += e.chars().count();
|
||||
equal += e.chars().count();
|
||||
a_pos += e.encode_utf16().count();
|
||||
equal += e.encode_utf16().count();
|
||||
}
|
||||
Some(Chunk::Delete(d)) => {
|
||||
if !started {
|
||||
|
@ -175,7 +288,7 @@ pub fn get_range_change(a: &str, b: &str) -> Value {
|
|||
started = true;
|
||||
equal = 0;
|
||||
}
|
||||
a_pos += d.chars().count();
|
||||
a_pos += d.encode_utf16().count();
|
||||
if started {
|
||||
end = a_pos;
|
||||
new_length += equal;
|
||||
|
@ -191,7 +304,7 @@ pub fn get_range_change(a: &str, b: &str) -> Value {
|
|||
} else {
|
||||
end += equal;
|
||||
}
|
||||
new_length += i.chars().count() + equal;
|
||||
new_length += i.encode_utf16().count() + equal;
|
||||
equal = 0;
|
||||
}
|
||||
}
|
||||
|
@ -215,7 +328,7 @@ pub fn slice(s: &str, range: impl RangeBounds<usize>) -> &str {
|
|||
let len = match range.end_bound() {
|
||||
Bound::Included(bound) => *bound + 1,
|
||||
Bound::Excluded(bound) => *bound,
|
||||
Bound::Unbounded => s.len(),
|
||||
Bound::Unbounded => s.encode_utf16().count(),
|
||||
} - start;
|
||||
substring(s, start, start + len)
|
||||
}
|
||||
|
@ -231,7 +344,7 @@ pub fn substring(s: &str, start: usize, end: usize) -> &str {
|
|||
break;
|
||||
}
|
||||
if let Some(c) = it.next() {
|
||||
char_pos += 1;
|
||||
char_pos += c.len_utf16();
|
||||
byte_start += c.len_utf8();
|
||||
} else {
|
||||
break;
|
||||
|
@ -244,7 +357,7 @@ pub fn substring(s: &str, start: usize, end: usize) -> &str {
|
|||
break;
|
||||
}
|
||||
if let Some(c) = it.next() {
|
||||
char_pos += 1;
|
||||
char_pos += c.len_utf16();
|
||||
byte_end += c.len_utf8();
|
||||
} else {
|
||||
break;
|
||||
|
@ -258,24 +371,194 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_apply_content_changes() {
|
||||
let mut content = "a\nb\nc\nd".to_string();
|
||||
let content_changes = vec![lsp_types::TextDocumentContentChangeEvent {
|
||||
range: Some(lsp_types::Range {
|
||||
start: lsp_types::Position {
|
||||
line: 1,
|
||||
character: 0,
|
||||
},
|
||||
end: lsp_types::Position {
|
||||
line: 1,
|
||||
character: 1,
|
||||
},
|
||||
}),
|
||||
range_length: Some(1),
|
||||
text: "e".to_string(),
|
||||
}];
|
||||
apply_content_changes(&mut content, content_changes);
|
||||
assert_eq!(content, "a\ne\nc\nd");
|
||||
fn test_line_index() {
|
||||
let text = "hello\nworld";
|
||||
let index = LineIndex::new(text);
|
||||
assert_eq!(
|
||||
index.position_utf16(0.into()),
|
||||
lsp_types::Position {
|
||||
line: 0,
|
||||
character: 0
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
index.position_utf16(1.into()),
|
||||
lsp_types::Position {
|
||||
line: 0,
|
||||
character: 1
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
index.position_utf16(5.into()),
|
||||
lsp_types::Position {
|
||||
line: 0,
|
||||
character: 5
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
index.position_utf16(6.into()),
|
||||
lsp_types::Position {
|
||||
line: 1,
|
||||
character: 0
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
index.position_utf16(7.into()),
|
||||
lsp_types::Position {
|
||||
line: 1,
|
||||
character: 1
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
index.position_utf16(8.into()),
|
||||
lsp_types::Position {
|
||||
line: 1,
|
||||
character: 2
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
index.position_utf16(10.into()),
|
||||
lsp_types::Position {
|
||||
line: 1,
|
||||
character: 4
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
index.position_utf16(11.into()),
|
||||
lsp_types::Position {
|
||||
line: 1,
|
||||
character: 5
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
index.position_utf16(12.into()),
|
||||
lsp_types::Position {
|
||||
line: 1,
|
||||
character: 6
|
||||
}
|
||||
);
|
||||
|
||||
let text = "\nhello\nworld";
|
||||
let index = LineIndex::new(text);
|
||||
assert_eq!(
|
||||
index.position_utf16(0.into()),
|
||||
lsp_types::Position {
|
||||
line: 0,
|
||||
character: 0
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
index.position_utf16(1.into()),
|
||||
lsp_types::Position {
|
||||
line: 1,
|
||||
character: 0
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
index.position_utf16(2.into()),
|
||||
lsp_types::Position {
|
||||
line: 1,
|
||||
character: 1
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
index.position_utf16(6.into()),
|
||||
lsp_types::Position {
|
||||
line: 1,
|
||||
character: 5
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
index.position_utf16(7.into()),
|
||||
lsp_types::Position {
|
||||
line: 2,
|
||||
character: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_char_len() {
|
||||
assert_eq!('メ'.len_utf8(), 3);
|
||||
assert_eq!('メ'.len_utf16(), 1);
|
||||
assert_eq!('编'.len_utf8(), 3);
|
||||
assert_eq!('编'.len_utf16(), 1);
|
||||
assert_eq!('🦕'.len_utf8(), 4);
|
||||
assert_eq!('🦕'.len_utf16(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_index() {
|
||||
let col_index = LineIndex::new(
|
||||
"
|
||||
const C: char = 'x';
|
||||
",
|
||||
);
|
||||
assert_eq!(col_index.utf16_lines.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_char() {
|
||||
let col_index = LineIndex::new(
|
||||
"
|
||||
const C: char = 'メ';
|
||||
",
|
||||
);
|
||||
|
||||
assert_eq!(col_index.utf16_lines.len(), 1);
|
||||
assert_eq!(col_index.utf16_lines[&1].len(), 1);
|
||||
assert_eq!(
|
||||
col_index.utf16_lines[&1][0],
|
||||
Utf16Char {
|
||||
start: 17.into(),
|
||||
end: 20.into()
|
||||
}
|
||||
);
|
||||
|
||||
// UTF-16 to UTF-8, no changes
|
||||
assert_eq!(col_index.utf16_to_utf8_col(1, 15), TextSize::from(15));
|
||||
|
||||
// UTF-16 to UTF-8
|
||||
assert_eq!(col_index.utf16_to_utf8_col(1, 19), TextSize::from(21));
|
||||
|
||||
let col_index = LineIndex::new("a𐐏b");
|
||||
assert_eq!(col_index.utf16_to_utf8_col(0, 3), TextSize::from(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string() {
|
||||
let col_index = LineIndex::new(
|
||||
"
|
||||
const C: char = \"メ メ\";
|
||||
",
|
||||
);
|
||||
|
||||
assert_eq!(col_index.utf16_lines.len(), 1);
|
||||
assert_eq!(col_index.utf16_lines[&1].len(), 2);
|
||||
assert_eq!(
|
||||
col_index.utf16_lines[&1][0],
|
||||
Utf16Char {
|
||||
start: 17.into(),
|
||||
end: 20.into()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
col_index.utf16_lines[&1][1],
|
||||
Utf16Char {
|
||||
start: 21.into(),
|
||||
end: 24.into()
|
||||
}
|
||||
);
|
||||
|
||||
// UTF-16 to UTF-8
|
||||
assert_eq!(col_index.utf16_to_utf8_col(1, 15), TextSize::from(15));
|
||||
|
||||
// メ UTF-8: 0xE3 0x83 0xA1, UTF-16: 0x30E1
|
||||
assert_eq!(col_index.utf16_to_utf8_col(1, 17), TextSize::from(17)); // first メ at 17..20
|
||||
assert_eq!(col_index.utf16_to_utf8_col(1, 18), TextSize::from(20)); // space
|
||||
assert_eq!(col_index.utf16_to_utf8_col(1, 19), TextSize::from(21)); // second メ at 21..24
|
||||
|
||||
assert_eq!(col_index.utf16_to_utf8_col(2, 15), TextSize::from(15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -318,6 +601,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_get_range_change() {
|
||||
let a = "abcdefg";
|
||||
let b = "abcdefg";
|
||||
let actual = get_range_change(a, b);
|
||||
assert_eq!(actual, json!(null));
|
||||
|
||||
let a = "abcdefg";
|
||||
let b = "abedcfg";
|
||||
let actual = get_range_change(a, b);
|
||||
|
@ -401,108 +689,56 @@ mod tests {
|
|||
"newLength": 3
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_index_lines() {
|
||||
let actual = index_lines("a\nb\r\nc");
|
||||
assert_eq!(actual, vec![0, 2, 5]);
|
||||
}
|
||||
let a = "hello 🦕!";
|
||||
let b = "hello deno!";
|
||||
let actual = get_range_change(a, b);
|
||||
assert_eq!(
|
||||
actual,
|
||||
json!({
|
||||
"span": {
|
||||
"start": 6,
|
||||
"length": 2,
|
||||
},
|
||||
"newLength": 4
|
||||
})
|
||||
);
|
||||
|
||||
#[test]
|
||||
fn test_to_position() {
|
||||
let line_index = index_lines("a\nb\r\nc\n");
|
||||
let a = "hello deno!";
|
||||
let b = "hello deno🦕!";
|
||||
let actual = get_range_change(a, b);
|
||||
assert_eq!(
|
||||
to_position(&line_index, 6),
|
||||
lsp_types::Position {
|
||||
line: 2,
|
||||
character: 1,
|
||||
}
|
||||
actual,
|
||||
json!({
|
||||
"span": {
|
||||
"start": 10,
|
||||
"length": 0,
|
||||
},
|
||||
"newLength": 2
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
to_position(&line_index, 0),
|
||||
lsp_types::Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
to_position(&line_index, 3),
|
||||
lsp_types::Position {
|
||||
line: 1,
|
||||
character: 1,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_position_mbc() {
|
||||
let line_index = index_lines("y̆\n😱🦕\n🤯\n");
|
||||
assert_eq!(
|
||||
to_position(&line_index, 0),
|
||||
lsp_types::Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
to_position(&line_index, 2),
|
||||
lsp_types::Position {
|
||||
line: 0,
|
||||
character: 2,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
to_position(&line_index, 3),
|
||||
lsp_types::Position {
|
||||
line: 1,
|
||||
character: 0,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
to_position(&line_index, 4),
|
||||
lsp_types::Position {
|
||||
line: 1,
|
||||
character: 1,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
to_position(&line_index, 5),
|
||||
lsp_types::Position {
|
||||
line: 1,
|
||||
character: 2,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
to_position(&line_index, 6),
|
||||
lsp_types::Position {
|
||||
line: 2,
|
||||
character: 0,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
to_position(&line_index, 7),
|
||||
lsp_types::Position {
|
||||
line: 2,
|
||||
character: 1,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
to_position(&line_index, 8),
|
||||
lsp_types::Position {
|
||||
line: 3,
|
||||
character: 0,
|
||||
}
|
||||
);
|
||||
// TODO(@kitsonk): https://github.com/dtolnay/dissimilar/issues/5
|
||||
// let a = r#" 🦕🇺🇸👍 "#;
|
||||
// let b = r#" 🇺🇸👍 "#;
|
||||
// let actual = get_range_change(a, b);
|
||||
// assert_eq!(
|
||||
// actual,
|
||||
// json!({
|
||||
// "span": {
|
||||
// "start": 1,
|
||||
// "length": 2,
|
||||
// },
|
||||
// "newLength": 0
|
||||
// })
|
||||
// );
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_substring() {
|
||||
assert_eq!(substring("Deno", 1, 3), "en");
|
||||
assert_eq!(substring("y̆y̆", 2, 4), "y̆");
|
||||
// this doesn't work like JavaScript, as 🦕 is treated as a single char in
|
||||
// Rust, but as two chars in JavaScript.
|
||||
// assert_eq!(substring("🦕🦕", 2, 4), "🦕");
|
||||
assert_eq!(substring("🦕🦕", 2, 4), "🦕");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -511,5 +747,6 @@ mod tests {
|
|||
assert_eq!(slice("Deno", 1..=3), "eno");
|
||||
assert_eq!(slice("Deno Land", 1..), "eno Land");
|
||||
assert_eq!(slice("Deno", ..3), "Den");
|
||||
assert_eq!(slice("Hello 🦕", 6..8), "🦕");
|
||||
}
|
||||
}
|
||||
|
|
341
cli/lsp/tsc.rs
341
cli/lsp/tsc.rs
|
@ -3,6 +3,7 @@
|
|||
use super::analysis::ResolvedDependency;
|
||||
use super::language_server::StateSnapshot;
|
||||
use super::text;
|
||||
use super::text::LineIndex;
|
||||
use super::utils;
|
||||
|
||||
use crate::media_type::MediaType;
|
||||
|
@ -32,6 +33,7 @@ use regex::Regex;
|
|||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::thread;
|
||||
use text_size::TextSize;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
|
@ -80,6 +82,14 @@ impl TsServer {
|
|||
}
|
||||
}
|
||||
|
||||
/// An lsp representation of an asset in memory, that has either been retrieved
|
||||
/// from static assets built into Rust, or static assets built into tsc.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AssetDocument {
|
||||
pub text: String,
|
||||
pub line_index: LineIndex,
|
||||
}
|
||||
|
||||
/// Optionally returns an internal asset, first checking for any static assets
|
||||
/// in Rust, then checking any previously retrieved static assets from the
|
||||
/// isolate, and then finally, the tsc isolate itself.
|
||||
|
@ -87,28 +97,41 @@ pub async fn get_asset(
|
|||
specifier: &ModuleSpecifier,
|
||||
ts_server: &TsServer,
|
||||
state_snapshot: &StateSnapshot,
|
||||
) -> Result<Option<String>, AnyError> {
|
||||
) -> Result<Option<AssetDocument>, AnyError> {
|
||||
let specifier_str = specifier.to_string().replace("asset:///", "");
|
||||
if let Some(asset_text) = tsc::get_asset(&specifier_str) {
|
||||
Ok(Some(asset_text.to_string()))
|
||||
if let Some(text) = tsc::get_asset(&specifier_str) {
|
||||
let maybe_asset = Some(AssetDocument {
|
||||
line_index: LineIndex::new(text),
|
||||
text: text.to_string(),
|
||||
});
|
||||
state_snapshot
|
||||
.assets
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(specifier.clone(), maybe_asset.clone());
|
||||
Ok(maybe_asset)
|
||||
} else {
|
||||
{
|
||||
let assets = state_snapshot.assets.lock().unwrap();
|
||||
if let Some(asset) = assets.get(specifier) {
|
||||
return Ok(asset.clone());
|
||||
}
|
||||
}
|
||||
let asset: Option<String> = serde_json::from_value(
|
||||
ts_server
|
||||
.request(
|
||||
state_snapshot.clone(),
|
||||
RequestMethod::GetAsset(specifier.clone()),
|
||||
)
|
||||
.await?,
|
||||
)?;
|
||||
let mut assets = state_snapshot.assets.lock().unwrap();
|
||||
assets.insert(specifier.clone(), asset.clone());
|
||||
Ok(asset)
|
||||
let res = ts_server
|
||||
.request(
|
||||
state_snapshot.clone(),
|
||||
RequestMethod::GetAsset(specifier.clone()),
|
||||
)
|
||||
.await?;
|
||||
let maybe_text: Option<String> = serde_json::from_value(res)?;
|
||||
let maybe_asset = if let Some(text) = maybe_text {
|
||||
Some(AssetDocument {
|
||||
line_index: LineIndex::new(&text),
|
||||
text,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
state_snapshot
|
||||
.assets
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(specifier.clone(), maybe_asset.clone());
|
||||
Ok(maybe_asset)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -342,10 +365,10 @@ pub struct TextSpan {
|
|||
}
|
||||
|
||||
impl TextSpan {
|
||||
pub fn to_range(&self, line_index: &[u32]) -> lsp_types::Range {
|
||||
pub fn to_range(&self, line_index: &LineIndex) -> lsp_types::Range {
|
||||
lsp_types::Range {
|
||||
start: text::to_position(line_index, self.start),
|
||||
end: text::to_position(line_index, self.start + self.length),
|
||||
start: line_index.position_tsc(self.start.into()),
|
||||
end: line_index.position_tsc(TextSize::from(self.start + self.length)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -376,7 +399,7 @@ pub struct QuickInfo {
|
|||
}
|
||||
|
||||
impl QuickInfo {
|
||||
pub fn to_hover(&self, line_index: &[u32]) -> lsp_types::Hover {
|
||||
pub fn to_hover(&self, line_index: &LineIndex) -> lsp_types::Hover {
|
||||
let mut contents = Vec::<lsp_types::MarkedString>::new();
|
||||
if let Some(display_string) =
|
||||
display_parts_to_string(self.display_parts.clone())
|
||||
|
@ -425,12 +448,12 @@ pub struct DocumentSpan {
|
|||
impl DocumentSpan {
|
||||
pub async fn to_link<F, Fut>(
|
||||
&self,
|
||||
line_index: &[u32],
|
||||
line_index: &LineIndex,
|
||||
index_provider: F,
|
||||
) -> Option<lsp_types::LocationLink>
|
||||
where
|
||||
F: Fn(ModuleSpecifier) -> Fut,
|
||||
Fut: Future<Output = Result<Vec<u32>, AnyError>>,
|
||||
Fut: Future<Output = Result<LineIndex, AnyError>>,
|
||||
{
|
||||
let target_specifier =
|
||||
ModuleSpecifier::resolve_url(&self.file_name).unwrap();
|
||||
|
@ -486,15 +509,16 @@ pub struct RenameLocations {
|
|||
}
|
||||
|
||||
impl RenameLocations {
|
||||
pub async fn into_workspace_edit<F, Fut>(
|
||||
pub async fn into_workspace_edit<F, Fut, V>(
|
||||
self,
|
||||
snapshot: StateSnapshot,
|
||||
index_provider: F,
|
||||
new_name: &str,
|
||||
index_provider: F,
|
||||
version_provider: V,
|
||||
) -> Result<lsp_types::WorkspaceEdit, AnyError>
|
||||
where
|
||||
F: Fn(ModuleSpecifier) -> Fut,
|
||||
Fut: Future<Output = Result<Vec<u32>, AnyError>>,
|
||||
Fut: Future<Output = Result<LineIndex, AnyError>>,
|
||||
V: Fn(ModuleSpecifier) -> Option<i32>,
|
||||
{
|
||||
let mut text_document_edit_map: HashMap<Url, lsp_types::TextDocumentEdit> =
|
||||
HashMap::new();
|
||||
|
@ -510,10 +534,7 @@ impl RenameLocations {
|
|||
lsp_types::TextDocumentEdit {
|
||||
text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
|
||||
uri: uri.clone(),
|
||||
version: snapshot
|
||||
.doc_data
|
||||
.get(&specifier)
|
||||
.map_or_else(|| None, |data| data.version),
|
||||
version: version_provider(specifier.clone()),
|
||||
},
|
||||
edits: Vec::<
|
||||
lsp_types::OneOf<
|
||||
|
@ -592,12 +613,12 @@ pub struct DefinitionInfoAndBoundSpan {
|
|||
impl DefinitionInfoAndBoundSpan {
|
||||
pub async fn to_definition<F, Fut>(
|
||||
&self,
|
||||
line_index: &[u32],
|
||||
line_index: &LineIndex,
|
||||
index_provider: F,
|
||||
) -> Option<lsp_types::GotoDefinitionResponse>
|
||||
where
|
||||
F: Fn(ModuleSpecifier) -> Fut + Clone,
|
||||
Fut: Future<Output = Result<Vec<u32>, AnyError>>,
|
||||
Fut: Future<Output = Result<LineIndex, AnyError>>,
|
||||
{
|
||||
if let Some(definitions) = &self.definitions {
|
||||
let mut location_links = Vec::<lsp_types::LocationLink>::new();
|
||||
|
@ -627,7 +648,7 @@ pub struct DocumentHighlights {
|
|||
impl DocumentHighlights {
|
||||
pub fn to_highlight(
|
||||
&self,
|
||||
line_index: &[u32],
|
||||
line_index: &LineIndex,
|
||||
) -> Vec<lsp_types::DocumentHighlight> {
|
||||
self
|
||||
.highlight_spans
|
||||
|
@ -656,7 +677,7 @@ pub struct ReferenceEntry {
|
|||
}
|
||||
|
||||
impl ReferenceEntry {
|
||||
pub fn to_location(&self, line_index: &[u32]) -> lsp_types::Location {
|
||||
pub fn to_location(&self, line_index: &LineIndex) -> lsp_types::Location {
|
||||
let uri =
|
||||
utils::normalize_file_name(&self.document_span.file_name).unwrap();
|
||||
lsp_types::Location {
|
||||
|
@ -676,7 +697,7 @@ pub struct CompletionInfo {
|
|||
impl CompletionInfo {
|
||||
pub fn into_completion_response(
|
||||
self,
|
||||
line_index: &[u32],
|
||||
line_index: &LineIndex,
|
||||
) -> lsp_types::CompletionResponse {
|
||||
let items = self
|
||||
.entries
|
||||
|
@ -704,7 +725,7 @@ pub struct CompletionEntry {
|
|||
impl CompletionEntry {
|
||||
pub fn into_completion_item(
|
||||
self,
|
||||
line_index: &[u32],
|
||||
line_index: &LineIndex,
|
||||
) -> lsp_types::CompletionItem {
|
||||
let mut item = lsp_types::CompletionItem {
|
||||
label: self.name,
|
||||
|
@ -801,11 +822,13 @@ fn cache_snapshot(
|
|||
.contains_key(&(specifier.clone().into(), version.clone().into()))
|
||||
{
|
||||
let s = ModuleSpecifier::resolve_url(&specifier)?;
|
||||
let content = {
|
||||
let file_cache = state.state_snapshot.file_cache.lock().unwrap();
|
||||
let file_id = file_cache.lookup(&s).unwrap();
|
||||
file_cache.get_contents(file_id)?
|
||||
};
|
||||
let content = state
|
||||
.state_snapshot
|
||||
.documents
|
||||
.lock()
|
||||
.unwrap()
|
||||
.content(&s)?
|
||||
.unwrap();
|
||||
state
|
||||
.snapshots
|
||||
.insert((specifier.into(), version.into()), content);
|
||||
|
@ -873,7 +896,7 @@ fn get_change_range(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
|||
"start": 0,
|
||||
"length": v.old_length,
|
||||
},
|
||||
"newLength": current.chars().count(),
|
||||
"newLength": current.encode_utf16().count(),
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
|
@ -890,16 +913,22 @@ fn get_change_range(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
|||
fn get_length(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
||||
let v: SourceSnapshotArgs = serde_json::from_value(args)?;
|
||||
let specifier = ModuleSpecifier::resolve_url(&v.specifier)?;
|
||||
if state.state_snapshot.doc_data.contains_key(&specifier) {
|
||||
if state
|
||||
.state_snapshot
|
||||
.documents
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains(&specifier)
|
||||
{
|
||||
cache_snapshot(state, v.specifier.clone(), v.version.clone())?;
|
||||
let content = state
|
||||
.snapshots
|
||||
.get(&(v.specifier.into(), v.version.into()))
|
||||
.unwrap();
|
||||
Ok(json!(content.chars().count()))
|
||||
Ok(json!(content.encode_utf16().count()))
|
||||
} else {
|
||||
let mut sources = state.state_snapshot.sources.lock().unwrap();
|
||||
Ok(json!(sources.get_length(&specifier).unwrap()))
|
||||
Ok(json!(sources.get_length_utf16(&specifier).unwrap()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -915,7 +944,13 @@ struct GetTextArgs {
|
|||
fn get_text(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
||||
let v: GetTextArgs = serde_json::from_value(args)?;
|
||||
let specifier = ModuleSpecifier::resolve_url(&v.specifier)?;
|
||||
let content = if state.state_snapshot.doc_data.contains_key(&specifier) {
|
||||
let content = if state
|
||||
.state_snapshot
|
||||
.documents
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains(&specifier)
|
||||
{
|
||||
cache_snapshot(state, v.specifier.clone(), v.version.clone())?;
|
||||
state
|
||||
.snapshots
|
||||
|
@ -939,8 +974,9 @@ fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
|||
return Err(custom_error("Deadlock", "deadlock locking sources"));
|
||||
};
|
||||
|
||||
if let Some(doc_data) = state.state_snapshot.doc_data.get(&referrer) {
|
||||
if let Some(dependencies) = &doc_data.dependencies {
|
||||
let documents = state.state_snapshot.documents.lock().unwrap();
|
||||
if documents.contains(&referrer) {
|
||||
if let Some(dependencies) = documents.dependencies(&referrer) {
|
||||
for specifier in &v.specifiers {
|
||||
if specifier.starts_with("asset:///") {
|
||||
resolved.push(Some((
|
||||
|
@ -959,10 +995,7 @@ fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
|||
if let ResolvedDependency::Resolved(resolved_specifier) =
|
||||
resolved_import
|
||||
{
|
||||
if state
|
||||
.state_snapshot
|
||||
.doc_data
|
||||
.contains_key(&resolved_specifier)
|
||||
if documents.contains(&resolved_specifier)
|
||||
|| sources.contains(&resolved_specifier)
|
||||
{
|
||||
let media_type = if let Some(media_type) =
|
||||
|
@ -1001,7 +1034,10 @@ fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
|||
} else {
|
||||
return Err(custom_error(
|
||||
"NotFound",
|
||||
"the referring specifier is unexpectedly missing",
|
||||
format!(
|
||||
"the referring ({}) specifier is unexpectedly missing",
|
||||
referrer
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -1014,8 +1050,8 @@ fn respond(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
|||
}
|
||||
|
||||
fn script_names(state: &mut State, _args: Value) -> Result<Value, AnyError> {
|
||||
let script_names: Vec<&ModuleSpecifier> =
|
||||
state.state_snapshot.doc_data.keys().collect();
|
||||
let documents = state.state_snapshot.documents.lock().unwrap();
|
||||
let script_names = documents.open_specifiers();
|
||||
Ok(json!(script_names))
|
||||
}
|
||||
|
||||
|
@ -1028,11 +1064,14 @@ struct ScriptVersionArgs {
|
|||
fn script_version(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
||||
let v: ScriptVersionArgs = serde_json::from_value(args)?;
|
||||
let specifier = ModuleSpecifier::resolve_url(&v.specifier)?;
|
||||
let maybe_doc_data = state.state_snapshot.doc_data.get(&specifier);
|
||||
if let Some(doc_data) = maybe_doc_data {
|
||||
if let Some(version) = doc_data.version {
|
||||
return Ok(json!(version.to_string()));
|
||||
}
|
||||
if let Some(version) = state
|
||||
.state_snapshot
|
||||
.documents
|
||||
.lock()
|
||||
.unwrap()
|
||||
.version(&specifier)
|
||||
{
|
||||
return Ok(json!(version.to_string()));
|
||||
} else {
|
||||
let mut sources = state.state_snapshot.sources.lock().unwrap();
|
||||
if let Some(version) = sources.get_script_version(&specifier) {
|
||||
|
@ -1153,13 +1192,14 @@ pub struct UserPreferences {
|
|||
}
|
||||
|
||||
/// Methods that are supported by the Language Service in the compiler isolate.
|
||||
#[derive(Debug)]
|
||||
pub enum RequestMethod {
|
||||
/// Configure the compilation settings for the server.
|
||||
Configure(TsConfig),
|
||||
/// Retrieve the text of an assets that exists in memory in the isolate.
|
||||
GetAsset(ModuleSpecifier),
|
||||
/// Return diagnostics for given file.
|
||||
GetDiagnostics(ModuleSpecifier),
|
||||
GetDiagnostics(Vec<ModuleSpecifier>),
|
||||
/// Return quick info at position (hover information).
|
||||
GetQuickInfo((ModuleSpecifier, u32)),
|
||||
/// Return document highlights at position.
|
||||
|
@ -1189,10 +1229,10 @@ impl RequestMethod {
|
|||
"method": "getAsset",
|
||||
"specifier": specifier,
|
||||
}),
|
||||
RequestMethod::GetDiagnostics(specifier) => json!({
|
||||
RequestMethod::GetDiagnostics(specifiers) => json!({
|
||||
"id": id,
|
||||
"method": "getDiagnostics",
|
||||
"specifier": specifier,
|
||||
"specifiers": specifiers,
|
||||
}),
|
||||
RequestMethod::GetQuickInfo((specifier, position)) => json!({
|
||||
"id": id,
|
||||
|
@ -1294,30 +1334,21 @@ pub fn request(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::memory_cache::MemoryCache;
|
||||
use super::*;
|
||||
use crate::lsp::language_server::DocumentData;
|
||||
use std::collections::HashMap;
|
||||
use crate::lsp::documents::DocumentCache;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
fn mock_state_snapshot(sources: Vec<(&str, &str, i32)>) -> StateSnapshot {
|
||||
let mut doc_data = HashMap::new();
|
||||
let mut file_cache = MemoryCache::default();
|
||||
let mut documents = DocumentCache::default();
|
||||
for (specifier, content, version) in sources {
|
||||
let specifier = ModuleSpecifier::resolve_url(specifier)
|
||||
.expect("failed to create specifier");
|
||||
doc_data.insert(
|
||||
specifier.clone(),
|
||||
DocumentData::new(specifier.clone(), version, content, None),
|
||||
);
|
||||
file_cache.set_contents(specifier, Some(content.as_bytes().to_vec()));
|
||||
documents.open(specifier, version, content.to_string());
|
||||
}
|
||||
let file_cache = Arc::new(Mutex::new(file_cache));
|
||||
StateSnapshot {
|
||||
assets: Default::default(),
|
||||
doc_data,
|
||||
file_cache,
|
||||
documents: Arc::new(Mutex::new(documents)),
|
||||
sources: Default::default(),
|
||||
}
|
||||
}
|
||||
|
@ -1413,29 +1444,31 @@ mod tests {
|
|||
let result = request(
|
||||
&mut runtime,
|
||||
state_snapshot,
|
||||
RequestMethod::GetDiagnostics(specifier),
|
||||
RequestMethod::GetDiagnostics(vec![specifier]),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
assert_eq!(
|
||||
response,
|
||||
json!([
|
||||
{
|
||||
"start": {
|
||||
"line": 0,
|
||||
"character": 0,
|
||||
},
|
||||
"end": {
|
||||
"line": 0,
|
||||
"character": 7
|
||||
},
|
||||
"fileName": "file:///a.ts",
|
||||
"messageText": "Cannot find name 'console'. Do you need to change your target library? Try changing the `lib` compiler option to include 'dom'.",
|
||||
"sourceLine": "console.log(\"hello deno\");",
|
||||
"category": 1,
|
||||
"code": 2584
|
||||
}
|
||||
])
|
||||
json!({
|
||||
"file:///a.ts": [
|
||||
{
|
||||
"start": {
|
||||
"line": 0,
|
||||
"character": 0,
|
||||
},
|
||||
"end": {
|
||||
"line": 0,
|
||||
"character": 7
|
||||
},
|
||||
"fileName": "file:///a.ts",
|
||||
"messageText": "Cannot find name 'console'. Do you need to change your target library? Try changing the `lib` compiler option to include 'dom'.",
|
||||
"sourceLine": "console.log(\"hello deno\");",
|
||||
"category": 1,
|
||||
"code": 2584
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1466,11 +1499,11 @@ mod tests {
|
|||
let result = request(
|
||||
&mut runtime,
|
||||
state_snapshot,
|
||||
RequestMethod::GetDiagnostics(specifier),
|
||||
RequestMethod::GetDiagnostics(vec![specifier]),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
assert_eq!(response, json!([]));
|
||||
assert_eq!(response, json!({ "file:///a.ts": [] }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1496,28 +1529,30 @@ mod tests {
|
|||
let result = request(
|
||||
&mut runtime,
|
||||
state_snapshot,
|
||||
RequestMethod::GetDiagnostics(specifier),
|
||||
RequestMethod::GetDiagnostics(vec![specifier]),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
assert_eq!(
|
||||
response,
|
||||
json!([{
|
||||
"start": {
|
||||
"line": 1,
|
||||
"character": 8
|
||||
},
|
||||
"end": {
|
||||
"line": 1,
|
||||
"character": 30
|
||||
},
|
||||
"fileName": "file:///a.ts",
|
||||
"messageText": "\'A\' is declared but its value is never read.",
|
||||
"sourceLine": " import { A } from \".\";",
|
||||
"category": 2,
|
||||
"code": 6133,
|
||||
"reportsUnnecessary": true,
|
||||
}])
|
||||
json!({
|
||||
"file:///a.ts": [{
|
||||
"start": {
|
||||
"line": 1,
|
||||
"character": 8
|
||||
},
|
||||
"end": {
|
||||
"line": 1,
|
||||
"character": 30
|
||||
},
|
||||
"fileName": "file:///a.ts",
|
||||
"messageText": "\'A\' is declared but its value is never read.",
|
||||
"sourceLine": " import { A } from \".\";",
|
||||
"category": 2,
|
||||
"code": 6133,
|
||||
"reportsUnnecessary": true,
|
||||
}]
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1548,11 +1583,11 @@ mod tests {
|
|||
let result = request(
|
||||
&mut runtime,
|
||||
state_snapshot,
|
||||
RequestMethod::GetDiagnostics(specifier),
|
||||
RequestMethod::GetDiagnostics(vec![specifier]),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
assert_eq!(response, json!([]));
|
||||
assert_eq!(response, json!({ "file:///a.ts": [] }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1585,42 +1620,44 @@ mod tests {
|
|||
let result = request(
|
||||
&mut runtime,
|
||||
state_snapshot,
|
||||
RequestMethod::GetDiagnostics(specifier),
|
||||
RequestMethod::GetDiagnostics(vec![specifier]),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
assert_eq!(
|
||||
response,
|
||||
json!([{
|
||||
"start": {
|
||||
"line": 1,
|
||||
"character": 8
|
||||
},
|
||||
"end": {
|
||||
"line": 6,
|
||||
"character": 55,
|
||||
},
|
||||
"fileName": "file:///a.ts",
|
||||
"messageText": "All imports in import declaration are unused.",
|
||||
"sourceLine": " import {",
|
||||
"category": 2,
|
||||
"code": 6192,
|
||||
"reportsUnnecessary": true
|
||||
}, {
|
||||
"start": {
|
||||
"line": 8,
|
||||
"character": 29
|
||||
},
|
||||
"end": {
|
||||
"line": 8,
|
||||
"character": 29
|
||||
},
|
||||
"fileName": "file:///a.ts",
|
||||
"messageText": "Expression expected.",
|
||||
"sourceLine": " import * as test from",
|
||||
"category": 1,
|
||||
"code": 1109
|
||||
}])
|
||||
json!({
|
||||
"file:///a.ts": [{
|
||||
"start": {
|
||||
"line": 1,
|
||||
"character": 8
|
||||
},
|
||||
"end": {
|
||||
"line": 6,
|
||||
"character": 55,
|
||||
},
|
||||
"fileName": "file:///a.ts",
|
||||
"messageText": "All imports in import declaration are unused.",
|
||||
"sourceLine": " import {",
|
||||
"category": 2,
|
||||
"code": 6192,
|
||||
"reportsUnnecessary": true
|
||||
}, {
|
||||
"start": {
|
||||
"line": 8,
|
||||
"character": 29
|
||||
},
|
||||
"end": {
|
||||
"line": 8,
|
||||
"character": 29
|
||||
},
|
||||
"fileName": "file:///a.ts",
|
||||
"messageText": "Expression expected.",
|
||||
"sourceLine": " import * as test from",
|
||||
"category": 1,
|
||||
"code": 1109
|
||||
}]
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1641,11 +1678,11 @@ mod tests {
|
|||
let result = request(
|
||||
&mut runtime,
|
||||
state_snapshot,
|
||||
RequestMethod::GetDiagnostics(specifier),
|
||||
RequestMethod::GetDiagnostics(vec![specifier]),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
assert_eq!(response, json!([]));
|
||||
assert_eq!(response, json!({}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
25
cli/tests/lsp/did_change_notification_large.json
Normal file
25
cli/tests/lsp/did_change_notification_large.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "textDocument/didChange",
|
||||
"params": {
|
||||
"textDocument": {
|
||||
"uri": "file:///a/file.ts",
|
||||
"version": 2
|
||||
},
|
||||
"contentChanges": [
|
||||
{
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 444,
|
||||
"character": 11
|
||||
},
|
||||
"end": {
|
||||
"line": 444,
|
||||
"character": 14
|
||||
}
|
||||
},
|
||||
"text": "+++"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
25
cli/tests/lsp/did_change_notification_mbc.json
Normal file
25
cli/tests/lsp/did_change_notification_mbc.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "textDocument/didChange",
|
||||
"params": {
|
||||
"textDocument": {
|
||||
"uri": "file:///a/file.ts",
|
||||
"version": 2
|
||||
},
|
||||
"contentChanges": [
|
||||
{
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 1,
|
||||
"character": 11
|
||||
},
|
||||
"end": {
|
||||
"line": 1,
|
||||
"character": 13
|
||||
}
|
||||
},
|
||||
"text": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
12
cli/tests/lsp/did_open_notification_large.json
Normal file
12
cli/tests/lsp/did_open_notification_large.json
Normal file
File diff suppressed because one or more lines are too long
12
cli/tests/lsp/did_open_notification_mbc.json
Normal file
12
cli/tests/lsp/did_open_notification_mbc.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "textDocument/didOpen",
|
||||
"params": {
|
||||
"textDocument": {
|
||||
"uri": "file:///a/file.ts",
|
||||
"languageId": "typescript",
|
||||
"version": 1,
|
||||
"text": "const a = `编写软件很难`;\nconst b = `👍🦕😃`;\nconsole.log(a, b);\n"
|
||||
}
|
||||
}
|
||||
}
|
14
cli/tests/lsp/hover_request_mbc.json
Normal file
14
cli/tests/lsp/hover_request_mbc.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "textDocument/hover",
|
||||
"params": {
|
||||
"textDocument": {
|
||||
"uri": "file:///a/file.ts"
|
||||
},
|
||||
"position": {
|
||||
"line": 2,
|
||||
"character": 14
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,8 +7,8 @@
|
|||
"uri": "file:///a/file.ts"
|
||||
},
|
||||
"position": {
|
||||
"line": 5,
|
||||
"character": 19
|
||||
"line": 0,
|
||||
"character": 4
|
||||
},
|
||||
"newName": "variable_modified"
|
||||
}
|
||||
|
|
|
@ -511,15 +511,23 @@ delete Object.prototype.__proto__;
|
|||
}
|
||||
case "getDiagnostics": {
|
||||
try {
|
||||
const diagnostics = [
|
||||
...languageService.getSemanticDiagnostics(request.specifier),
|
||||
...languageService.getSuggestionDiagnostics(request.specifier),
|
||||
...languageService.getSyntacticDiagnostics(request.specifier),
|
||||
].filter(({ code }) => !IGNORED_DIAGNOSTICS.includes(code));
|
||||
return respond(id, fromTypeScriptDiagnostic(diagnostics));
|
||||
/** @type {Record<string, any[]>} */
|
||||
const diagnosticMap = {};
|
||||
for (const specifier of request.specifiers) {
|
||||
diagnosticMap[specifier] = fromTypeScriptDiagnostic([
|
||||
...languageService.getSemanticDiagnostics(specifier),
|
||||
...languageService.getSuggestionDiagnostics(specifier),
|
||||
...languageService.getSyntacticDiagnostics(specifier),
|
||||
].filter(({ code }) => !IGNORED_DIAGNOSTICS.includes(code)));
|
||||
}
|
||||
return respond(id, diagnosticMap);
|
||||
} catch (e) {
|
||||
error(e);
|
||||
return respond(id, []);
|
||||
if ("stack" in e) {
|
||||
error(e.stack);
|
||||
} else {
|
||||
error(e);
|
||||
}
|
||||
return respond(id, {});
|
||||
}
|
||||
}
|
||||
case "getQuickInfo": {
|
||||
|
|
2
cli/tsc/compiler.d.ts
vendored
2
cli/tsc/compiler.d.ts
vendored
|
@ -70,7 +70,7 @@ declare global {
|
|||
|
||||
interface GetDiagnosticsRequest extends BaseLanguageServerRequest {
|
||||
method: "getDiagnostics";
|
||||
specifier: string;
|
||||
specifiers: string[];
|
||||
}
|
||||
|
||||
interface GetQuickInfoRequest extends BaseLanguageServerRequest {
|
||||
|
|
Loading…
Reference in a new issue