Skip to content

Commit

Permalink
Added FoldingRange Feature to LSP
Browse files Browse the repository at this point in the history
  • Loading branch information
joewyz committed Feb 5, 2025
1 parent 1e48800 commit 5ae4a5e
Show file tree
Hide file tree
Showing 4 changed files with 689 additions and 2 deletions.
23 changes: 23 additions & 0 deletions src/main/java/software/amazon/smithy/lsp/SmithyLanguageServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
import org.eclipse.lsp4j.DocumentFormattingParams;
import org.eclipse.lsp4j.DocumentSymbol;
import org.eclipse.lsp4j.DocumentSymbolParams;
import org.eclipse.lsp4j.FoldingRangeRequestParams;
import org.eclipse.lsp4j.FoldingRange;
import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.HoverParams;
import org.eclipse.lsp4j.InitializeParams;
Expand Down Expand Up @@ -97,6 +99,7 @@
import software.amazon.smithy.lsp.language.DefinitionHandler;
import software.amazon.smithy.lsp.language.DocumentSymbolHandler;
import software.amazon.smithy.lsp.language.HoverHandler;
import software.amazon.smithy.lsp.language.FoldingRangeHandler;
import software.amazon.smithy.lsp.project.BuildFile;
import software.amazon.smithy.lsp.project.IdlFile;
import software.amazon.smithy.lsp.project.Project;
Expand Down Expand Up @@ -565,6 +568,26 @@ public CompletableFuture<CompletionItem> resolveCompletionItem(CompletionItem un
return CompletableFuture.supplyAsync(handler::handle);
}

@Override
public CompletableFuture<List<FoldingRange>> foldingRange(FoldingRangeRequestParams params) {
LOGGER.finest("FoldingRange");

String uri = params.getTextDocument().getUri();
ProjectAndFile projectAndFile = state.findProjectAndFile(uri);
if (projectAndFile == null) {
client.unknownFileError(uri, "folding range");
return completedFuture(Collections.emptyList());
}

if (!(projectAndFile.file() instanceof IdlFile idlFile)) {
return completedFuture(List.of());
}

List<Syntax.Statement> statements = idlFile.getParse().statements();
var handler = new FoldingRangeHandler(idlFile.document(), idlFile.getParse().imports(), statements);
return CompletableFuture.supplyAsync(handler::handle);
}

@Override
public CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>>
definition(DefinitionParams params) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.lsp.language;

import org.eclipse.lsp4j.FoldingRange;
import org.eclipse.lsp4j.Range;
import software.amazon.smithy.lsp.document.Document;
import software.amazon.smithy.lsp.document.DocumentImports;
import software.amazon.smithy.lsp.syntax.Syntax;

import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;


