diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java index f80d26194..5e5271277 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java @@ -72,6 +72,7 @@ import io.usethesource.vallang.ISet; import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IString; +import io.usethesource.vallang.ITuple; import io.usethesource.vallang.IValue; import io.usethesource.vallang.IValueFactory; import io.usethesource.vallang.exceptions.FactTypeUseException; @@ -204,7 +205,7 @@ public InterruptibleFuture getDocumentSymbols(IConstructor module) { } - public InterruptibleFuture getRename(ITree module, Position cursor, Set workspaceFolders, Function getPathConfig, String newName, ColumnMaps columns) { + public InterruptibleFuture getRename(ITree module, Position cursor, Set workspaceFolders, Function getPathConfig, String newName, ColumnMaps columns) { var moduleLocation = TreeAdapter.getLocation(module); Position pos = Locations.toRascalPosition(moduleLocation, cursor, columns); var cursorTree = TreeAdapter.locateLexical(module, pos.getLine(), pos.getCharacter()); @@ -212,7 +213,7 @@ public InterruptibleFuture getRename(ITree module, Position cursor, Set { try { IFunction rascalGetPathConfig = eval.getFunctionValueFactory().function(getPathConfigType, (t, u) -> addResources(getPathConfig.apply((ISourceLocation) t[0]))); - return (IList) eval.call("rascalRenameSymbol", cursorTree, VF.set(workspaceFolders.toArray(ISourceLocation[]::new)), VF.string(newName), rascalGetPathConfig); + return (ITuple) eval.call("rascalRenameSymbol", cursorTree, VF.set(workspaceFolders.toArray(ISourceLocation[]::new)), VF.string(newName), rascalGetPathConfig); } catch (Throw e) { if (e.getException() instanceof IConstructor) { var exception = (IConstructor)e.getException(); @@ -231,7 +232,7 @@ public InterruptibleFuture getRename(ITree module, Position cursor, Set rename(RenameParams params) { .thenApply(Versioned::get) .handle((t, r) -> (t == null ? (file.getMostRecentTree().get()) : t)) .thenCompose(tr -> rascalServices.getRename(tr, params.getPosition(), workspaceFolders, facts::getPathConfig, params.getNewName(), columns).get()) - .thenApply(c -> new WorkspaceEdit(DocumentChanges.translateDocumentChanges(this, c))) - ; + .thenApply(t -> { + WorkspaceEdit wsEdit = new WorkspaceEdit(); + wsEdit.setDocumentChanges(DocumentChanges.translateDocumentChanges(this, (IList) t.get(0))); + wsEdit.setChangeAnnotations(DocumentChanges.translateChangeAnnotations((IMap) t.get(1))); + return wsEdit; + }); } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentChanges.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentChanges.java index d931eed21..040ed2dcf 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentChanges.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentChanges.java @@ -28,8 +28,11 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; +import org.eclipse.lsp4j.AnnotatedTextEdit; +import org.eclipse.lsp4j.ChangeAnnotation; import org.eclipse.lsp4j.CreateFile; import org.eclipse.lsp4j.DeleteFile; import org.eclipse.lsp4j.Range; @@ -43,10 +46,13 @@ import org.rascalmpl.vscode.lsp.util.locations.LineColumnOffsetMap; import org.rascalmpl.vscode.lsp.util.locations.Locations; +import io.usethesource.vallang.IBool; import io.usethesource.vallang.IConstructor; import io.usethesource.vallang.IList; +import io.usethesource.vallang.IMap; import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IString; +import io.usethesource.vallang.ITuple; import io.usethesource.vallang.IValue; /** @@ -89,8 +95,13 @@ public static List> translateDocumen private static List translateTextEdits(final IBaseTextDocumentService docService, IList edits) { return edits.stream() - .map(e -> (IConstructor) e) - .map(c -> new TextEdit(locationToRange(docService, (ISourceLocation) c.get("range")), ((IString) c.get("replacement")).getValue())) + .map(IConstructor.class::cast) + .map(c -> { + var kw = c.asWithKeywordParameters(); + return kw.hasParameter("annotation") + ? new AnnotatedTextEdit(locationToRange(docService, (ISourceLocation) c.get("range")), ((IString) c.get("replacement")).getValue(), ((IString) kw.getParameter("annotation")).getValue()) + : new TextEdit(locationToRange(docService, (ISourceLocation) c.get("range")), ((IString) c.get("replacement")).getValue()); + }) .collect(Collectors.toList()); } @@ -102,4 +113,19 @@ private static Range locationToRange(final IBaseTextDocumentService docService, private static String getFileURI(IConstructor edit, String label) { return ((ISourceLocation) edit.get(label)).getURI().toString(); } + + public static Map translateChangeAnnotations(IMap annos) { + return annos.stream() + .map(ITuple.class::cast) + .map(entry -> { + String annoId = ((IString) entry.get(0)).getValue(); + ChangeAnnotation anno = new ChangeAnnotation(); + IConstructor c = (IConstructor) entry.get(1); + anno.setLabel(((IString) c.get("label")).getValue()); + anno.setDescription(((IString) c.get("description")).getValue()); + anno.setNeedsConfirmation(((IBool) c.get("needsConfirmation")).getValue()); + return Map.entry(annoId, anno); + }) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } } diff --git a/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/Rename.rsc b/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/Rename.rsc index d40dc2859..aa0205eea 100644 --- a/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/Rename.rsc +++ b/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/Rename.rsc @@ -53,13 +53,15 @@ extend lang::rascal::lsp::refactor::Exception; import lang::rascal::lsp::refactor::Util; import lang::rascal::lsp::refactor::WorkspaceInfo; -import analysis::diff::edits::TextEdits; +import lang::rascal::lsp::refactor::TextEdits; import util::FileSystem; import util::Maybe; import util::Monitor; import util::Reflective; +alias Edits = tuple[list[DocumentEdit], map[ChangeAnnotationId, ChangeAnnotation]]; + // Rascal compiler-specific extension void throwAnyErrors(list[ModuleMessages] mmsgs) { for (mmsg <- mmsgs) { @@ -190,7 +192,7 @@ private set[IllegalRenameReason] rascalCollectIllegalRenames(WorkspaceInfo ws, s private str rascalEscapeName(str name) = name in getRascalReservedIdentifiers() ? "\\" : name; // Find the smallest trees of defined non-terminal type with a source location in `useDefs` -private set[loc] rascalFindNamesInUseDefs(start[Module] m, set[loc] useDefs) { +private map[loc, loc] rascalFindNamesInUseDefs(start[Module] m, set[loc] useDefs) { map[loc, loc] useDefNameAt = (); useDefsToDo = useDefs; visit(m.top) { @@ -206,7 +208,7 @@ private set[loc] rascalFindNamesInUseDefs(start[Module] m, set[loc] useDefs) { throw unsupportedRename("Rename unsupported", issues={."> | l <- useDefsToDo}); } - return range(useDefNameAt); + return useDefNameAt; } Maybe[loc] rascalLocationOfName(Name n) = just(n.src); @@ -227,16 +229,24 @@ Maybe[loc] rascalLocationOfName(Nonterminal nt) = just(nt.src); Maybe[loc] rascalLocationOfName(NonterminalLabel l) = just(l.src); default Maybe[loc] rascalLocationOfName(Tree t) = nothing(); -private tuple[set[IllegalRenameReason] reasons, list[TextEdit] edits] computeTextEdits(WorkspaceInfo ws, start[Module] m, set[loc] defs, set[loc] uses, str name) { - if (reasons := rascalCollectIllegalRenames(ws, m, defs, uses, name), reasons != {}) { +private tuple[set[IllegalRenameReason] reasons, list[TextEdit] edits] computeTextEdits(WorkspaceInfo ws, start[Module] m, set[RenameLocation] defs, set[RenameLocation] uses, str name) { + if (reasons := rascalCollectIllegalRenames(ws, m, defs.l, uses.l, name), reasons != {}) { return ; } replaceName = rascalEscapeName(name); - return <{}, [replace(l, replaceName) | l <- rascalFindNamesInUseDefs(m, defs + uses)]>; + + set[RenameLocation] renames = defs + uses; + set[loc] renameLocs = renames.l; + map[loc, loc] namesAt = rascalFindNamesInUseDefs(m, renameLocs); + + return <{}, [{just(annotation), *_} := renames[l] + ? replace(namesAt[l], replaceName, annotation = annotation) + : replace(namesAt[l], replaceName) + | l <- renameLocs]>; } -private tuple[set[IllegalRenameReason] reasons, list[TextEdit] edits] computeTextEdits(WorkspaceInfo ws, loc moduleLoc, set[loc] defs, set[loc] uses, str name) = +private tuple[set[IllegalRenameReason] reasons, list[TextEdit] edits] computeTextEdits(WorkspaceInfo ws, loc moduleLoc, set[RenameLocation] defs, set[RenameLocation] uses, str name) = computeTextEdits(ws, parseModuleWithSpacesCached(moduleLoc), defs, uses, name); private bool rascalIsFunctionLocalDefs(WorkspaceInfo ws, set[loc] defs) { @@ -382,7 +392,7 @@ private Cursor rascalGetCursor(WorkspaceInfo ws, Tree cursorT) { rel[loc field, loc container] fields = { | /Tree t := parseModuleWithSpacesCached(cursorLoc.top) - , just() := rascalGetFieldLocs(cursorName, t) + , just() := rascalGetFieldLocs(cursorName, t) || just() := rascalGetHasLocs(cursorName, t) , loc fieldLoc <- fieldLocs }; @@ -508,8 +518,8 @@ private bool rascalContainsName(loc l, str name) { 2. It does not change the semantics of the application. 3. It does not change definitions outside of the current workspace. } -list[DocumentEdit] rascalRenameSymbol(Tree cursorT, set[loc] workspaceFolders, str newName, PathConfig(loc) getPathConfig) - = job("renaming to ", list[DocumentEdit](void(str, int) step) { +Edits rascalRenameSymbol(Tree cursorT, set[loc] workspaceFolders, str newName, PathConfig(loc) getPathConfig) + = job("renaming to ", Edits(void(str, int) step) { loc cursorLoc = cursorT.src; str cursorName = ""; @@ -572,14 +582,34 @@ list[DocumentEdit] rascalRenameSymbol(Tree cursorT, set[loc] workspaceFolders, s } step("collecting uses of \'\'", 1); - = rascalGetDefsUses(ws, cur, rascalMayOverloadSameName); - rel[loc file, loc defines] defsPerFile = { | d <- defs}; - rel[loc file, loc uses] usesPerFile = { | u <- uses}; + map[ChangeAnnotationId, ChangeAnnotation] changeAnnotations = (); + ChangeAnnotationRegister registerChangeAnnotation = ChangeAnnotationId(str label, str description, bool needsConfirmation) { + ChangeAnnotationId makeKey(str label, int suffix) = "