Skip to content

Commit

Permalink
feat(lsp): implement textDocument/foldingRange (denoland#9900)
Browse files Browse the repository at this point in the history
Co-authored-by: Kitson Kelly <[email protected]>
  • Loading branch information
jeanp413 and kitsonk committed Apr 2, 2021
1 parent f50385b commit 035f7b0
Show file tree
Hide file tree
Showing 9 changed files with 274 additions and 2 deletions.
3 changes: 2 additions & 1 deletion cli/lsp/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use lspower::lsp::CodeActionOptions;
use lspower::lsp::CodeActionProviderCapability;
use lspower::lsp::CodeLensOptions;
use lspower::lsp::CompletionOptions;
use lspower::lsp::FoldingRangeProviderCapability;
use lspower::lsp::HoverProviderCapability;
use lspower::lsp::ImplementationProviderCapability;
use lspower::lsp::OneOf;
Expand Down Expand Up @@ -108,7 +109,7 @@ pub fn server_capabilities(
selection_range_provider: Some(SelectionRangeProviderCapability::Simple(
true,
)),
folding_range_provider: None,
folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),
rename_provider: Some(OneOf::Left(true)),
document_link_provider: None,
color_provider: None,
Expand Down
9 changes: 9 additions & 0 deletions cli/lsp/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub struct ClientCapabilities {
pub status_notification: bool,
pub workspace_configuration: bool,
pub workspace_did_change_watched_files: bool,
pub line_folding_only: bool,
}

#[derive(Debug, Clone, Deserialize)]
Expand Down Expand Up @@ -125,5 +126,13 @@ impl Config {
.and_then(|it| it.dynamic_registration)
.unwrap_or(false);
}

if let Some(text_document) = &capabilities.text_document {
self.client_capabilities.line_folding_only = text_document
.folding_range
.as_ref()
.and_then(|it| it.line_folding_only)
.unwrap_or(false);
}
}
}
131 changes: 131 additions & 0 deletions cli/lsp/language_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,25 @@ impl Inner {
maybe_line_index
}

// TODO(@kitsonk) we really should find a better way to just return the
// content as a `&str`, or be able to get the byte at a particular offset
// which is all that this API that is consuming it is trying to do at the
// moment
/// Searches already cached assets and documents and returns its text
/// content. If not found, `None` is returned.
fn get_text_content(&self, specifier: &ModuleSpecifier) -> Option<String> {
if specifier.scheme() == "asset" {
self
.assets
.get(specifier)
.map(|o| o.clone().map(|a| a.text))?
} else if self.documents.contains_key(specifier) {
self.documents.content(specifier).unwrap()
} else {
self.sources.get_source(specifier)
}
}

