1
1
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:
oxalica 2022-09-05 23:50:43 +08:00
parent efb87783e6
commit 90e3ec7852
6 changed files with 303 additions and 8 deletions

View File

@ -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.

View File

@ -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(&params.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, &params.text_document)?;
let pos = from_pos(snap, file, params.position)?;
Some(FilePos::new(file, pos))
}

View File

@ -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, &params.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()
}

View File

@ -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
View 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; }
"#]],
);
}
}

View File

@ -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))
}
}