From 90e3ec785268c4c184d2bd0fdc25453bfbb4a749 Mon Sep 17 00:00:00 2001 From: oxalica Date: Mon, 5 Sep 2022 23:50:43 +0800 Subject: [PATCH] Impl expand-selection --- README.md | 1 + lsp/src/convert.rs | 22 +++- lsp/src/handler.rs | 43 ++++++- lsp/src/state.rs | 1 + src/ide/expand_selection.rs | 239 ++++++++++++++++++++++++++++++++++++ src/ide/mod.rs | 5 + 6 files changed, 303 insertions(+), 8 deletions(-) create mode 100644 src/ide/expand_selection.rs diff --git a/README.md b/README.md index ba3f68b..6ac74a9 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Super fast incremental analysis! Scans `all-packages.nix` in less than 0.1s and - [x] Warnings of unnecessary syntax. - [x] Warnings of unused bindings, `with` and `rec`. - [ ] Client pulled diagnostics. +- [x] Expand selection. `textDocument/selectionRange` - [ ] Cross-file analysis. - [ ] Multi-threaded. diff --git a/lsp/src/convert.rs b/lsp/src/convert.rs index 1cac0c5..837e452 100644 --- a/lsp/src/convert.rs +++ b/lsp/src/convert.rs @@ -1,19 +1,29 @@ use crate::{LineMap, StateSnapshot, Vfs}; use lsp_types::{ self as lsp, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, Location, - Position, Range, TextDocumentPositionParams, + Position, Range, TextDocumentIdentifier, TextDocumentPositionParams, }; use nil::{CompletionItem, CompletionItemKind, Diagnostic, FileId, FilePos, FileRange, Severity}; -use text_size::TextRange; +use text_size::{TextRange, TextSize}; + +pub(crate) fn from_file(snap: &StateSnapshot, doc: &TextDocumentIdentifier) -> Option { + let vfs = snap.vfs.read().unwrap(); + vfs.get_file_for_uri(&doc.uri) +} + +pub(crate) fn from_pos(snap: &StateSnapshot, file: FileId, pos: Position) -> Option { + let vfs = snap.vfs.read().unwrap(); + let line_map = vfs.get_line_map(file)?; + let pos = line_map.pos(pos.line, pos.character); + Some(pos) +} pub(crate) fn from_file_pos( snap: &StateSnapshot, params: &TextDocumentPositionParams, ) -> Option { - let vfs = snap.vfs.read().unwrap(); - let file = vfs.get_file_for_uri(¶ms.text_document.uri)?; - let line_map = vfs.get_line_map(file)?; - let pos = line_map.pos(params.position.line, params.position.character); + let file = from_file(snap, ¶ms.text_document)?; + let pos = from_pos(snap, file, params.position)?; Some(FilePos::new(file, pos)) } diff --git a/lsp/src/handler.rs b/lsp/src/handler.rs index a871bc4..313a59e 100644 --- a/lsp/src/handler.rs +++ b/lsp/src/handler.rs @@ -1,10 +1,12 @@ use crate::{convert, StateSnapshot}; use lsp_types::{ CompletionOptions, CompletionParams, CompletionResponse, GotoDefinitionParams, - GotoDefinitionResponse, Location, OneOf, ReferenceParams, ServerCapabilities, - TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, + GotoDefinitionResponse, Location, OneOf, ReferenceParams, SelectionRange, SelectionRangeParams, + SelectionRangeProviderCapability, ServerCapabilities, TextDocumentSyncCapability, + TextDocumentSyncKind, TextDocumentSyncOptions, }; use nil::FileRange; +use text_size::TextRange; pub(crate) fn server_capabilities() -> ServerCapabilities { ServerCapabilities { @@ -21,6 +23,7 @@ pub(crate) fn server_capabilities() -> ServerCapabilities { ..Default::default() }), references_provider: Some(OneOf::Left(true)), + selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)), ..Default::default() } } @@ -66,3 +69,39 @@ pub(crate) fn completion( .collect::>(); Some(CompletionResponse::Array(items)) } + +pub(crate) fn selection_range( + snap: StateSnapshot, + params: SelectionRangeParams, +) -> Option> { + let file = convert::from_file(&snap, ¶ms.text_document)?; + params + .positions + .iter() + .map(|&pos| { + let pos = convert::from_pos(&snap, file, pos)?; + let frange = FileRange::new(file, TextRange::empty(pos)); + + let mut ranges = snap.analysis.expand_selection(frange).ok()??; + if ranges.is_empty() { + ranges.push(TextRange::empty(pos)); + } + + // FIXME: Use Arc for LineMap. + let vfs = snap.vfs.read().unwrap(); + let line_map = vfs.get_line_map(file)?; + let mut ret = SelectionRange { + range: convert::to_range(line_map, *ranges.last().unwrap()), + parent: None, + }; + for &r in ranges.iter().rev().skip(1) { + ret = SelectionRange { + range: convert::to_range(line_map, r), + parent: Some(ret.into()), + }; + } + + Some(ret) + }) + .collect() +} diff --git a/lsp/src/state.rs b/lsp/src/state.rs index b6be6d3..46422e4 100644 --- a/lsp/src/state.rs +++ b/lsp/src/state.rs @@ -84,6 +84,7 @@ impl State { .on::(handler::goto_definition) .on::(handler::references) .on::(handler::completion) + .on::(handler::selection_range) .finish(); } diff --git a/src/ide/expand_selection.rs b/src/ide/expand_selection.rs new file mode 100644 index 0000000..7b0454c --- /dev/null +++ b/src/ide/expand_selection.rs @@ -0,0 +1,239 @@ +use crate::{DefDatabase, FileRange}; +use rowan::{NodeOrToken, TextRange}; +use syntax::{best_token_at_offset, SyntaxKind, SyntaxNode, T}; + +/// Interesting parent ranges covering the given range. +/// Returns all ranges from the smallest to the largest. +pub(crate) fn expand_selection( + db: &dyn DefDatabase, + FileRange { file_id, range }: FileRange, +) -> Option> { + let parse = db.parse(file_id); + + let mut ret = Vec::new(); + + let leaf = if range.start() == range.end() { + let tok = best_token_at_offset(&parse.syntax_node(), range.start())?; + NodeOrToken::Token(tok) + } else { + parse.syntax_node().covering_element(range) + }; + + let node = match leaf { + NodeOrToken::Node(node) => Some(node), + NodeOrToken::Token(tok) => { + if is_node_kind_good(tok.kind()) { + ret.push(tok.text_range()); + } + tok.parent() + } + }; + + ret.extend( + std::iter::successors(node, |node| node.parent()).filter_map(|node| { + is_node_kind_good(node.kind()) + .then(|| non_space_range(&node)) + .flatten() + }), + ); + ret.dedup(); + + Some(ret) +} + +/// Trim spaces for the range of a node. +/// Note that comments are not trimmed. +fn non_space_range(node: &SyntaxNode) -> Option { + // Whitespaces would be merged into one token if exist. + let first = node.first_token()?; + let lhs = if first.kind() != SyntaxKind::SPACE { + first.text_range().start() + } else { + first.text_range().end() + }; + + let last = node.last_token()?; + let rhs = if last.kind() != SyntaxKind::SPACE { + last.text_range().end() + } else { + last.text_range().start() + }; + + Some(TextRange::empty(lhs).cover_offset(rhs)) +} + +/// If this node/token kind is good enough to show as interesting selection. +fn is_node_kind_good(kind: SyntaxKind) -> bool { + !matches!( + kind, + // Ignore spaces, but keep comments. + | SyntaxKind::SPACE + // Ignore fragments and internal tokens. + | SyntaxKind::STRING_FRAGMENT + | SyntaxKind::PATH_FRAGMENT + | SyntaxKind::PATH_START + | SyntaxKind::PATH_END + // Ignore delimiters. Only select the whole node. + | T!['('] | T![')'] + | T!['['] | T![']'] + | T!['{'] | T!['}'] | T!["${"] + | T!['"'] | T!["''"] + // Separators seem not useful. + | T![,] | T![;] | T![.] + ) +} + +#[cfg(test)] +mod tests { + use crate::base::SourceDatabase; + use crate::tests::TestDB; + use crate::FileRange; + use expect_test::{expect, Expect}; + use rowan::TextRange; + + fn check(fixture: &str, expect: Expect) { + let (db, f) = TestDB::from_fixture(fixture).unwrap(); + let frange = match f.markers() { + [fpos] => FileRange::new(fpos.file_id, TextRange::empty(fpos.pos)), + [lpos, rpos] => { + assert_eq!(lpos.file_id, rpos.file_id); + FileRange::new(lpos.file_id, TextRange::new(lpos.pos, rpos.pos)) + } + _ => unreachable!(), + }; + + let src = db.file_content(f[0].file_id); + let got = super::expand_selection(&db, frange) + .into_iter() + .flatten() + .flat_map(|range| { + assert_eq!(src[range].trim(), &src[range]); + [&src[range], "\n"] + }) + .collect::(); + expect.assert_eq(&got); + } + + #[test] + fn operators() { + check( + "a + b $0c * d == e", + expect![[r#" + c + b c + b c * d + a + b c * d + a + b c * d == e + "#]], + ); + + check( + "a + b $0* c", + expect![[r#" + * + b * c + a + b * c + "#]], + ); + } + + #[test] + fn comment() { + check( + "f /* $0foo */ x", + expect![[r#" + /* foo */ + f /* foo */ x + "#]], + ); + } + + #[test] + fn binding_path_value() { + check( + "let a.b.c = a; $0in a", + expect![[r#" + in + let a.b.c = a; in a + "#]], + ); + check( + "let a.b.c = $0a; in a", + expect![[r#" + a + a.b.c = a; + let a.b.c = a; in a + "#]], + ); + check( + "let a.b.c $0= a; in a", + expect![[r#" + = + a.b.c = a; + let a.b.c = a; in a + "#]], + ); + check( + "let a.$0b.c = a; in a", + expect![[r#" + b + a.b.c + a.b.c = a; + let a.b.c = a; in a + "#]], + ); + check( + "let a$0.b.$1c = a; in a", + expect![[r#" + a.b.c + a.b.c = a; + let a.b.c = a; in a + "#]], + ); + } + + #[test] + fn inherit() { + check( + "{ inherit a $0b; }", + expect![[r#" + b + inherit a b; + { inherit a b; } + "#]], + ); + check( + "{ inherit$0 a b; }", + expect![[r#" + inherit + inherit a b; + { inherit a b; } + "#]], + ); + check( + "{ inheri$0t a$1 b; }", + expect![[r#" + inherit a b; + { inherit a b; } + "#]], + ); + + check( + "{ inherit (a$0) a b; }", + expect![[r#" + a + (a) + inherit (a) a b; + { inherit (a) a b; } + "#]], + ); + check( + "{ inherit (a)$0 a b; }", + expect![[r#" + (a) + inherit (a) a b; + { inherit (a) a b; } + "#]], + ); + } +} diff --git a/src/ide/mod.rs b/src/ide/mod.rs index 15aafe3..9d0fed0 100644 --- a/src/ide/mod.rs +++ b/src/ide/mod.rs @@ -1,5 +1,6 @@ mod completion; mod diagnostics; +mod expand_selection; mod goto_definition; mod references; @@ -97,4 +98,8 @@ impl Analysis { pub fn references(&self, pos: FilePos) -> Cancellable>> { self.with_db(|db| references::references(db, pos)) } + + pub fn expand_selection(&self, frange: FileRange) -> Cancellable>> { + self.with_db(|db| expand_selection::expand_selection(db, frange)) + } }