public record FoldingRangeHandler(Document document, DocumentImports documentImports,
List<Syntax.Statement> statements) {
/**
* Main public handle function in the handler class
*
* @return A list of FoldingRange
*/
public List<FoldingRange> handle() {
return generateFoldingRanges();

}

/**
* Check if the given range can be folded
*
* @param range The Range object held the start line and end line info for the folding range
* @return If the given Range can be folded (end line - start line >= 1)
*/
private boolean isFoldable(Range range) {
return range != null && range.getEnd().getLine() - range.getStart().getLine() >= 1;
}

/**
* Create a fold range object with given range and add it to the foldingRanges list
*
* @param range The Range object held the start line and end line info for the folding range
* @param foldingRanges The list holds folding ranges for current document
*/
private void addFoldingRange(Range range, List<FoldingRange> foldingRanges) {
if (!isFoldable(range)) return;
foldingRanges.add(new FoldingRange(range.getStart().getLine(), range.getEnd().getLine()));
}

/**
* Create range with start and end character index and then create a fold range object with given range and add it
* to the foldingRanges list.
*
* @param startCharacter The index of the start character of the folding range
* @param endCharacter The index of the start character of the folding range
* @param foldingRanges The list holds folding ranges for current document
*/
private void addFoldingRange(int startCharacter, int endCharacter, List<FoldingRange> foldingRanges) {
Range range = document.rangeBetween(startCharacter, endCharacter);
addFoldingRange(range, foldingRanges);
}

/**
* Use the import range in DocumentImport class to create the folding range directly
*
* @param documentImports The document import object that held the import range
* @param foldingRanges The list holds folding ranges for current document
*/
private void addFoldingRangeForImports(DocumentImports documentImports, List<FoldingRange> foldingRanges) {
Range range = documentImports.importsRange();
addFoldingRange(range, foldingRanges);
}

/**
* Main function to iterate statements and process the folding range based on the statement type
*
* @return The list holds folding ranges for current document
*/
private List<FoldingRange> generateFoldingRanges() {
List<FoldingRange> foldingRanges = new ArrayList<>();

addFoldingRangeForImports(documentImports, foldingRanges);

ListIterator<Syntax.Statement> iterator = statements.listIterator();

while (iterator.hasNext()) {
var statement = iterator.next();
switch (statement) {
case Syntax.Statement.TraitApplication trait ->
processFoldingRangeForTraitApplication(trait, iterator, foldingRanges);

case Syntax.Statement.Metadata metadata -> processFoldingRangeForNode(metadata.value(), foldingRanges);

case Syntax.Statement.Block blk -> processFoldingRangeForBlock(blk, foldingRanges);

case Syntax.Statement.NodeMemberDef nodeMember ->
processFoldingRangeForNode(nodeMember.value(), foldingRanges);

// For most of the current statement types just create the folding range based on
// its own range
default -> addFoldingRange(statement.start(), statement.end(), foldingRanges);
}
}
return foldingRanges;
}

/**
* Function to process the block statement's folding range.
*
* @param blk The block statement to be processed
* @param foldingRanges The list holds folding ranges for current document
*/
private void processFoldingRangeForBlock(Syntax.Statement.Block blk, List<FoldingRange> foldingRanges) {
// If the block is empty, the last statement index will not be set.
if (blk.lastStatementIndex() == blk.statementIndex()) {
return;
}
addFoldingRange(blk.start(), blk.end(), foldingRanges);
}

/**
* Since traits can appear multiple times, we want to fold the trait block. Need to use iterator to find the last
* trait statement for the trait block.
*
* @param trait The first trait statement for this potential trait block
* @param iterator The list iterator for the statements passed into this handler class
* @param foldingRanges The list holds folding ranges for current document
*/
private void processFoldingRangeForTraitApplication(Syntax.Statement.TraitApplication trait, ListIterator<Syntax.Statement> iterator, List<FoldingRange> foldingRanges) {
int traitBlockStart = trait.start();
int traitBlockEnd = -1;
// Create folding range for the start trait statement
processFoldingRangeForNode(trait.value(), foldingRanges);
// Find next non-trait statement and create folding range for the statement traversed
while (iterator.hasNext()) {
var nextStatement = iterator.next();

if (nextStatement instanceof Syntax.Statement.TraitApplication nextTrait) {
traitBlockEnd = nextTrait.value() == null ? nextTrait.end() : nextTrait.value().end();
processFoldingRangeForNode(nextTrait.value(), foldingRanges);
} else {
iterator.previous();
break;
}
}
//Single nested trait is handled by createFoldingRangeForNode
if (traitBlockEnd != -1) {
addFoldingRange(traitBlockStart, traitBlockEnd, foldingRanges);
}
}

/**
* Recursively process the node's folding range for nested declarations.
*
* @param node The node object to be processed
* @param foldingRanges The list holds folding ranges for current document
*/
private void processFoldingRangeForNode(Syntax.Node node, List<FoldingRange> foldingRanges) {
if (node == null) {
return;
}

Range range = document.rangeBetween(node.start(), node.end());

if (!isFoldable(range)) {
return;
}

switch (node) {
case Syntax.Node.Kvps kvps -> {
if (kvps.kvps().isEmpty()) {
return;
}

addFoldingRange(kvps.start(), kvps.end(), foldingRanges);

kvps.kvps().forEach(kvp -> processFoldingRangeForNode(kvp.value(), foldingRanges));
}
// Obj only contains kvps, will use kvps as its folding range
case Syntax.Node.Obj obj -> processFoldingRangeForNode(obj.kvps(), foldingRanges);

case Syntax.Node.Arr arr -> {
if (arr.elements().isEmpty()) {
return;
}

addFoldingRange(arr.start(), arr.end(), foldingRanges);

arr.elements().forEach(element -> processFoldingRangeForNode(element, foldingRanges));
}

default -> addFoldingRange(node.start(), node.end(), foldingRanges);
}
}

}
7 changes: 5 additions & 2 deletions src/main/java/software/amazon/smithy/lsp/syntax/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ private Syntax.Node.Obj obj() {
if (is('}')) {
skip();
setEnd(obj);
obj.kvps.start = obj.start;
obj.kvps.end = obj.end;
return obj;
}

Expand All @@ -228,7 +230,6 @@ private Syntax.Node.Obj obj() {

ws();
}

Syntax.Node.Err err = new Syntax.Node.Err("missing }");
setStart(err);
setEnd(err);
Expand Down Expand Up @@ -308,6 +309,7 @@ private Syntax.Node.Err kvp(Syntax.Node.Kvps kvps, char close) {
err = e;
} else if (err == null) {
kvp.value = value;
kvp.end = value.end;
if (is(',')) {
skip();
}
Expand Down Expand Up @@ -813,15 +815,16 @@ private void enumMember(Syntax.Statement.Block parent) {
Syntax.Ident name = ident();
var enumMemberDef = new Syntax.Statement.EnumMemberDef(parent, name);
enumMemberDef.start = start;
setEnd(enumMemberDef);
addStatement(enumMemberDef);

ws();
if (is('=')) {
skip(); // '='
ws();
enumMemberDef.value = parseNode();
setEnd(enumMemberDef);
}
setEnd(enumMemberDef);
} else {
addErr(position(), position(),
"unexpected token " + peekSingleCharForMessage() + " expected trait or member");
Expand Down
Loading

0 comments on commit 5ae4a5e

Please sign in to comment.