async fn get_navigation_tree(
&mut self,
specifier: &ModuleSpecifier,
Expand Down Expand Up @@ -1515,6 +1534,63 @@ impl Inner {
Ok(result)
}

async fn folding_range(
&self,
params: FoldingRangeParams,
) -> LspResult<Option<Vec<FoldingRange>>> {
if !self.enabled() {
return Ok(None);
}
let mark = self.performance.mark("folding_range");
let specifier = self.url_map.normalize_url(&params.text_document.uri);

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::GetOutliningSpans(specifier.clone());
let outlining_spans: Vec<tsc::OutliningSpan> = self
.ts_server
.request(self.snapshot(), req)
.await
.map_err(|err| {
error!("Failed to request to tsserver {}", err);
LspError::invalid_request()
})?;

let response = if !outlining_spans.is_empty() {
let text_content =
self.get_text_content(&specifier).ok_or_else(|| {
LspError::invalid_params(format!(
"An unexpected specifier ({}) was provided.",
specifier
))
})?;
Some(
outlining_spans
.iter()
.map(|span| {
span.to_folding_range(
&line_index,
text_content.as_str().as_bytes(),
self.config.client_capabilities.line_folding_only,
)
})
.collect::<Vec<FoldingRange>>(),
)
} else {
None
};
self.performance.measure(mark);
Ok(response)
}

async fn rename(
&mut self,
params: RenameParams,
Expand Down Expand Up @@ -1840,6 +1916,13 @@ impl lspower::LanguageServer for LanguageServer {
self.0.lock().await.goto_implementation(params).await
}

async fn folding_range(
&self,
params: FoldingRangeParams,
) -> LspResult<Option<Vec<FoldingRange>>> {
self.0.lock().await.folding_range(params).await
}

async fn rename(
&self,
params: RenameParams,
Expand Down Expand Up @@ -2425,6 +2508,54 @@ mod tests {
);
}

#[tokio::test]
async fn test_folding_range() {
let mut harness = LspTestHarness::new(vec![
("initialize_request.json", LspResponse::RequestAny),
("initialized_notification.json", LspResponse::None),
(
"folding_range_did_open_notification.json",
LspResponse::None,
),
(
"folding_range_request.json",
LspResponse::Request(
2,
json!([
{
"startLine": 0,
"endLine": 12,
"kind": "region"
},
{
"startLine": 1,
"endLine": 3,
"kind": "comment"
},
{
"startLine": 4,
"endLine": 10
},
{
"startLine": 5,
"endLine": 9
},
{
"startLine": 6,
"endLine": 7
}
]),
),
),
(
"shutdown_request.json",
LspResponse::Request(3, json!(null)),
),
("exit_notification.json", LspResponse::None),
]);
harness.run().await;
}

#[tokio::test]
async fn test_rename() {
let mut harness = LspTestHarness::new(vec![
Expand Down
94 changes: 93 additions & 1 deletion cli/lsp/tsc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ use log::warn;
use lspower::lsp;
use regex::Captures;
use regex::Regex;
use std::borrow::Cow;
use std::collections::HashMap;
use std::collections::HashSet;
use std::thread;
use std::{borrow::Cow, cmp};
use text_size::TextSize;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
Expand Down Expand Up @@ -1227,6 +1227,91 @@ impl CompletionEntry {
}
}

#[derive(Debug, Deserialize)]
pub enum OutliningSpanKind {
#[serde(rename = "comment")]
Comment,
#[serde(rename = "region")]
Region,
#[serde(rename = "code")]
Code,
#[serde(rename = "imports")]
Imports,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OutliningSpan {
text_span: TextSpan,
hint_span: TextSpan,
banner_text: String,
auto_collapse: bool,
kind: OutliningSpanKind,
}

const FOLD_END_PAIR_CHARACTERS: &[u8] = &[b'}', b']', b')', b'`'];

impl OutliningSpan {
pub fn to_folding_range(
&self,
line_index: &LineIndex,
content: &[u8],
line_folding_only: bool,
) -> lsp::FoldingRange {
let range = self.text_span.to_range(line_index);
lsp::FoldingRange {
start_line: range.start.line,
start_character: if line_folding_only {
None
} else {
Some(range.start.character)
},
end_line: self.adjust_folding_end_line(
&range,
line_index,
content,
line_folding_only,
),
end_character: if line_folding_only {
None
} else {
Some(range.end.character)
},
kind: self.get_folding_range_kind(&self.kind),
}
}

fn adjust_folding_end_line(
&self,
range: &lsp::Range,
line_index: &LineIndex,
content: &[u8],
line_folding_only: bool,
) -> u32 {
if line_folding_only && range.end.character > 0 {
let offset_end: usize = line_index.offset(range.end).unwrap().into();
let fold_end_char = content[offset_end - 1];
if FOLD_END_PAIR_CHARACTERS.contains(&fold_end_char) {
return cmp::max(range.end.line - 1, range.start.line);
}
}

range.end.line
}

fn get_folding_range_kind(
&self,
span_kind: &OutliningSpanKind,
) -> Option<lsp::FoldingRangeKind> {
match span_kind {
OutliningSpanKind::Comment => Some(lsp::FoldingRangeKind::Comment),
OutliningSpanKind::Region => Some(lsp::FoldingRangeKind::Region),
OutliningSpanKind::Imports => Some(lsp::FoldingRangeKind::Imports),
_ => None,
}
}
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignatureHelpItems {
Expand Down Expand Up @@ -1859,6 +1944,8 @@ pub enum RequestMethod {
GetImplementation((ModuleSpecifier, u32)),
/// Get a "navigation tree" for a specifier.
GetNavigationTree(ModuleSpecifier),
/// Get outlining spans for a specifier.
GetOutliningSpans(ModuleSpecifier),
/// Return quick info at position (hover information).
GetQuickInfo((ModuleSpecifier, u32)),
/// Get document references for a specific position.
Expand Down Expand Up @@ -1967,6 +2054,11 @@ impl RequestMethod {
"method": "getNavigationTree",
"specifier": specifier,
}),
RequestMethod::GetOutliningSpans(specifier) => json!({
"id": id,
"method": "getOutliningSpans",
"specifier": specifier,
}),
RequestMethod::GetQuickInfo((specifier, position)) => json!({
"id": id,
"method": "getQuickInfo",
Expand Down
12 changes: 12 additions & 0 deletions cli/tests/lsp/folding_range_did_open_notification.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file:https:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "// #region 1\n/*\n * Some comment\n */\nclass Foo {\n bar(a, b) {\n if (a === b) {\n return true;\n }\n return false;\n }\n}\n// #endregion"
}
}
}
10 changes: 10 additions & 0 deletions cli/tests/lsp/folding_range_request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"jsonrpc": "2.0",
"id": 2,
"method": "textDocument/foldingRange",
"params": {
"textDocument": {
"uri": "file:https:///a/file.ts"
}
}
}
3 changes: 3 additions & 0 deletions cli/tests/lsp/initialize_request.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
]
}
},
"foldingRange": {
"lineFoldingOnly": true
},
"synchronization": {
"dynamicRegistration": true,
"willSave": true,
Expand Down
8 changes: 8 additions & 0 deletions cli/tsc/99_main_compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,14 @@ delete Object.prototype.__proto__;
languageService.getNavigationTree(request.specifier),
);
}
case "getOutliningSpans": {
return respond(
id,
languageService.getOutliningSpans(
request.specifier,
),
);
}
case "getQuickInfo": {
return respond(
id,
Expand Down
6 changes: 6 additions & 0 deletions cli/tsc/compiler.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ declare global {
| GetDocumentHighlightsRequest
| GetImplementationRequest
| GetNavigationTree
| GetOutliningSpans
| GetQuickInfoRequest
| GetReferencesRequest
| GetSignatureHelpItemsRequest
Expand Down Expand Up @@ -151,6 +152,11 @@ declare global {
specifier: string;
}

interface GetOutliningSpans extends BaseLanguageServerRequest {
method: "getOutliningSpans";
specifier: string;
}

interface GetQuickInfoRequest extends BaseLanguageServerRequest {
method: "getQuickInfo";
specifier: string;
Expand Down

0 comments on commit 035f7b0

Please sign in to comment.