mirror of
https://github.com/oxalica/nil.git
synced 2024-11-23 03:57:06 +03:00
Impl expand-selection
This commit is contained in:
parent
efb87783e6
commit
90e3ec7852
@ -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.
|
||||
|
||||
|
@ -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<FileId> {
|
||||
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<TextSize> {
|
||||
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<FilePos> {
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -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::<Vec<_>>();
|
||||
Some(CompletionResponse::Array(items))
|
||||
}
|
||||
|
||||
pub(crate) fn selection_range(
|
||||
snap: StateSnapshot,
|
||||
params: SelectionRangeParams,
|
||||
) -> Option<Vec<SelectionRange>> {
|
||||
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()
|
||||
}
|
||||
|
@ -84,6 +84,7 @@ impl State {
|
||||
.on::<req::GotoDefinition>(handler::goto_definition)
|
||||
.on::<req::References>(handler::references)
|
||||
.on::<req::Completion>(handler::completion)
|
||||
.on::<req::SelectionRangeRequest>(handler::selection_range)
|
||||
.finish();
|
||||
}
|
||||
|
||||
|
239
src/ide/expand_selection.rs
Normal file
239
src/ide/expand_selection.rs
Normal file
@ -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<Vec<TextRange>> {
|
||||
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<TextRange> {
|
||||
// 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::<String>();
|
||||
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; }
|
||||
"#]],
|
||||
);
|
||||
}
|
||||
}
|
@ -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<Option<Vec<FileRange>>> {
|
||||
self.with_db(|db| references::references(db, pos))
|
||||
}
|
||||
|
||||
pub fn expand_selection(&self, frange: FileRange) -> Cancellable<Option<Vec<TextRange>>> {
|
||||
self.with_db(|db| expand_selection::expand_selection(db, frange))
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user