diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java index e40db703e..860fc3724 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java @@ -41,7 +41,10 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; -import java.util.function.Supplier; +import java.util.concurrent.ExecutorService; +import java.util.function.BiFunction; +import java.util.function.Function; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.Nullable; @@ -149,17 +152,23 @@ public void write(JsonWriter target, IValue value) throws IOException { } @SuppressWarnings({"java:S2189", "java:S106"}) - public static void startLanguageServer(Supplier service, int portNumber) { + public static void startLanguageServer(ExecutorService threadPool, Function docServiceProvider, BiFunction workspaceServiceProvider, int portNumber) { logger.info("Starting Rascal Language Server: {}", getVersion()); if (DEPLOY_MODE) { - startLSP(constructLSPClient(capturedIn, capturedOut, new ActualLanguageServer(() -> System.exit(0), service.get()))); + var docService = docServiceProvider.apply(threadPool); + var wsService = workspaceServiceProvider.apply(threadPool, docService); + docService.pair(wsService); + startLSP(constructLSPClient(capturedIn, capturedOut, new ActualLanguageServer(() -> System.exit(0), docService, wsService))); } else { try (ServerSocket serverSocket = new ServerSocket(portNumber, 0, InetAddress.getByName("127.0.0.1"))) { logger.info("Rascal LSP server listens on port number: {}", portNumber); while (true) { - startLSP(constructLSPClient(serverSocket.accept(), new ActualLanguageServer(() -> {}, service.get()))); + var docService = docServiceProvider.apply(threadPool); + var wsService = workspaceServiceProvider.apply(threadPool, docService); + docService.pair(wsService); + startLSP(constructLSPClient(serverSocket.accept(), new ActualLanguageServer(() -> {}, docService, wsService))); } } catch (IOException e) { logger.fatal("Failure to start TCP server", e); @@ -206,10 +215,10 @@ private static class ActualLanguageServer implements IBaseLanguageServerExtensi private final Runnable onExit; private IDEServicesConfiguration ideServicesConfiguration; - private ActualLanguageServer(Runnable onExit, IBaseTextDocumentService lspDocumentService) { + private ActualLanguageServer(Runnable onExit, IBaseTextDocumentService lspDocumentService, BaseWorkspaceService lspWorkspaceService) { this.onExit = onExit; this.lspDocumentService = lspDocumentService; - this.lspWorkspaceService = new BaseWorkspaceService(lspDocumentService); + this.lspWorkspaceService = lspWorkspaceService; reg.registerLogical(new ProjectURIResolver(this::resolveProjectLocation)); reg.registerLogical(new TargetURIResolver(this::resolveProjectLocation)); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseWorkspaceService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseWorkspaceService.java index f9b9e3fa9..85537d1dd 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseWorkspaceService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseWorkspaceService.java @@ -30,6 +30,8 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; + import com.google.gson.JsonPrimitive; import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.lsp4j.ClientCapabilities; @@ -50,13 +52,15 @@ public class BaseWorkspaceService implements WorkspaceService, LanguageClientAwa public static final String RASCAL_META_COMMAND = "rascal-meta-command"; public static final String RASCAL_COMMAND = "rascal-command"; + private final ExecutorService ownExecuter; + private final IBaseTextDocumentService documentService; private final CopyOnWriteArrayList workspaceFolders = new CopyOnWriteArrayList<>(); - BaseWorkspaceService(IBaseTextDocumentService documentService) { + protected BaseWorkspaceService(ExecutorService exec, IBaseTextDocumentService documentService) { this.documentService = documentService; - documentService.pair(this); + this.ownExecuter = exec; } @@ -120,6 +124,9 @@ public CompletableFuture executeCommand(ExecuteCommandParams params) { } + protected final ExecutorService getExecuter() { + return ownExecuter; + } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java index 2615b4a0a..443e28924 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java @@ -26,12 +26,16 @@ */ package org.rascalmpl.vscode.lsp; +import java.util.Set; import java.util.concurrent.CompletableFuture; + +import org.eclipse.lsp4j.RenameFilesParams; import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.TextDocumentService; import org.rascalmpl.vscode.lsp.terminal.ITerminalIDEServer.LanguageParameter; import org.rascalmpl.vscode.lsp.util.locations.LineColumnOffsetMap; + import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IValue; @@ -45,4 +49,5 @@ public interface IBaseTextDocumentService extends TextDocumentService { CompletableFuture executeCommand(String languageName, String command); LineColumnOffsetMap getColumnMap(ISourceLocation file); + default void didRenameFiles(RenameFilesParams params, Set workspaceFolders) {} } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java index 89a2ec9c3..8466247ca 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricLanguageServer.java @@ -26,11 +26,11 @@ */ package org.rascalmpl.vscode.lsp.parametric; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.rascalmpl.vscode.lsp.BaseLanguageServer; import org.rascalmpl.vscode.lsp.terminal.ITerminalIDEServer.LanguageParameter; + import com.google.gson.GsonBuilder; public class ParametricLanguageServer extends BaseLanguageServer { @@ -43,9 +43,10 @@ public static void main(String[] args) { dedicatedLanguage = null; } - startLanguageServer(() -> { - ExecutorService threadPool = Executors.newCachedThreadPool(); - return new ParametricTextDocumentService(threadPool, dedicatedLanguage); - }, 9999); + startLanguageServer(Executors.newCachedThreadPool() + , threadPool -> new ParametricTextDocumentService(threadPool, dedicatedLanguage) + , ParametricWorkspaceService::new + , 9999 + ); } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java index 3c3efa605..7c7bbb648 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java @@ -167,7 +167,7 @@ public LineColumnOffsetMap getColumnMap(ISourceLocation file) { return columns.get(file); } - private String getContents(ISourceLocation file) { + public String getContents(ISourceLocation file) { file = file.top(); TextDocumentState ideState = files.get(file); if (ideState != null) { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricWorkspaceService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricWorkspaceService.java new file mode 100644 index 000000000..3105f6883 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricWorkspaceService.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2018-2023, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.vscode.lsp.parametric; + +import java.util.concurrent.ExecutorService; + +import org.rascalmpl.vscode.lsp.BaseWorkspaceService; +import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; + +public class ParametricWorkspaceService extends BaseWorkspaceService { + ParametricWorkspaceService(ExecutorService exec, IBaseTextDocumentService docService) { + super(exec, docService); + } +} diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServer.java index 6792885ab..fdc327d55 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServer.java @@ -26,7 +26,6 @@ */ package org.rascalmpl.vscode.lsp.rascal; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.apache.logging.log4j.LogManager; @@ -36,10 +35,7 @@ public class RascalLanguageServer extends BaseLanguageServer { public static void main(String[] args) { try { - startLanguageServer(() -> { - ExecutorService threadPool = Executors.newCachedThreadPool(); - return new RascalTextDocumentService(threadPool); - }, 8888); + startLanguageServer(Executors.newCachedThreadPool(), RascalTextDocumentService::new, RascalWorkspaceService::new, 8888); } catch (Throwable e) { final Logger logger = LogManager.getLogger(RascalLanguageServer.class); 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 5e5271277..a9c3516aa 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 @@ -30,22 +30,29 @@ import static org.rascalmpl.vscode.lsp.util.EvaluatorUtil.runEvaluator; import java.io.IOException; +import java.io.Reader; import java.io.StringReader; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.function.Function; import java.util.stream.Collectors; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.FileRename; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; @@ -54,6 +61,8 @@ import org.rascalmpl.interpreter.Evaluator; import org.rascalmpl.interpreter.env.ModuleEnvironment; import org.rascalmpl.library.util.PathConfig; +import org.rascalmpl.uri.URIResolverRegistry; +import org.rascalmpl.uri.URIUtil; import org.rascalmpl.values.IRascalValueFactory; import org.rascalmpl.values.functions.IFunction; import org.rascalmpl.values.parsetrees.ITree; @@ -61,8 +70,10 @@ import org.rascalmpl.vscode.lsp.BaseWorkspaceService; import org.rascalmpl.vscode.lsp.IBaseLanguageClient; import org.rascalmpl.vscode.lsp.RascalLSPMonitor; +import org.rascalmpl.vscode.lsp.TextDocumentState; import org.rascalmpl.vscode.lsp.util.EvaluatorUtil; import org.rascalmpl.vscode.lsp.util.RascalServices; +import org.rascalmpl.vscode.lsp.util.Versioned; import org.rascalmpl.vscode.lsp.util.concurrent.InterruptibleFuture; import org.rascalmpl.vscode.lsp.util.locations.ColumnMaps; import org.rascalmpl.vscode.lsp.util.locations.Locations; @@ -235,6 +246,84 @@ public InterruptibleFuture getRename(ITree module, Position cursor, Set< }, VF.tuple(VF.list(), VF.map()), exec, false, client); } + private ISourceLocation sourceLocationFromUri(String uri) { + try { + return URIUtil.createFromURI(uri); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + private Optional findContainingWorkspaceFolder(ISourceLocation loc, List workspaceFolders) { + return workspaceFolders.stream() + .filter(folderLoc -> URIUtil.isParentOf(folderLoc, loc)) + .findFirst(); + } + + private ISet qualfiedNameChangesFromRenames(List renames, Set workspaceFolders, Function getPathConfig) { + // Sort workspace folders so we get the most specific folders first + List sortedWorkspaceFolders = workspaceFolders.stream() + .sorted((o1, o2) -> o1.toString().compareTo(o2.toString())) + .collect(Collectors.toList()); + + return renames.parallelStream() + .map(rename -> { + ISourceLocation currentLoc = sourceLocationFromUri(rename.getOldUri()); + ISourceLocation newLoc = sourceLocationFromUri(rename.getNewUri()); + + ISourceLocation currentWsFolder = findContainingWorkspaceFolder(currentLoc, sortedWorkspaceFolders) + .orElseThrow(() -> new RuntimeException(String.format("Cannot automatically change uses of %s, since it is outside the current workspace.", currentLoc))); + + ISourceLocation newWsFolder = findContainingWorkspaceFolder(newLoc, sortedWorkspaceFolders) + .orElseThrow(() -> new RuntimeException(String.format("Cannot automatically change uses of %s, since it is outside the current workspace.", newLoc))); + + if (!currentWsFolder.equals(newWsFolder)) { + String commonProjPrefix = StringUtils.getCommonPrefix(currentWsFolder.toString(), newWsFolder.toString()); + String currentProject = StringUtils.removeStart(currentWsFolder.toString(), commonProjPrefix); + String newProject = StringUtils.removeStart(newWsFolder.toString(), commonProjPrefix); + + throw new RuntimeException(String.format("Cannot automatically change uses of %s, since moving files between projects (from %s to %s) is not supported", currentLoc, currentProject, newProject)); + } + + PathConfig pcfg = getPathConfig.apply(currentWsFolder); + try { + IString currentName = VF.string(pcfg.getModuleName(currentLoc)); + IString newName = VF.string(pcfg.getModuleName(newLoc)); + + return VF.tuple(currentName, newName, addResources(pcfg)); + } catch (IOException e) { + throw new RuntimeException(e.getMessage()); + } + }) + .collect(VF.setWriter()); + } + + private static String readFile(ISourceLocation loc) { + URIResolverRegistry reg = URIResolverRegistry.getInstance(); + try (Reader reader = reg.getCharacterReader(loc)) { + return IOUtils.toString(reader); + } catch (IOException e) { + throw new RuntimeException(String.format("Error reading file %s", loc)); + } + } + + public InterruptibleFuture getModuleRenames(List fileRenames, Set workspaceFolders, Function getPathConfig, Map documents) { + var emptyResult = VF.tuple(VF.list(), VF.map()); + if (fileRenames.isEmpty()) { + return InterruptibleFuture.completedFuture(emptyResult); + } + + return runEvaluator("Rascal module rename", semanticEvaluator, eval -> { + IFunction rascalGetPathConfig = eval.getFunctionValueFactory().function(getPathConfigType, (t, u) -> addResources(getPathConfig.apply((ISourceLocation) t[0]))); + ISet qualifiedNameChanges = qualfiedNameChangesFromRenames(fileRenames, workspaceFolders, getPathConfig); + try { + return (ITuple) eval.call("rascalRenameModule", qualifiedNameChanges, VF.set(workspaceFolders.toArray(ISourceLocation[]::new)), rascalGetPathConfig); + } catch (Throw e) { + throw new RuntimeException(e.getMessage()); + } + }, emptyResult, exec, false, client); + } + public CompletableFuture parseSourceFile(ISourceLocation loc, String input) { return CompletableFuture.supplyAsync(() -> RascalServices.parseRascalModule(loc, input.toCharArray()), exec); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java index 14e4eb71a..27a947d8d 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java @@ -44,6 +44,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.eclipse.lsp4j.ApplyWorkspaceEditParams; import org.eclipse.lsp4j.CodeAction; import org.eclipse.lsp4j.CodeActionParams; import org.eclipse.lsp4j.CodeLens; @@ -67,8 +68,15 @@ import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.LocationLink; import org.eclipse.lsp4j.MarkupContent; +import org.eclipse.lsp4j.MessageParams; +import org.eclipse.lsp4j.MessageType; import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.PrepareRenameDefaultBehavior; +import org.eclipse.lsp4j.PrepareRenameParams; +import org.eclipse.lsp4j.PrepareRenameResult; import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.RenameFilesParams; +import org.eclipse.lsp4j.RenameOptions; import org.eclipse.lsp4j.RenameParams; import org.eclipse.lsp4j.SemanticTokens; import org.eclipse.lsp4j.SemanticTokensDelta; @@ -84,6 +92,7 @@ import org.eclipse.lsp4j.WorkspaceEdit; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.lsp4j.jsonrpc.messages.Either3; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; import org.eclipse.lsp4j.services.LanguageClient; @@ -93,6 +102,8 @@ import org.rascalmpl.parser.gtd.exception.ParseError; import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.values.parsetrees.ITree; +import org.rascalmpl.values.parsetrees.ProductionAdapter; +import org.rascalmpl.values.parsetrees.TreeAdapter; import org.rascalmpl.vscode.lsp.BaseWorkspaceService; import org.rascalmpl.vscode.lsp.IBaseLanguageClient; import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; @@ -114,7 +125,6 @@ import org.rascalmpl.vscode.lsp.util.locations.impl.TreeSearch; import io.usethesource.vallang.IList; -import io.usethesource.vallang.IMap; import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IValue; @@ -142,7 +152,7 @@ public LineColumnOffsetMap getColumnMap(ISourceLocation file) { return columns.get(file); } - private String getContents(ISourceLocation file) { + public String getContents(ISourceLocation file) { file = file.top(); TextDocumentState ideState = documents.get(file); if (ideState != null) { @@ -166,9 +176,9 @@ public void initializeServerCapabilities(ServerCapabilities result) { result.setSemanticTokensProvider(tokenizer.options()); result.setCodeLensProvider(new CodeLensOptions(false)); result.setFoldingRangeProvider(true); - result.setRenameProvider(true); + result.setRenameProvider(new RenameOptions(true)); result.setCodeActionProvider(true); - result.setExecuteCommandProvider(new ExecuteCommandOptions(Collections.singletonList(BaseWorkspaceService.RASCAL_COMMAND))); + result.setExecuteCommandProvider(new ExecuteCommandOptions(Collections.singletonList(RascalWorkspaceService.RASCAL_COMMAND))); } @Override @@ -285,6 +295,52 @@ public CompletableFuture, List> prepareRename(PrepareRenameParams params) { + logger.debug("textDocument/prepareRename: {} at {}", params.getTextDocument(), params.getPosition()); + TextDocumentState file = getFile(params.getTextDocument()); + + return file.getCurrentTreeAsync() + .thenApply(Versioned::get) + .handle((t, r) -> (t == null ? file.getMostRecentTree().get() : t)) + .thenApply(tr -> { + Position rascalCursorPos = Locations.toRascalPosition(file.getLocation(), params.getPosition(), columns); + + // Find all trees containing the cursor, in ascending order of size + IList focusList = TreeSearch.computeFocusList(tr, rascalCursorPos.getLine(), rascalCursorPos.getCharacter()); + List sortNames = focusList.stream() + .map(tree -> ProductionAdapter.getSortName(TreeAdapter.getProduction((ITree) tree))) + .collect(Collectors.toList()); + + int qNameIdx = sortNames.indexOf("QualifiedName"); + if (qNameIdx != -1) { + // Cursor is at a qualified name + ITree qualifiedName = (ITree) focusList.get(qNameIdx); + + // If the qualified name is in a header, but not in module parameters or a syntax defintion, it is a full module path + if (sortNames.contains("Header") && !(sortNames.contains("ModuleParameters") || sortNames.contains("SyntaxDefinition"))) { + return Either3.forLeft3(DocumentChanges.locationToRange(this, TreeAdapter.getLocation(qualifiedName))); + } + + // Since the cursor is not in a header, the qualified name consists of a declaration name on the right, and an optional module path prefix. + IList names = TreeAdapter.getListASTArgs(TreeAdapter.getArg(qualifiedName, "names")); + + // Even if the cursor is on the module prefix, we steer towards renaming the declaration + return Either3.forLeft3(DocumentChanges.locationToRange(this, TreeAdapter.getLocation((ITree) names.get(names.size() - 1)))); + } + + switch (sortNames.get(0)) { + case "Name": // intentional fall-through + case "Nonterminal": // intentional fall-through + case "NonterminalLabel": { + // Return name location + return Either3.forLeft3(DocumentChanges.locationToRange(this, TreeAdapter.getLocation((ITree) focusList.get(0)))); + } + default: return null; + } + }); + } + @Override public CompletableFuture rename(RenameParams params) { logger.debug("textDocument/rename: {} at {} to {}", params.getTextDocument(), params.getPosition(), params.getNewName()); @@ -299,12 +355,7 @@ public CompletableFuture 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(t -> { - WorkspaceEdit wsEdit = new WorkspaceEdit(); - wsEdit.setDocumentChanges(DocumentChanges.translateDocumentChanges(this, (IList) t.get(0))); - wsEdit.setChangeAnnotations(DocumentChanges.translateChangeAnnotations((IMap) t.get(1))); - return wsEdit; - }); + .thenApply(t -> DocumentChanges.translateDocumentChanges(this, t)); } @Override @@ -335,6 +386,24 @@ public CompletableFuture> foldingRange(FoldingRangeRequestPar ); } + @Override + public void didRenameFiles(RenameFilesParams params, Set workspaceFolders) { + logger.debug("workspace/didRenameFiles: {}", params.getFiles()); + + rascalServices.getModuleRenames(params.getFiles(), workspaceFolders, facts::getPathConfig, documents).get() + .thenApply(edits -> DocumentChanges.translateDocumentChanges(this, edits)) + .thenCompose(docChanges -> client.applyEdit(new ApplyWorkspaceEditParams(docChanges))) + .thenAccept(editResponse -> { + if (!editResponse.isApplied()) { + throw new RuntimeException("Applying module rename failed: " + editResponse.getFailureReason()); + } + }) + .exceptionally(e -> { + client.showMessage(new MessageParams(MessageType.Error, e.getCause().getMessage())); + return null; + }); + } + // Private utility methods private static T last(List l) { @@ -350,7 +419,7 @@ private TextDocumentState getFile(TextDocumentIdentifier doc) { return getFile(Locations.toLoc(doc)); } - private TextDocumentState getFile(ISourceLocation loc) { + protected TextDocumentState getFile(ISourceLocation loc) { TextDocumentState file = documents.get(loc); if (file == null) { throw new ResponseErrorException(new ResponseError(-1, "Unknown file: " + loc, loc)); @@ -458,7 +527,7 @@ public CompletableFuture>> codeAction(CodeActio ; // final merging the two streams of commmands, and their conversion to LSP Command data-type - return CodeActions.mergeAndConvertCodeActions(this, "", BaseWorkspaceService.RASCAL_LANGUAGE, quickfixes, codeActions); + return CodeActions.mergeAndConvertCodeActions(this, "", RascalWorkspaceService.RASCAL_LANGUAGE, quickfixes, codeActions); } private CompletableFuture computeCodeActions(final int startLine, final int startColumn, ITree tree, PathConfig pcfg) { @@ -488,4 +557,8 @@ private static CompletableFuture recoverExceptions(CompletableFuture f return defaultValue.get(); }); } + + public @MonotonicNonNull FileFacts getFileFacts() { + return facts; + } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalWorkspaceService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalWorkspaceService.java new file mode 100644 index 000000000..3937aef88 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalWorkspaceService.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2018-2023, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.vscode.lsp.rascal; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.ClientCapabilities; +import org.eclipse.lsp4j.FileOperationFilter; +import org.eclipse.lsp4j.FileOperationOptions; +import org.eclipse.lsp4j.FileOperationPattern; +import org.eclipse.lsp4j.FileOperationsServerCapabilities; +import org.eclipse.lsp4j.RenameFilesParams; +import org.eclipse.lsp4j.ServerCapabilities; +import org.eclipse.lsp4j.WorkspaceFolder; +import org.eclipse.lsp4j.services.LanguageClient; +import org.rascalmpl.vscode.lsp.BaseWorkspaceService; +import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; +import org.rascalmpl.vscode.lsp.util.locations.Locations; + +public class RascalWorkspaceService extends BaseWorkspaceService { + private static final Logger logger = LogManager.getLogger(RascalWorkspaceService.class); + + private final IBaseTextDocumentService docService; + private @MonotonicNonNull LanguageClient client; + + RascalWorkspaceService(ExecutorService exec, IBaseTextDocumentService documentService) { + super(exec, documentService); + this.docService = documentService; + } + + @Override + public void initialize(ClientCapabilities clientCap, @Nullable List currentWorkspaceFolders, + ServerCapabilities capabilities) { + super.initialize(clientCap, currentWorkspaceFolders, capabilities); + + var fileCap = new FileOperationsServerCapabilities(); + fileCap.setDidRename(new FileOperationOptions( + List.of(new FileOperationFilter(new FileOperationPattern("**/*.rsc"))) + )); + + capabilities.getWorkspace().setFileOperations(fileCap); + } + + @Override + public void connect(LanguageClient client) { + super.connect(client); + this.client = client; + } + + @Override + public void didRenameFiles(RenameFilesParams params) { + logger.debug("workspace/didRenameFiles: {}", params.getFiles()); + + CompletableFuture.supplyAsync(() -> workspaceFolders() + .stream() + .map(f -> Locations.toLoc(f.getUri())) + .collect(Collectors.toSet()), getExecuter()) + .thenAccept(folders -> ((RascalTextDocumentService) docService).didRenameFiles(params, folders)); + } +} 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 040ed2dcf..d90a5b0b6 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 @@ -41,6 +41,7 @@ import org.eclipse.lsp4j.TextDocumentEdit; import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; +import org.eclipse.lsp4j.WorkspaceEdit; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.rascalmpl.vscode.lsp.IBaseTextDocumentService; import org.rascalmpl.vscode.lsp.util.locations.LineColumnOffsetMap; @@ -64,6 +65,13 @@ public class DocumentChanges { private DocumentChanges() { } + public static WorkspaceEdit translateDocumentChanges(final IBaseTextDocumentService docService, ITuple edits) { + WorkspaceEdit wsEdit = new WorkspaceEdit(); + wsEdit.setDocumentChanges(DocumentChanges.translateDocumentChanges(docService, (IList) edits.get(0))); + wsEdit.setChangeAnnotations(DocumentChanges.translateChangeAnnotations((IMap) edits.get(1))); + return wsEdit; + } + public static List> translateDocumentChanges(final IBaseTextDocumentService docService, IList list) { List> result = new ArrayList<>(list.size()); @@ -105,7 +113,7 @@ private static List translateTextEdits(final IBaseTextDocumentService .collect(Collectors.toList()); } - private static Range locationToRange(final IBaseTextDocumentService docService, ISourceLocation loc) { + public static Range locationToRange(final IBaseTextDocumentService docService, ISourceLocation loc) { LineColumnOffsetMap columnMap = docService.getColumnMap(loc); return Locations.toRange(loc, columnMap); } @@ -114,7 +122,7 @@ private static String getFileURI(IConstructor edit, String label) { return ((ISourceLocation) edit.get(label)).getURI().toString(); } - public static Map translateChangeAnnotations(IMap annos) { + private static Map translateChangeAnnotations(IMap annos) { return annos.stream() .map(ITuple.class::cast) .map(entry -> { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/locations/Locations.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/locations/Locations.java index da1a42627..00b50aadd 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/locations/Locations.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/locations/Locations.java @@ -125,6 +125,10 @@ public static Location toLSPLocation(ISourceLocation sloc, ColumnMaps cm) { return new Location(sloc.getURI().toString(), toRange(sloc, cm)); } + public static Location toLSPLocation(ISourceLocation sloc, LineColumnOffsetMap map) { + return new Location(sloc.getURI().toString(), toRange(sloc, map)); + } + public static Range toRange(ISourceLocation sloc, ColumnMaps cm) { return toRange(sloc, cm.get(sloc)); } 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 5b56660b8..d12ce9c9c 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 @@ -50,6 +50,8 @@ import lang::rascal::\syntax::Rascal; import lang::rascalcore::check::Checker; +import lang::rascal::lsp::refactor::rename::Modules; + extend lang::rascal::lsp::refactor::Exception; import lang::rascal::lsp::refactor::Util; import lang::rascal::lsp::refactor::WorkspaceInfo; @@ -57,12 +59,11 @@ import lang::rascal::lsp::refactor::WorkspaceInfo; import lang::rascal::lsp::refactor::TextEdits; import util::FileSystem; +import util::LanguageServer; import util::Maybe; import util::Monitor; import util::Reflective; -alias Edits = tuple[list[DocumentEdit], map[ChangeAnnotationId, ChangeAnnotation]]; - private str MANDATORY_CHANGE_DESCRIPTION = "These changes are required for a correct renaming. They can be previewed here, but it is not advised to disable them."; // Rascal compiler-specific extension @@ -128,13 +129,14 @@ private set[IllegalRenameReason] rascalCheckCausesDoubleDeclarations(WorkspaceIn return {doubleDeclaration(old, doubleDeclarations[old]) | old <- (doubleDeclarations + doubleFieldDeclarations + doubleTypeParamDeclarations).old}; } -private set[IllegalRenameReason] rascalCheckCausesCaptures(WorkspaceInfo ws, start[Module] m, set[loc] currentDefs, set[loc] currentUses, set[Define] newDefs) { +private set[IllegalRenameReason] rascalCheckCausesCaptures(WorkspaceInfo ws, loc moduleLoc, set[loc] currentDefs, set[loc] currentUses, set[Define] newDefs) { set[Define] rascalFindImplicitDefinitions(WorkspaceInfo ws, start[Module] m, set[Define] newDefs) { - set[loc] maybeImplicitDefs = {l | /QualifiedName n := m, just(l) := rascalLocationOfName(n)}; + set[loc] maybeImplicitDefs = {n.names[-1].src | /QualifiedName n := m}; return {def | Define def <- newDefs, (def.idRole is variableId && def.defined in ws.useDef<0>) || (def.idRole is patternVariableId && def.defined in maybeImplicitDefs)}; } + start[Module] m = parseModuleWithSpacesCached(moduleLoc); set[Define] newNameImplicitDefs = rascalFindImplicitDefinitions(ws, m, newDefs); // Will this rename turn an implicit declaration of `newName` into a use of a current declaration? @@ -170,28 +172,38 @@ private set[IllegalRenameReason] rascalCheckCausesCaptures(WorkspaceInfo ws, sta return allCaptures == {} ? {} : {captureChange(allCaptures)}; } -private set[IllegalRenameReason] rascalCollectIllegalRenames(WorkspaceInfo ws, start[Module] m, set[loc] currentDefs, set[loc] currentUses, str newName) { +private set[IllegalRenameReason] rascalCollectIllegalRenames(WorkspaceInfo ws, rel[loc file, RenameLocation rename] defsPerFile, rel[loc file, RenameLocation rename] usesPerFile, str newName) { set[Define] newNameDefs = {def | Define def:<_, newName, _, _, _, _> <- ws.defines}; - - return - rascalCheckLegalName(newName, definitionsRel(ws)[currentDefs].idRole) - + rascalCheckDefinitionsOutsideWorkspace(ws, currentDefs) - + rascalCheckCausesDoubleDeclarations(ws, currentDefs, newNameDefs, newName) - + rascalCheckCausesCaptures(ws, m, currentDefs, currentUses, newNameDefs) - ; + set[loc] editFiles = defsPerFile.file + usesPerFile.file; + + set[IllegalRenameReason] reasons = {}; + reasons += rascalCheckLegalName(newName, definitionsRel(ws)[defsPerFile.rename.l].idRole); + reasons += rascalCheckDefinitionsOutsideWorkspace(ws, defsPerFile.rename.l); + reasons += rascalCheckCausesDoubleDeclarations(ws, defsPerFile.rename.l, newNameDefs, newName); + for (file <- editFiles) { + reasons += rascalCheckCausesCaptures(ws, file, defsPerFile[file].l, usesPerFile[file].l, newNameDefs); + } + return reasons; } -private str rascalEscapeName(str name) = name in getRascalReservedIdentifiers() ? "\\" : name; +@memo{maximumSize(1000), expireAfter(minutes=5)} +private str rascalEscapeName(str name) = intercalate("::", [n in getRascalReservedIdentifiers() ? "\\" : n | n <- split("::", name)]); + +private str rascalUnescapeName(str name) = replaceAll(name, "\\", ""); // Find the smallest trees of defined non-terminal type with a source location in `useDefs` -private rel[loc, loc] rascalFindNamesInUseDefs(start[Module] m, set[loc] useDefs) { +private rel[loc name, loc useDef] rascalFindNamesInUseDefs(start[Module] m, set[loc] useDefs, CursorKind cursorKind) { rel[loc, loc] nameOfUseDef = {}; useDefsToDo = useDefs; + visit(m.top) { case t: appl(prod(_, _, _), _): { - if (t.src in useDefsToDo && just(nameLoc) := rascalLocationOfName(t)) { - nameOfUseDef += ; - useDefsToDo -= t.src; + if (t.src in useDefsToDo) { + focus = computeFocusList(m, t.src.begin.line, t.src.begin.column); + if (just(nameLoc) := rascalLocationOfName(t, cursorKind, focus)) { + nameOfUseDef += ; + useDefsToDo -= t.src; + } } } } @@ -203,50 +215,78 @@ private rel[loc, loc] rascalFindNamesInUseDefs(start[Module] m, set[loc] useDefs return nameOfUseDef; } -Maybe[loc] rascalLocationOfName(Name n) = just(n.src); -Maybe[loc] rascalLocationOfName(QualifiedName qn) = just((qn.names[-1]).src); -Maybe[loc] rascalLocationOfName(FunctionDeclaration f) = just(f.signature.name.src); -Maybe[loc] rascalLocationOfName(Variable v) = just(v.name.src); -Maybe[loc] rascalLocationOfName(KeywordFormal kw) = just(kw.name.src); -Maybe[loc] rascalLocationOfName(Declaration d) = just(d.name.src) when d is annotation - || d is \tag; -Maybe[loc] rascalLocationOfName(Declaration d) = rascalLocationOfName(d.user.name) when d is \alias - || d is dataAbstract - || d is \data; -Maybe[loc] rascalLocationOfName(TypeVar tv) = just(tv.name.src); -Maybe[loc] rascalLocationOfName(Header h) = rascalLocationOfName(h.name); -Maybe[loc] rascalLocationOfName(SyntaxDefinition sd) = rascalLocationOfName(sd.defined); -Maybe[loc] rascalLocationOfName(Sym sym) = just(sym.nonterminal.src); -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[RenameLocation] defs, set[RenameLocation] uses, str name, ChangeAnnotationRegister registerChangeAnnotation) { - if (reasons := rascalCollectIllegalRenames(ws, m, defs.l, uses.l, name), reasons != {}) { - return ; +bool isModuleNameInFocus([*_, QualifiedName _, Header _, *_]) = true; // module name declaration +bool isModuleNameInFocus([*_, QualifiedName _, ImportedModule _, Import _, _, Header _, *_]) = true; // module import/extend +bool isModuleNameInFocus([*_, QualifiedName _, Import _, _, Header _, *_]) = true; // external module import +default bool isModuleNameInFocus(Focus _) = false; + +/* + QualifiedName: + - In header + - When cursor kind == moduleName(), return full location + - When cursor kind != moduleName(), return nothing + - Everywhere else + - When cursor kind == moduleName() (and >1 name segment), return prefix location + - When cursor kind != moduleName(), return last location + */ +Maybe[loc] rascalLocationOfName(QualifiedName qn, CursorKind cursorKind, Focus focus) { + if (isModuleNameInFocus(focus)) { + if (cursorKind is moduleName) { + // Full module name + return just(qn.src); + } + } else if (cursorKind == moduleName() && size([n | n <- qn.names]) > 1) { + // Only module prefix + return just(cover(prefix([n.src | n <- qn.names]))); + } else if (cursorKind != moduleName()) { + // Only definition name + return just(qn.names[-1].src); } - replaceName = rascalEscapeName(name); - + fail; +} +Maybe[loc] rascalLocationOfName(Name n, CursorKind _, Focus _) = just(n.src); +Maybe[loc] rascalLocationOfName(FunctionDeclaration f, CursorKind _, Focus _) = just(f.signature.name.src); +Maybe[loc] rascalLocationOfName(Variable v, CursorKind _, Focus _) = just(v.name.src); +Maybe[loc] rascalLocationOfName(KeywordFormal kw, CursorKind _, Focus _) = just(kw.name.src); +Maybe[loc] rascalLocationOfName(Declaration d, CursorKind _, Focus _) = just(d.name.src) when d is annotation + || d is \tag; +Maybe[loc] rascalLocationOfName(Declaration d, CursorKind cursorKind, Focus focus) = + rascalLocationOfName(d.user.name, cursorKind, focus) when d is \alias + || d is dataAbstract + || d is \data; +Maybe[loc] rascalLocationOfName(TypeVar tv, CursorKind _, Focus _) = just(tv.name.src); +Maybe[loc] rascalLocationOfName(Header h, CursorKind cursorKind, Focus focus) = rascalLocationOfName(h.name, cursorKind, focus); +Maybe[loc] rascalLocationOfName(UserType ut, CursorKind cursorKind, Focus focus) = rascalLocationOfName(ut.name, cursorKind, focus); +Maybe[loc] rascalLocationOfName(Module m, CursorKind cursorKind, Focus focus) = just(m.header.name.src); +Maybe[loc] rascalLocationOfName(SyntaxDefinition sd, CursorKind cursorKind, Focus focus) = rascalLocationOfName(sd.defined, cursorKind, focus); +Maybe[loc] rascalLocationOfName(Sym sym, CursorKind _, Focus _) = just(sym.nonterminal.src); +Maybe[loc] rascalLocationOfName(Nonterminal nt, CursorKind _, Focus _) = just(nt.src); +Maybe[loc] rascalLocationOfName(NonterminalLabel l, CursorKind _, Focus _) = just(l.src); +default Maybe[loc] rascalLocationOfName(Tree t, CursorKind _, Focus _) = nothing(); + +private list[TextEdit] computeTextEdits(WorkspaceInfo ws, start[Module] m, set[RenameLocation] defs, set[RenameLocation] uses, cursor(cursorKind, _, _), str newName, ChangeAnnotationRegister registerChangeAnnotation) { rel[loc l, Maybe[ChangeAnnotationId] ann, bool isDef] renames = { | <- defs} + { | <- uses}; - rel[loc name, loc useDef] nameOfUseDef = rascalFindNamesInUseDefs(m, renames.l); + rel[loc name, loc useDef] nameOfUseDef = rascalFindNamesInUseDefs(m, renames.l, cursorKind); ChangeAnnotationId defAnno = registerChangeAnnotation("Definitions", MANDATORY_CHANGE_DESCRIPTION, false); ChangeAnnotationId useAnno = registerChangeAnnotation("References", MANDATORY_CHANGE_DESCRIPTION, false); // Note: if the implementer of the rename logic has attached annotations to multiple rename suggestions that have the same // name location, one will be arbitrarily chosen here. This could mean that a `needsConfirmation` annotation is thrown away. - return <{}, [{just(annotation), *_} := renameOpts.ann - ? replace(l, replaceName, annotation = annotation) - : replace(l, replaceName, annotation = any(b <- renameOpts.isDef) ? defAnno : useAnno) - | l <- nameOfUseDef.name - , rel[Maybe[ChangeAnnotationId] ann, bool isDef] renameOpts := renames[nameOfUseDef[l]]]>; + return [ + {just(annotation), *_} := renameOpts.ann + ? replace(l, rascalEscapeName(newName), annotation = annotation) + : replace(l, rascalEscapeName(newName), annotation = any(b <- renameOpts.isDef) ? defAnno : useAnno) + | l <- nameOfUseDef.name + , rel[Maybe[ChangeAnnotationId] ann, bool isDef] renameOpts := renames[nameOfUseDef[l]] + ]; } -private tuple[set[IllegalRenameReason] reasons, list[TextEdit] edits] computeTextEdits(WorkspaceInfo ws, loc moduleLoc, set[RenameLocation] defs, set[RenameLocation] uses, str name, ChangeAnnotationRegister registerChangeAnnotation) = - computeTextEdits(ws, parseModuleWithSpacesCached(moduleLoc), defs, uses, name, registerChangeAnnotation); +private list[TextEdit] computeTextEdits(WorkspaceInfo ws, loc moduleLoc, set[RenameLocation] defs, set[RenameLocation] uses, Cursor cur, str newName, ChangeAnnotationRegister registerChangeAnnotation) = + computeTextEdits(ws, parseModuleWithSpacesCached(moduleLoc), defs, uses, cur, newName, registerChangeAnnotation); private bool rascalIsFunctionLocalDefs(WorkspaceInfo ws, set[loc] defs) { for (d <- defs) { @@ -402,6 +442,7 @@ private Cursor rascalGetCursor(WorkspaceInfo ws, Tree cursorT) { Maybe[loc] smallestFieldContainingCursor = findSmallestContaining(fields.field, cursorLoc); Maybe[loc] smallestKeywordContainingCursor = findSmallestContaining(keywords.kw, cursorLoc); + loc moduleNameLoc = parseModuleWithSpacesCached(cursorLoc.top).top.header.name.src; rel[loc l, CursorKind kind] locsContainingCursor = { @@ -421,8 +462,8 @@ private Cursor rascalGetCursor(WorkspaceInfo ws, Tree cursorT) { , , , - // Module name declaration, where the cursor location is in the module header - , + // Module name declaration + , // Nonterminal constructor names in exception productions , } @@ -436,16 +477,12 @@ private Cursor rascalGetCursor(WorkspaceInfo ws, Tree cursorT) { return cursor(kind, min(locsContainingCursor.l), cursorName); } -private set[Name] rascalNameToEquivalentNames(str name) = { - [Name] name, - startsWith(name, "\\") ? [Name] name : [Name] "\\" -}; +private set[str] rascalNameToEquivalentNames(str name) = + {name, startsWith(name, "\\") ? name : "\\"}; private bool rascalContainsName(loc l, str name) { m = parseModuleWithSpacesCached(l); - for (n <- rascalNameToEquivalentNames(name)) { - if (/n := m) return true; - } + if (/Tree t := m, "" in rascalNameToEquivalentNames(name)) return true; return false; } @@ -605,20 +642,23 @@ Edits rascalRenameSymbol(Tree cursorT, set[loc] workspaceFolders, str newName, P set[loc] \files = defsPerFile.file + usesPerFile.file; step("checking rename validity", 1); - - map[loc, tuple[set[IllegalRenameReason] reasons, list[TextEdit] edits]] moduleResults = - (file: | file <- \files, := computeTextEdits(ws, file, defsPerFile[file], usesPerFile[file], newName, registerChangeAnnotation)); - - if (reasons := union({moduleResults[file].reasons | file <- moduleResults}), reasons != {}) { - list[str] reasonDescs = toList({describe(r) | r <- reasons}); - throw illegalRename("Rename is not valid, because:\n - ", reasons); + if (reasons := rascalCollectIllegalRenames(ws, defsPerFile, usesPerFile, newName) + , reasons != {}) { + throw illegalRename("Rename is not valid, because:\n - ", reasons); } - list[DocumentEdit] changes = [changed(file, moduleResults[file].edits) | file <- moduleResults]; - list[DocumentEdit] renames = [renamed(from, to) | <- getRenames(newName)]; + step("building list of edits", 1); + map[loc, list[TextEdit]] moduleResults = + (file: edits | file <- \files, edits := computeTextEdits(ws, file, defsPerFile[file], usesPerFile[file], cur, newName, registerChangeAnnotation)); + + list[DocumentEdit] changes = [changed(file, moduleResults[file]) | file <- moduleResults]; + list[DocumentEdit] renames = [renamed(from, to) | <- getRenames(rascalUnescapeName(newName))]; return ; -}, totalWork = 6); +}, totalWork = 7); + +Edits rascalRenameModule(rel[str oldName, str newName, PathConfig pcfg] qualifiedNameChanges, set[loc] workspaceFolders, PathConfig(loc) getPathConfig) = + propagateModuleRenames(qualifiedNameChanges, workspaceFolders, getPathConfig); //// WORKAROUNDS diff --git a/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/Util.rsc b/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/Util.rsc index e51a1fda4..9865b89d9 100644 --- a/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/Util.rsc +++ b/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/Util.rsc @@ -38,6 +38,10 @@ import util::Reflective; import lang::rascal::\syntax::Rascal; +import lang::rascal::lsp::refactor::TextEdits; + +alias Edits = tuple[list[DocumentEdit], map[ChangeAnnotationId, ChangeAnnotation]]; + @synopsis{ Finds the smallest location in `wrappers` than contains `l`. If none contains `l`, returns `nothing().` Accepts a predicate deciding containment as an optional argument. @@ -92,9 +96,6 @@ bool tryParseAs(type[&T <: Tree] begin, str name, bool allowAmbiguity = false) { } } -Maybe[&B] flatMap(nothing(), Maybe[&B](&A) _) = nothing(); -Maybe[&B] flatMap(just(&A a), Maybe[&B](&A) f) = f(a); - str toString(error(msg, l)) = "[error] \'\' at "; str toString(error(msg)) = "[error] \'\'"; str toString(warning(msg, l)) = "[warning] \'\' at "; @@ -129,3 +130,6 @@ bool(&T, &T) desc(bool(&T, &T) f) { return f(t2, t1); }; } + +set[&T] flatMap(set[&S] ss, set[&T](&S) f) = ({} | it + f(s) | s <- ss); +list[&T] flatMap(list[&S] ss, list[&T](&S) f) = ([] | it + f(s) | s <- ss); diff --git a/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/WorkspaceInfo.rsc b/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/WorkspaceInfo.rsc index defeeb52d..fae585722 100644 --- a/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/WorkspaceInfo.rsc +++ b/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/WorkspaceInfo.rsc @@ -43,6 +43,7 @@ import lang::rascal::lsp::refactor::Exception; import lang::rascal::lsp::refactor::TextEdits; import lang::rascal::lsp::refactor::Util; +import IO; import List; import Location; import Map; @@ -308,7 +309,6 @@ set[loc] rascalGetOverloadedDefs(WorkspaceInfo ws, set[loc] defs, MayOverloadFun } private rel[loc, loc] NO_RENAMES(str _) = {}; -private int qualSepSize = size("::"); bool rascalIsCollectionType(AType at) = at is arel || at is alrel || at is atuple; bool rascalIsConstructorType(AType at) = at is acons; @@ -734,27 +734,34 @@ DefsUsesRenames rascalGetDefsUses(WorkspaceInfo ws, cursor(moduleName(), cursorL } } - modName = getModuleName(moduleFile, ws.getPathConfig(getProjectFolder(ws, moduleFile))); + set[loc] defs = {ms | ms <- getModuleScopes(ws), ms.top == moduleFile}; - defs = {parseModuleWithSpacesCached(moduleFile).top.header.name.names[-1].src}; + rel[loc fromFile, loc toFile] modulePaths = toRel(getModuleScopePerFile(ws)) o rascalGetTransitiveReflexiveModulePaths(ws); + set[loc] importUses = {u + | > <- ws.useDef o definitionsRel(ws) + , in modulePaths + }; - imports = {u | u <- ws.useDef<0>, amodule(modName) := ws.facts[u]}; - qualifiedUses = { - // We compute the location of the module name in the qualified name at `u` - // some::qualified::path::to::Foo::SomeVar - // \____________________________/\/\_____/ - // moduleNameSize ^ qualSepSize ^ ^ idSize - trim(u, removePrefix = moduleNameSize - size(cursorName) - , removeSuffix = idSize + qualSepSize) + rel[loc file, loc use] qualifiedUseCandidates = { + | <- ws.useDef o definitionsRel(ws) - , idSize := size(d.id) - , u.length > idSize // There might be a qualified prefix - , moduleNameSize := size(modName) - , u.length == moduleNameSize + qualSepSize + idSize + , u.length > size(d.id) // use name > declaration name, i.e. there is a qualified prefix + }; + set[loc] qualifiedUses = { + qn.src + | loc file <- qualifiedUseCandidates.file + , start[Module] m := parseModuleWithSpacesCached(file) + , set[loc] localUses := qualifiedUseCandidates[file] + , /QualifiedName qn := m + , qn.src in localUses + , cursorName == intercalate("::", prefix(["" | n <- qn.names])) }; - uses = imports + qualifiedUses; - rel[loc, loc] getRenames(str newName) = { | d <- defs, file := d.top}; + set[loc] uses = importUses + qualifiedUses; + + pcfg = ws.getPathConfig(getProjectFolder(ws, moduleFile)); + loc srcFolder = [srcFolder | relModulePath := relativize(pcfg.srcs, moduleFile), srcFolder <- pcfg.srcs, exists(srcFolder + relModulePath.path)][0]; + rel[loc, loc] getRenames(str newName) = { | d <- defs, file := d.top}; return ; } diff --git a/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/rename/Modules.rsc b/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/rename/Modules.rsc new file mode 100644 index 000000000..cc43053e6 --- /dev/null +++ b/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/rename/Modules.rsc @@ -0,0 +1,92 @@ +@license{ +Copyright (c) 2018-2023, NWO-I CWI and Swat.engineering +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +} +module lang::rascal::lsp::refactor::rename::Modules + +import lang::rascal::lsp::refactor::TextEdits; +import lang::rascal::lsp::refactor::Util; + +import lang::rascal::\syntax::Rascal; + +import IO; +import List; +import Location; +import ParseTree; +import Set; +import String; + +import util::FileSystem; +import util::Reflective; + +private tuple[str, loc] fullQualifiedName(QualifiedName qn) = <"", qn.src>; +private tuple[str, loc] qualifiedPrefix(QualifiedName qn) { + list[Name] names = [n | n <- qn.names]; + if (size(names) <= 1) return <"", |unknown:///|>; + + str fullName = ""; + str namePrefix = fullName[..findLast(fullName, "::")]; + loc prefixLoc = cover([n.src | Name n <- prefix(names)]); + + return ; +} + +private bool isReachable(PathConfig toProject, PathConfig fromProject) = + toProject == fromProject // Both configs belong to the same project + || toProject.bin in fromProject.libs; // The using project can import the declaring project + +list[TextEdit] getChanges(loc f, PathConfig wsProject, rel[str oldName, str newName, PathConfig pcfg] qualifiedNameChanges) { + list[TextEdit] changes = []; + + start[Module] m = parseModuleWithSpacesCached(f); + for (/QualifiedName qn := m) { + for ( <- {fullQualifiedName(qn), qualifiedPrefix(qn)} + , {} := qualifiedNameChanges[oldName] + , isReachable(projWithRenamedMod, wsProject) + ) { + changes += replace(l, newName); + } + } + + return changes; +} + +Edits propagateModuleRenames(rel[str oldName, str newName, PathConfig pcfg] qualifiedNameChanges, set[loc] workspaceFolders, PathConfig(loc) getPathConfig) { + set[PathConfig] projectWithRenamedModule = qualifiedNameChanges.pcfg; + set[DocumentEdit] edits = flatMap(workspaceFolders, set[DocumentEdit](loc wsFolder) { + PathConfig wsFolderPcfg = getPathConfig(wsFolder); + + // If this workspace cannot reach any of the renamed modules, no need to continue looking for references to renamed modules here at all + if (!any(PathConfig changedProj <- projectWithRenamedModule, isReachable(changedProj, wsFolderPcfg))) return {}; + + return {changed(file, changes) + | loc file <- find(wsFolder, "rsc") + , changes := getChanges(file, wsFolderPcfg, qualifiedNameChanges) + , changes != [] + }; + }); + + return ; +} diff --git a/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Modules.rsc b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Modules.rsc index 9b0299864..69c076b3a 100644 --- a/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Modules.rsc +++ b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Modules.rsc @@ -38,8 +38,8 @@ test bool deepModule() = testRenameOccurrences({ 'void main() { ' some::path::to::Foo::Bool b = and(t(), f()); '} - ", {0, 1}) -}, oldName = "Foo", newName = "Bar"); + ", {0, 1}, skipCursors = {1}) +}, oldName = "some::path::to::Foo", newName = "some::path::to::Bar"); test bool shadowedModuleWithVar() = testRenameOccurrences({ byText("Foo", " @@ -52,7 +52,7 @@ test bool shadowedModuleWithVar() = testRenameOccurrences({ 'void main() { ' Foo::Bool b = and(t(), f()); '} - ", {0, 1}) + ", {0, 1}, skipCursors = {1}) }, oldName = "Foo", newName = "Bar"); test bool shadowedModuleWithFunc() = testRenameOccurrences({ @@ -65,7 +65,7 @@ test bool shadowedModuleWithFunc() = testRenameOccurrences({ 'void main() { ' Foo::f(); '} - ", {0, 1}) + ", {0, 1}, skipCursors = {1}) }, oldName = "Foo", newName = "Bar"); test bool singleModule() = testRenameOccurrences({ @@ -78,5 +78,39 @@ test bool singleModule() = testRenameOccurrences({ 'void main() { ' util::Foo::Bool b = and(t(), f()); '} - ", {0, 1}) + ", {0, 1}, skipCursors = {1}) +}, oldName = "util::Foo", newName = "util::Bar"); + +test bool moduleBarIsNotBaz() = testRenameOccurrences({ + byText("foo::Foo", "import Foo;", {1}), + byText("Foo", "import Baz;", {0}, newName = "Bar"), + byText("Baz", " + 'void f() {} + 'void g() { + ' Baz::f(); + '} + ", {}) +}, oldName = "Foo", newName = "Bar"); + +test bool moveModule() = testRenameOccurrences({ + byText("Foo", "int foo() = 8;", {0}, newName = "path::to::Foo"), + byText("Main", " + 'import Foo; + 'int f = Foo::foo();", {0, 1}, skipCursors = {1}) +}, oldName = "Foo", newName = "path::to::Foo"); + +test bool qualifiedSelf() = testRenameOccurrences({ + byText("Foo", " + 'void f() {} + 'void g() { + ' Foo::f(); + '} + ", {0, 1}, skipCursors = {1}, newName = "Bar") +}, oldName = "Foo", newName = "Bar"); + +@expected{unsupportedRename} +test bool externalImport() = testRenameOccurrences({ + byText("Main", " + 'import Foo = |memory:///Foo.rsc|; + ", {0}) }, oldName = "Foo", newName = "Bar"); diff --git a/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/TestUtils.rsc b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/TestUtils.rsc index 188a56dfd..4dad67de0 100644 --- a/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/TestUtils.rsc +++ b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/TestUtils.rsc @@ -50,6 +50,7 @@ import lang::rascal::lsp::refactor::TextEdits; import util::FileSystem; import util::Math; +import util::Maybe; import util::Reflective; @@ -263,25 +264,39 @@ private tuple[Edits, set[int]] getEditsAndModule(str stmtsStr, int cursorAtOldNa return ; } -private list[Tree] collectNameTrees(start[Module] m, str name) { - list[Tree] names = []; - visit (m) { +private lrel[int, loc, Maybe[Tree]] collectNameTrees(start[Module] m, str name) { + lrel[loc, Maybe[Tree]] names = []; + top-down-break visit (m) { + case QualifiedName qn: { + if ("" == name) { + names += ; + } + else { + modPrefix = prefix([n | n <- qn.names]); + if (intercalate("::", ["" | n <- modPrefix]) == name) { + names += ; + } else { + fail; + } + } + } // 'Normal' names case Name n: - if ("" == name) names += n; + if ("" == name) names += ; // Nonterminals (grammars) case Nonterminal s: - if ("" == name) names += s; + if ("" == name) names += ; // Labels for nonterminals (grammars) case NonterminalLabel label: - if ("