From f7d843e71c6a3162fa158d6483cf52b35d9406bb Mon Sep 17 00:00:00 2001 From: Miles Ziemer Date: Wed, 25 Sep 2024 13:10:44 -0400 Subject: [PATCH 1/3] Support projects in subdirectories This commit makes the language server work with Smithy projects in subdirectories of the folder (or folders, in the case of e.g. vscode workspace folders) open in the editor. I previously added support for multiple _workspace folders_ (an LSP concept), but I assumed only one _project_ (Smithy LS concept) per workspace folder. So this commit fixes that mixing, allowing many projects per workspace folder. Now, the language server will search through subdirectories of all workspace folders (by default, one workspace folder is open in the client) to find projects. Changes to build files, i.e. smithy-build.json, .smithy-project.json, are now tracked at the workspace level, so you can add a new project to an existing workspace. I also did _some_ cleanup of the project/workspace synchronization code, and moved some things around. A note on some tests: I'm using a `Files.createTempDirectory`, and adding the `TestWorkspace` as a subdir. In a follow-up commit, I will be changing `TestWorkspace` to be something like `TestProject`, which is more accurate. I didn't include it here to avoid a bunch of noise. --- ...ectFilePatterns.java => FilePatterns.java} | 84 +++-- ...ler.java => FileWatcherRegistrations.java} | 37 ++- .../amazon/smithy/lsp/ProjectRootVisitor.java | 52 ++++ .../smithy/lsp/SmithyLanguageServer.java | 123 ++++++-- .../amazon/smithy/lsp/WorkspaceChanges.java | 120 ++++++++ .../amazon/smithy/lsp/project/Project.java | 4 + ...ProjectChanges.java => ProjectChange.java} | 19 +- .../smithy/lsp/project/ProjectConfig.java | 14 +- .../lsp/project/ProjectConfigLoader.java | 15 +- .../smithy/lsp/project/ProjectManager.java | 61 +--- ...atternsTest.java => FilePatternsTest.java} | 43 ++- ...java => FileWatcherRegistrationsTest.java} | 8 +- .../smithy/lsp/ProjectRootVisitorTest.java | 32 ++ .../smithy/lsp/SmithyLanguageServerTest.java | 289 ++++++++++++++++-- .../amazon/smithy/lsp/TestWorkspace.java | 25 +- .../amazon/smithy/lsp/UtilMatchers.java | 14 + .../multi-nested/nested-a/smithy-build.json | 0 .../nested-b/.smithy-project.json | 0 .../project/nested/nested/smithy-build.json | 0 19 files changed, 742 insertions(+), 198 deletions(-) rename src/main/java/software/amazon/smithy/lsp/{project/ProjectFilePatterns.java => FilePatterns.java} (50%) rename src/main/java/software/amazon/smithy/lsp/{handler/FileWatcherRegistrationHandler.java => FileWatcherRegistrations.java} (68%) create mode 100644 src/main/java/software/amazon/smithy/lsp/ProjectRootVisitor.java create mode 100644 src/main/java/software/amazon/smithy/lsp/WorkspaceChanges.java rename src/main/java/software/amazon/smithy/lsp/project/{ProjectChanges.java => ProjectChange.java} (59%) rename src/test/java/software/amazon/smithy/lsp/{project/ProjectFilePatternsTest.java => FilePatternsTest.java} (51%) rename src/test/java/software/amazon/smithy/lsp/{handler/FileWatcherRegistrationHandlerTest.java => FileWatcherRegistrationsTest.java} (90%) create mode 100644 src/test/java/software/amazon/smithy/lsp/ProjectRootVisitorTest.java create mode 100644 src/test/resources/software/amazon/smithy/lsp/project/multi-nested/nested-a/smithy-build.json create mode 100644 src/test/resources/software/amazon/smithy/lsp/project/multi-nested/nested-b/.smithy-project.json create mode 100644 src/test/resources/software/amazon/smithy/lsp/project/nested/nested/smithy-build.json diff --git a/src/main/java/software/amazon/smithy/lsp/project/ProjectFilePatterns.java b/src/main/java/software/amazon/smithy/lsp/FilePatterns.java similarity index 50% rename from src/main/java/software/amazon/smithy/lsp/project/ProjectFilePatterns.java rename to src/main/java/software/amazon/smithy/lsp/FilePatterns.java index f4ba000c..34d407e6 100644 --- a/src/main/java/software/amazon/smithy/lsp/project/ProjectFilePatterns.java +++ b/src/main/java/software/amazon/smithy/lsp/FilePatterns.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package software.amazon.smithy.lsp.project; +package software.amazon.smithy.lsp; import java.io.File; import java.nio.file.FileSystems; @@ -13,22 +13,24 @@ import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; +import software.amazon.smithy.lsp.project.Project; +import software.amazon.smithy.lsp.project.ProjectConfigLoader; /** - * Utility methods for creating file patterns corresponding to meaningful - * paths of a {@link Project}, such as sources and build files. + * Utility methods for computing glob patterns that match against Smithy files + * or build files in Projects and workspaces. */ -public final class ProjectFilePatterns { +final class FilePatterns { private static final int BUILD_FILE_COUNT = 2 + ProjectConfigLoader.SMITHY_BUILD_EXTS.length; - private ProjectFilePatterns() { + private FilePatterns() { } /** * @param project The project to get watch patterns for * @return A list of glob patterns used to watch Smithy files in the given project */ - public static List getSmithyFileWatchPatterns(Project project) { + static List getSmithyFileWatchPatterns(Project project) { return Stream.concat(project.sources().stream(), project.imports().stream()) .map(path -> getSmithyFilePattern(path, true)) .toList(); @@ -38,45 +40,71 @@ public static List getSmithyFileWatchPatterns(Project project) { * @param project The project to get a path matcher for * @return A path matcher that can check if Smithy files belong to the given project */ - public static PathMatcher getSmithyFilesPathMatcher(Project project) { + static PathMatcher getSmithyFilesPathMatcher(Project project) { String pattern = Stream.concat(project.sources().stream(), project.imports().stream()) .map(path -> getSmithyFilePattern(path, false)) .collect(Collectors.joining(",")); - return FileSystems.getDefault().getPathMatcher("glob:{" + pattern + "}"); + return toPathMatcher("{" + pattern + "}"); } /** - * @param project The project to get the watch pattern for - * @return A glob pattern used to watch build files in the given project + * @param root The root to get the watch pattern for + * @return A glob pattern used to watch build files in the given workspace */ - public static String getBuildFilesWatchPattern(Project project) { - Path root = project.root(); - String buildJsonPattern = escapeBackslashes(root.resolve(ProjectConfigLoader.SMITHY_BUILD).toString()); - String projectJsonPattern = escapeBackslashes(root.resolve(ProjectConfigLoader.SMITHY_PROJECT).toString()); - - List patterns = new ArrayList<>(BUILD_FILE_COUNT); - patterns.add(buildJsonPattern); - patterns.add(projectJsonPattern); - for (String buildExt : ProjectConfigLoader.SMITHY_BUILD_EXTS) { - patterns.add(escapeBackslashes(root.resolve(buildExt).toString())); - } + static String getWorkspaceBuildFilesWatchPattern(Path root) { + return getBuildFilesPattern(root, true); + } - return "{" + String.join(",", patterns) + "}"; + /** + * @param root The root to get a path matcher for + * @return A path matcher that can check if a file is a build file within the given workspace + */ + static PathMatcher getWorkspaceBuildFilesPathMatcher(Path root) { + String pattern = getWorkspaceBuildFilesWatchPattern(root); + return toPathMatcher(pattern); } /** * @param project The project to get a path matcher for * @return A path matcher that can check if a file is a build file belonging to the given project */ - public static PathMatcher getBuildFilesPathMatcher(Project project) { - // Watch pattern is the same as the pattern used for matching - String pattern = getBuildFilesWatchPattern(project); - return FileSystems.getDefault().getPathMatcher("glob:" + pattern); + static PathMatcher getProjectBuildFilesPathMatcher(Project project) { + String pattern = getBuildFilesPattern(project.root(), false); + return toPathMatcher(pattern); + } + + private static PathMatcher toPathMatcher(String globPattern) { + return FileSystems.getDefault().getPathMatcher("glob:" + globPattern); + } + + // Patterns for the workspace need to match on all build files in all subdirectories, + // whereas patterns for projects only look at the top level (because project locations + // are defined by the presence of these build files). + private static String getBuildFilesPattern(Path root, boolean isWorkspacePattern) { + List patterns = new ArrayList<>(BUILD_FILE_COUNT); + patterns.add(ProjectConfigLoader.SMITHY_BUILD); + patterns.add(ProjectConfigLoader.SMITHY_PROJECT); + for (String buildExt : ProjectConfigLoader.SMITHY_BUILD_EXTS) { + Path extPath = Path.of(buildExt); // buildExt may have file separators + patterns.add(extPath.toString()); + } + + String rootString = root.toString(); + if (!rootString.endsWith(File.separator)) { + rootString += File.separator; + } + + if (isWorkspacePattern) { + rootString += "**" + File.separator; + } + + return escapeBackslashes(rootString + "{" + String.join(",", patterns) + "}"); } // When computing the pattern used for telling the client which files to watch, we want - // to only watch .smithy/.json files. We don't need in the PathMatcher pattern (and it - // is impossible anyway because we can't have a nested pattern). + // to only watch .smithy/.json files. We don't need it in the PathMatcher pattern because + // we only need to match files, not listen for specific changes (and it is impossible anyway + // because we can't have a nested pattern). private static String getSmithyFilePattern(Path path, boolean isWatcherPattern) { String glob = path.toString(); if (glob.endsWith(".smithy") || glob.endsWith(".json")) { diff --git a/src/main/java/software/amazon/smithy/lsp/handler/FileWatcherRegistrationHandler.java b/src/main/java/software/amazon/smithy/lsp/FileWatcherRegistrations.java similarity index 68% rename from src/main/java/software/amazon/smithy/lsp/handler/FileWatcherRegistrationHandler.java rename to src/main/java/software/amazon/smithy/lsp/FileWatcherRegistrations.java index 57602501..6299e84f 100644 --- a/src/main/java/software/amazon/smithy/lsp/handler/FileWatcherRegistrationHandler.java +++ b/src/main/java/software/amazon/smithy/lsp/FileWatcherRegistrations.java @@ -3,8 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -package software.amazon.smithy.lsp.handler; +package software.amazon.smithy.lsp; +import java.nio.file.Path; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -15,7 +16,6 @@ import org.eclipse.lsp4j.WatchKind; import org.eclipse.lsp4j.jsonrpc.messages.Either; import software.amazon.smithy.lsp.project.Project; -import software.amazon.smithy.lsp.project.ProjectFilePatterns; /** * Handles computing the {@link Registration}s and {@link Unregistration}s for @@ -32,7 +32,7 @@ * everything, since these events should be rarer. But we can optimize it in the * future. */ -public final class FileWatcherRegistrationHandler { +final class FileWatcherRegistrations { private static final Integer SMITHY_WATCH_FILE_KIND = WatchKind.Delete | WatchKind.Create; private static final String WATCH_BUILD_FILES_ID = "WatchSmithyBuildFiles"; private static final String WATCH_SMITHY_FILES_ID = "WatchSmithyFiles"; @@ -40,17 +40,23 @@ public final class FileWatcherRegistrationHandler { private static final List SMITHY_FILE_WATCHER_UNREGISTRATIONS = List.of(new Unregistration( WATCH_SMITHY_FILES_ID, WATCH_FILES_METHOD)); + private static final List BUILD_FILE_WATCHER_UNREGISTRATIONS = List.of(new Unregistration( + WATCH_BUILD_FILES_ID, + WATCH_FILES_METHOD)); - private FileWatcherRegistrationHandler() { + private FileWatcherRegistrations() { } /** + * Creates registrations to tell the client to watch for new or deleted + * Smithy files, specifically for files that are part of {@link Project}s. + * * @param projects The projects to get registrations for * @return The registrations to watch for Smithy file changes across all projects */ - public static List getSmithyFileWatcherRegistrations(Collection projects) { + static List getSmithyFileWatcherRegistrations(Collection projects) { List smithyFileWatchers = projects.stream() - .flatMap(project -> ProjectFilePatterns.getSmithyFileWatchPatterns(project).stream()) + .flatMap(project -> FilePatterns.getSmithyFileWatchPatterns(project).stream()) .map(pattern -> new FileSystemWatcher(Either.forLeft(pattern), SMITHY_WATCH_FILE_KIND)) .toList(); @@ -63,17 +69,20 @@ public static List getSmithyFileWatcherRegistrations(Collection getSmithyFileWatcherUnregistrations() { + static List getSmithyFileWatcherUnregistrations() { return SMITHY_FILE_WATCHER_UNREGISTRATIONS; } /** - * @param projects The projects to get registrations for - * @return The registrations to watch for build file changes across all projects + * Creates registrations to tell the client to watch for any build file + * changes, creations, or deletions, across all workspaces. + * + * @param workspaceRoots The roots of the workspaces to get registrations for + * @return The registrations to watch for build file changes across all workspaces */ - public static List getBuildFileWatcherRegistrations(Collection projects) { - List watchers = projects.stream() - .map(ProjectFilePatterns::getBuildFilesWatchPattern) + static List getBuildFileWatcherRegistrations(Collection workspaceRoots) { + List watchers = workspaceRoots.stream() + .map(FilePatterns::getWorkspaceBuildFilesWatchPattern) .map(pattern -> new FileSystemWatcher(Either.forLeft(pattern))) .toList(); @@ -82,4 +91,8 @@ public static List getBuildFileWatcherRegistrations(Collection getBuildFileWatcherUnregistrations() { + return BUILD_FILE_WATCHER_UNREGISTRATIONS; + } } diff --git a/src/main/java/software/amazon/smithy/lsp/ProjectRootVisitor.java b/src/main/java/software/amazon/smithy/lsp/ProjectRootVisitor.java new file mode 100644 index 00000000..d99f75e4 --- /dev/null +++ b/src/main/java/software/amazon/smithy/lsp/ProjectRootVisitor.java @@ -0,0 +1,52 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.lsp; + +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.List; +import software.amazon.smithy.lsp.project.ProjectConfigLoader; + +/** + * Finds Project roots based on the location of smithy-build.json and .smithy-project.json. + */ +final class ProjectRootVisitor extends SimpleFileVisitor { + private static final PathMatcher PROJECT_ROOT_MATCHER = FileSystems.getDefault().getPathMatcher( + "glob:{" + ProjectConfigLoader.SMITHY_BUILD + "," + ProjectConfigLoader.SMITHY_PROJECT + "}"); + + private final List roots = new ArrayList<>(); + + /** + * Walks through the file tree starting at {@code workspaceRoot}, collecting + * paths of Project roots. + * + * @param workspaceRoot Root of the workspace to find projects in + * @return A list of project roots + * @throws IOException If an I/O error is thrown while walking files + */ + static List findProjectRoots(Path workspaceRoot) throws IOException { + ProjectRootVisitor visitor = new ProjectRootVisitor(); + Files.walkFileTree(workspaceRoot, visitor); + return visitor.roots; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + Path name = file.getFileName(); + if (name != null && PROJECT_ROOT_MATCHER.matches(name)) { + roots.add(file.getParent()); + return FileVisitResult.SKIP_SIBLINGS; + } + return FileVisitResult.CONTINUE; + } +} diff --git a/src/main/java/software/amazon/smithy/lsp/SmithyLanguageServer.java b/src/main/java/software/amazon/smithy/lsp/SmithyLanguageServer.java index 436c3f44..d80d4a9a 100644 --- a/src/main/java/software/amazon/smithy/lsp/SmithyLanguageServer.java +++ b/src/main/java/software/amazon/smithy/lsp/SmithyLanguageServer.java @@ -107,10 +107,8 @@ import software.amazon.smithy.lsp.ext.SmithyProtocolExtensions; import software.amazon.smithy.lsp.handler.CompletionHandler; import software.amazon.smithy.lsp.handler.DefinitionHandler; -import software.amazon.smithy.lsp.handler.FileWatcherRegistrationHandler; import software.amazon.smithy.lsp.handler.HoverHandler; import software.amazon.smithy.lsp.project.Project; -import software.amazon.smithy.lsp.project.ProjectChanges; import software.amazon.smithy.lsp.project.ProjectLoader; import software.amazon.smithy.lsp.project.ProjectManager; import software.amazon.smithy.lsp.project.SmithyFile; @@ -154,6 +152,7 @@ public class SmithyLanguageServer implements private final DocumentLifecycleManager lifecycleManager = new DocumentLifecycleManager(); private Severity minimumSeverity = Severity.WARNING; private boolean onlyReloadOnSave = false; + private final Set workspacePaths = new HashSet<>(); SmithyLanguageServer() { } @@ -174,6 +173,10 @@ DocumentLifecycleManager getLifecycleManager() { return this.lifecycleManager; } + Set getWorkspacePaths() { + return workspacePaths; + } + @Override public void connect(LanguageClient client) { LOGGER.finest("Connect"); @@ -227,8 +230,7 @@ public CompletableFuture initialize(InitializeParams params) { } for (WorkspaceFolder workspaceFolder : params.getWorkspaceFolders()) { - Path root = Paths.get(URI.create(workspaceFolder.getUri())); - tryInitProject(workspaceFolder.getName(), root); + loadWorkspace(workspaceFolder); } if (workDoneProgressToken != null) { @@ -241,15 +243,17 @@ public CompletableFuture initialize(InitializeParams params) { return completedFuture(new InitializeResult(CAPABILITIES)); } - private void tryInitProject(String name, Path root) { + private void tryInitProject(Path root) { LOGGER.finest("Initializing project at " + root); lifecycleManager.cancelAllTasks(); + Result> loadResult = ProjectLoader.load( root, projects, lifecycleManager.managedDocuments()); + + String projectName = root.toString(); if (loadResult.isOk()) { Project updatedProject = loadResult.unwrap(); - resolveDetachedProjects(this.projects.getProjectByName(name), updatedProject); - this.projects.updateProjectByName(name, updatedProject); + updateProject(projectName, updatedProject); LOGGER.finest("Initialized project at " + root); } else { LOGGER.severe("Init project failed"); @@ -257,11 +261,11 @@ private void tryInitProject(String name, Path root) { // if we find a smithy-build.json, etc. // If we overwrite an existing project with an empty one, we lose track of the state of tracked // files. Instead, we will just keep the original project before the reload failure. - if (projects.getProjectByName(name) == null) { - projects.updateProjectByName(name, Project.empty(root)); + if (projects.getProjectByName(projectName) == null) { + projects.updateProjectByName(projectName, Project.empty(root)); } - String baseMessage = "Failed to load Smithy project " + name + " at " + root; + String baseMessage = "Failed to load Smithy project at " + root; StringBuilder errorMessage = new StringBuilder(baseMessage).append(":"); for (Exception error : loadResult.unwrapErr()) { errorMessage.append(System.lineSeparator()); @@ -275,6 +279,16 @@ private void tryInitProject(String name, Path root) { } } + private void updateProject(String projectName, Project updatedProject) { + // If the project didn't load any config files, it is now empty and should be removed + if (updatedProject.config().loadedConfigPaths().isEmpty()) { + removeProjectAndResolveDetached(projectName); + } else { + resolveDetachedProjects(this.projects.getProjectByName(projectName), updatedProject); + this.projects.updateProjectByName(projectName, updatedProject); + } + } + private void resolveDetachedProjects(Project oldProject, Project updatedProject) { // This is a project reload, so we need to resolve any added/removed files // that need to be moved to or from detached projects. @@ -306,21 +320,29 @@ private void resolveDetachedProjects(Project oldProject, Project updatedProject) } private CompletableFuture registerSmithyFileWatchers() { - List registrations = FileWatcherRegistrationHandler.getSmithyFileWatcherRegistrations( + List registrations = FileWatcherRegistrations.getSmithyFileWatcherRegistrations( projects.attachedProjects().values()); return client.registerCapability(new RegistrationParams(registrations)); } private CompletableFuture unregisterSmithyFileWatchers() { - List unregistrations = FileWatcherRegistrationHandler.getSmithyFileWatcherUnregistrations(); + List unregistrations = FileWatcherRegistrations.getSmithyFileWatcherUnregistrations(); + return client.unregisterCapability(new UnregistrationParams(unregistrations)); + } + + private CompletableFuture registerWorkspaceBuildFileWatchers() { + var registrations = FileWatcherRegistrations.getBuildFileWatcherRegistrations(workspacePaths); + return client.registerCapability(new RegistrationParams(registrations)); + } + + private CompletableFuture unregisterWorkspaceBuildFileWatchers() { + var unregistrations = FileWatcherRegistrations.getBuildFileWatcherUnregistrations(); return client.unregisterCapability(new UnregistrationParams(unregistrations)); } @Override public void initialized(InitializedParams params) { - List registrations = FileWatcherRegistrationHandler.getBuildFileWatcherRegistrations( - projects.attachedProjects().values()); - client.registerCapability(new RegistrationParams(registrations)); + registerWorkspaceBuildFileWatchers(); registerSmithyFileWatchers(); } @@ -411,19 +433,22 @@ public CompletableFuture serverStatus() { public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) { LOGGER.finest("DidChangeWatchedFiles"); // Smithy files were added or deleted to watched sources/imports (specified by smithy-build.json), - // or the smithy-build.json itself was changed + // the smithy-build.json itself was changed, added, or deleted. - Map changesByProject = projects.computeProjectChanges(params.getChanges()); + WorkspaceChanges changes = WorkspaceChanges.computeWorkspaceChanges( + params.getChanges(), projects, workspacePaths); - changesByProject.forEach((projectName, projectChanges) -> { + changes.byProject().forEach((projectName, projectChange) -> { Project project = projects.getProjectByName(projectName); - if (projectChanges.hasChangedBuildFiles()) { + + if (!projectChange.changedBuildFileUris().isEmpty()) { client.info("Build files changed, reloading project"); // TODO: Handle more granular updates to build files. - tryInitProject(projectName, project.root()); - } else if (projectChanges.hasChangedSmithyFiles()) { - Set createdUris = projectChanges.createdSmithyFileUris(); - Set deletedUris = projectChanges.deletedSmithyFileUris(); + // Note: This will take care of removing projects when build files are deleted + tryInitProject(project.root()); + } else { + Set createdUris = projectChange.createdSmithyFileUris(); + Set deletedUris = projectChange.deletedSmithyFileUris(); client.info("Project files changed, adding files " + createdUris + " and removing files " + deletedUris); @@ -433,7 +458,10 @@ public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) { } }); + changes.newProjectRoots().forEach(this::tryInitProject); + // TODO: Update watchers based on specific changes + // Note: We don't update build file watchers here - only on workspace changes unregisterSmithyFileWatchers().thenRun(this::registerSmithyFileWatchers); sendFileDiagnosticsForManagedDocuments(); @@ -458,20 +486,15 @@ public void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) { } for (WorkspaceFolder folder : params.getEvent().getAdded()) { - Path root = Paths.get(URI.create(folder.getUri())); - tryInitProject(folder.getName(), root); + loadWorkspace(folder); } for (WorkspaceFolder folder : params.getEvent().getRemoved()) { - Project removedProject = projects.removeProjectByName(folder.getName()); - if (removedProject == null) { - continue; - } - - resolveDetachedProjects(removedProject, Project.empty(removedProject.root())); + removeWorkspace(folder); } unregisterSmithyFileWatchers().thenRun(this::registerSmithyFileWatchers); + unregisterWorkspaceBuildFileWatchers().thenRun(this::registerWorkspaceBuildFileWatchers); sendFileDiagnosticsForManagedDocuments(); if (progressToken != null) { @@ -480,6 +503,44 @@ public void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) { } } + private void loadWorkspace(WorkspaceFolder workspaceFolder) { + Path workspaceRoot = Paths.get(URI.create(workspaceFolder.getUri())); + workspacePaths.add(workspaceRoot); + try { + List projectRoots = ProjectRootVisitor.findProjectRoots(workspaceRoot); + for (Path root : projectRoots) { + tryInitProject(root); + } + } catch (IOException e) { + LOGGER.severe(e.getMessage()); + } + } + + private void removeWorkspace(WorkspaceFolder folder) { + Path workspaceRoot = Paths.get(URI.create(folder.getUri())); + workspacePaths.remove(workspaceRoot); + + // Have to do the removal separately, so we don't modify project.attachedProjects() + // while iterating through it + List projectsToRemove = new ArrayList<>(); + for (var entry : projects.attachedProjects().entrySet()) { + if (entry.getValue().root().startsWith(workspaceRoot)) { + projectsToRemove.add(entry.getKey()); + } + } + + for (String projectName : projectsToRemove) { + removeProjectAndResolveDetached(projectName); + } + } + + private void removeProjectAndResolveDetached(String projectName) { + Project removedProject = this.projects.removeProjectByName(projectName); + if (removedProject != null) { + resolveDetachedProjects(removedProject, Project.empty(removedProject.root())); + } + } + @Override public void didChangeConfiguration(DidChangeConfigurationParams params) { } diff --git a/src/main/java/software/amazon/smithy/lsp/WorkspaceChanges.java b/src/main/java/software/amazon/smithy/lsp/WorkspaceChanges.java new file mode 100644 index 00000000..8fa94dc2 --- /dev/null +++ b/src/main/java/software/amazon/smithy/lsp/WorkspaceChanges.java @@ -0,0 +1,120 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.lsp; + +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.eclipse.lsp4j.FileChangeType; +import org.eclipse.lsp4j.FileEvent; +import software.amazon.smithy.lsp.project.Project; +import software.amazon.smithy.lsp.project.ProjectChange; +import software.amazon.smithy.lsp.project.ProjectManager; +import software.amazon.smithy.lsp.protocol.LspAdapter; + +/** + * Aggregates changes to the workspace, including existing project changes and + * new project additions. + */ +final class WorkspaceChanges { + // smithy-build.json + .smithy-project.json + exts + private final Map byProject = new HashMap<>(); + private final List newProjectRoots = new ArrayList<>(); + + private WorkspaceChanges() { + } + + static WorkspaceChanges computeWorkspaceChanges( + List events, + ProjectManager projects, + Set workspacePaths + ) { + WorkspaceChanges changes = new WorkspaceChanges(); + + List projectFileMatchers = new ArrayList<>(projects.attachedProjects().size()); + projects.attachedProjects().forEach((projectName, project) -> + projectFileMatchers.add(createProjectFileMatcher(projectName, project))); + + List workspaceBuildFileMatchers = new ArrayList<>(workspacePaths.size()); + workspacePaths.forEach(workspacePath -> + workspaceBuildFileMatchers.add(FilePatterns.getWorkspaceBuildFilesPathMatcher(workspacePath))); + + for (FileEvent event : events) { + changes.addEvent(event, projectFileMatchers, workspaceBuildFileMatchers); + } + + return changes; + } + + Map byProject() { + return byProject; + } + + List newProjectRoots() { + return newProjectRoots; + } + + private void addEvent( + FileEvent event, + List projectFileMatchers, + List workspaceBuildFileMatchers + ) { + String changedUri = event.getUri(); + Path changedPath = Path.of(LspAdapter.toPath(changedUri)); + if (changedUri.endsWith(".smithy")) { + for (ProjectFileMatcher projectFileMatcher : projectFileMatchers) { + if (projectFileMatcher.smithyFileMatcher().matches(changedPath)) { + ProjectChange projectChange = byProject.computeIfAbsent( + projectFileMatcher.projectName(), ignored -> ProjectChange.empty()); + + switch (event.getType()) { + case Created -> projectChange.createdSmithyFileUris().add(changedUri); + case Deleted -> projectChange.deletedSmithyFileUris().add(changedUri); + default -> { + } + } + return; + } + } + } else { + for (ProjectFileMatcher projectFileMatcher : projectFileMatchers) { + if (projectFileMatcher.buildFileMatcher().matches(changedPath)) { + byProject.computeIfAbsent(projectFileMatcher.projectName(), ignored -> ProjectChange.empty()) + .changedBuildFileUris() + .add(changedUri); + return; + } + } + + // Only check if there's an added project. If there was a project we didn't match before, there's + // not much we could do at this point anyway. + if (event.getType() == FileChangeType.Created) { + for (PathMatcher workspaceBuildFileMatcher : workspaceBuildFileMatchers) { + if (workspaceBuildFileMatcher.matches(changedPath)) { + Path newProjectRoot = changedPath.getParent(); + this.newProjectRoots.add(newProjectRoot); + return; + } + } + } + } + } + + private record ProjectFileMatcher(String projectName, PathMatcher smithyFileMatcher, PathMatcher buildFileMatcher) { + } + + private static ProjectFileMatcher createProjectFileMatcher(String projectName, Project project) { + PathMatcher smithyFileMatcher = FilePatterns.getSmithyFilesPathMatcher(project); + + PathMatcher buildFileMatcher = FilePatterns.getProjectBuildFilesPathMatcher(project); + return new ProjectFileMatcher(projectName, smithyFileMatcher, buildFileMatcher); + } +} + diff --git a/src/main/java/software/amazon/smithy/lsp/project/Project.java b/src/main/java/software/amazon/smithy/lsp/project/Project.java index 42c9135e..850f1082 100644 --- a/src/main/java/software/amazon/smithy/lsp/project/Project.java +++ b/src/main/java/software/amazon/smithy/lsp/project/Project.java @@ -76,6 +76,10 @@ public Path root() { return root; } + public ProjectConfig config() { + return config; + } + /** * @return The paths of all Smithy sources specified * in this project's smithy build configuration files, diff --git a/src/main/java/software/amazon/smithy/lsp/project/ProjectChanges.java b/src/main/java/software/amazon/smithy/lsp/project/ProjectChange.java similarity index 59% rename from src/main/java/software/amazon/smithy/lsp/project/ProjectChanges.java rename to src/main/java/software/amazon/smithy/lsp/project/ProjectChange.java index 0da219e9..2e85742a 100644 --- a/src/main/java/software/amazon/smithy/lsp/project/ProjectChanges.java +++ b/src/main/java/software/amazon/smithy/lsp/project/ProjectChange.java @@ -5,6 +5,7 @@ package software.amazon.smithy.lsp.project; +import java.util.HashSet; import java.util.Set; /** @@ -14,22 +15,18 @@ * @param createdSmithyFileUris The uris of created Smithy files * @param deletedSmithyFileUris The uris of deleted Smithy files */ -public record ProjectChanges( +public record ProjectChange( Set changedBuildFileUris, Set createdSmithyFileUris, Set deletedSmithyFileUris ) { /** - * @return Whether there are any changed build files + * @return An empty and mutable set of project changes */ - public boolean hasChangedBuildFiles() { - return !changedBuildFileUris.isEmpty(); - } - - /** - * @return Whether there are any changed Smithy files - */ - public boolean hasChangedSmithyFiles() { - return !createdSmithyFileUris.isEmpty() || !deletedSmithyFileUris.isEmpty(); + public static ProjectChange empty() { + return new ProjectChange( + new HashSet<>(), + new HashSet<>(), + new HashSet<>()); } } diff --git a/src/main/java/software/amazon/smithy/lsp/project/ProjectConfig.java b/src/main/java/software/amazon/smithy/lsp/project/ProjectConfig.java index 33e5ec21..17893273 100644 --- a/src/main/java/software/amazon/smithy/lsp/project/ProjectConfig.java +++ b/src/main/java/software/amazon/smithy/lsp/project/ProjectConfig.java @@ -20,12 +20,13 @@ * A complete view of all a project's configuration that is needed to load it, * merged from all configuration sources. */ -final class ProjectConfig { +public final class ProjectConfig { private final List sources; private final List imports; private final String outputDirectory; private final List dependencies; private final MavenConfig mavenConfig; + private final List loadedConfigPaths; private ProjectConfig(Builder builder) { this.sources = builder.sources; @@ -33,6 +34,7 @@ private ProjectConfig(Builder builder) { this.outputDirectory = builder.outputDirectory; this.dependencies = builder.dependencies; this.mavenConfig = builder.mavenConfig; + this.loadedConfigPaths = builder.loadedConfigPaths; } static ProjectConfig empty() { @@ -78,12 +80,17 @@ public Optional maven() { return Optional.ofNullable(mavenConfig); } + public List loadedConfigPaths() { + return loadedConfigPaths; + } + static final class Builder { final List sources = new ArrayList<>(); final List imports = new ArrayList<>(); String outputDirectory; final List dependencies = new ArrayList<>(); MavenConfig mavenConfig; + final List loadedConfigPaths = new ArrayList<>(); private Builder() { } @@ -148,6 +155,11 @@ public Builder mavenConfig(MavenConfig mavenConfig) { return this; } + public Builder loadedConfigPaths(List loadedConfigPaths) { + this.loadedConfigPaths.addAll(loadedConfigPaths); + return this; + } + public ProjectConfig build() { return new ProjectConfig(this); } diff --git a/src/main/java/software/amazon/smithy/lsp/project/ProjectConfigLoader.java b/src/main/java/software/amazon/smithy/lsp/project/ProjectConfigLoader.java index c299ecea..77844694 100644 --- a/src/main/java/software/amazon/smithy/lsp/project/ProjectConfigLoader.java +++ b/src/main/java/software/amazon/smithy/lsp/project/ProjectConfigLoader.java @@ -74,17 +74,19 @@ private ProjectConfigLoader() { } static Result> loadFromRoot(Path workspaceRoot) { + List loadedConfigPaths = new ArrayList<>(); SmithyBuildConfig.Builder builder = SmithyBuildConfig.builder(); List exceptions = new ArrayList<>(); - // TODO: We don't handle cases where the smithy-build.json isn't in the top level of the root. - // In order to do so, we probably need to be able to keep track of multiple projects. Path smithyBuildPath = workspaceRoot.resolve(SMITHY_BUILD); if (Files.isRegularFile(smithyBuildPath)) { LOGGER.info("Loading smithy-build.json from " + smithyBuildPath); Result result = Result.ofFallible(() -> SmithyBuildConfig.load(smithyBuildPath)); - result.get().ifPresent(builder::merge); + result.get().ifPresent(config -> { + builder.merge(config); + loadedConfigPaths.add(smithyBuildPath); + }); result.getErr().ifPresent(exceptions::add); } else { LOGGER.info("No smithy-build.json found at " + smithyBuildPath + ", defaulting to empty config."); @@ -97,7 +99,10 @@ static Result> loadFromRoot(Path workspaceRoot) { if (Files.isRegularFile(extPath)) { Result result = Result.ofFallible(() -> loadSmithyBuildExtensions(extPath)); - result.get().ifPresent(extensionsBuilder::merge); + result.get().ifPresent(config -> { + extensionsBuilder.merge(config); + loadedConfigPaths.add(extPath); + }); result.getErr().ifPresent(exceptions::add); } } @@ -110,6 +115,7 @@ static Result> loadFromRoot(Path workspaceRoot) { ProjectConfig.Builder.load(smithyProjectPath)); if (result.isOk()) { finalConfigBuilder = result.unwrap(); + loadedConfigPaths.add(smithyProjectPath); } else { exceptions.add(result.unwrapErr()); } @@ -126,6 +132,7 @@ static Result> loadFromRoot(Path workspaceRoot) { if (finalConfigBuilder.outputDirectory == null) { config.getOutputDirectory().ifPresent(finalConfigBuilder::outputDirectory); } + finalConfigBuilder.loadedConfigPaths(loadedConfigPaths); return Result.ok(finalConfigBuilder.build()); } diff --git a/src/main/java/software/amazon/smithy/lsp/project/ProjectManager.java b/src/main/java/software/amazon/smithy/lsp/project/ProjectManager.java index de6927e2..3793dad2 100644 --- a/src/main/java/software/amazon/smithy/lsp/project/ProjectManager.java +++ b/src/main/java/software/amazon/smithy/lsp/project/ProjectManager.java @@ -5,16 +5,9 @@ package software.amazon.smithy.lsp.project; -import java.nio.file.Path; -import java.nio.file.PathMatcher; import java.util.HashMap; -import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.logging.Logger; -import org.eclipse.lsp4j.FileChangeType; -import org.eclipse.lsp4j.FileEvent; -import org.eclipse.lsp4j.WorkspaceFolder; import software.amazon.smithy.lsp.document.Document; import software.amazon.smithy.lsp.protocol.LspAdapter; @@ -31,7 +24,7 @@ public ProjectManager() { } /** - * @param name Name of the project, usually comes from {@link WorkspaceFolder#getName()} + * @param name Name of the project, should be path of the project directory * @return The project with the given name, if it exists */ public Project getProjectByName(String name) { @@ -160,56 +153,4 @@ public Document getDocument(String uri) { } return project.getDocument(uri); } - - /** - * Computes per-project file changes from the given file events. - * - *

>Note: if you have lots of projects, this will create a bunch of - * garbage because most times you aren't getting multiple sets of large - * updates to a project. Project changes are relatively rare, so this - * shouldn't have a huge impact. - * - * @param events The file events to compute per-project file changes from - * @return A map of project name to the corresponding project's changes - */ - public Map computeProjectChanges(List events) { - // Note: we could eagerly compute these and store them, but project changes are relatively rare, - // and doing it this way means we don't need to manage the state. - Map projectSmithyFileMatchers = new HashMap<>(attachedProjects().size()); - Map projectBuildFileMatchers = new HashMap<>(attachedProjects().size()); - - Map changes = new HashMap<>(attachedProjects().size()); - - attachedProjects().forEach((projectName, project) -> { - projectSmithyFileMatchers.put(projectName, ProjectFilePatterns.getSmithyFilesPathMatcher(project)); - projectBuildFileMatchers.put(projectName, ProjectFilePatterns.getBuildFilesPathMatcher(project)); - - // Need these to be hash sets so they are mutable - changes.put(projectName, new ProjectChanges(new HashSet<>(), new HashSet<>(), new HashSet<>())); - }); - - for (FileEvent event : events) { - String changedUri = event.getUri(); - Path changedPath = Path.of(LspAdapter.toPath(changedUri)); - if (changedUri.endsWith(".smithy")) { - projectSmithyFileMatchers.forEach((projectName, matcher) -> { - if (matcher.matches(changedPath)) { - if (event.getType() == FileChangeType.Created) { - changes.get(projectName).createdSmithyFileUris().add(changedUri); - } else if (event.getType() == FileChangeType.Deleted) { - changes.get(projectName).deletedSmithyFileUris().add(changedUri); - } - } - }); - } else { - projectBuildFileMatchers.forEach((projectName, matcher) -> { - if (matcher.matches(changedPath)) { - changes.get(projectName).changedBuildFileUris().add(changedUri); - } - }); - } - } - - return changes; - } } diff --git a/src/test/java/software/amazon/smithy/lsp/project/ProjectFilePatternsTest.java b/src/test/java/software/amazon/smithy/lsp/FilePatternsTest.java similarity index 51% rename from src/test/java/software/amazon/smithy/lsp/project/ProjectFilePatternsTest.java rename to src/test/java/software/amazon/smithy/lsp/FilePatternsTest.java index fe9a2c50..06d7973c 100644 --- a/src/test/java/software/amazon/smithy/lsp/project/ProjectFilePatternsTest.java +++ b/src/test/java/software/amazon/smithy/lsp/FilePatternsTest.java @@ -3,22 +3,26 @@ * SPDX-License-Identifier: Apache-2.0 */ -package software.amazon.smithy.lsp.project; +package software.amazon.smithy.lsp; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.not; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.util.HashSet; import org.junit.jupiter.api.Test; import software.amazon.smithy.build.model.SmithyBuildConfig; -import software.amazon.smithy.lsp.TestWorkspace; -import software.amazon.smithy.lsp.UtilMatchers; +import software.amazon.smithy.lsp.project.Project; +import software.amazon.smithy.lsp.project.ProjectLoader; +import software.amazon.smithy.lsp.project.ProjectManager; import software.amazon.smithy.utils.ListUtils; -public class ProjectFilePatternsTest { +public class FilePatternsTest { @Test - public void createsPathMatchers() { + public void createsProjectPathMatchers() { TestWorkspace workspace = TestWorkspace.builder() .withSourceDir(new TestWorkspace.Dir() .withPath("foo") @@ -38,8 +42,8 @@ public void createsPathMatchers() { .build(); Project project = ProjectLoader.load(workspace.getRoot(), new ProjectManager(), new HashSet<>()).unwrap(); - PathMatcher smithyMatcher = ProjectFilePatterns.getSmithyFilesPathMatcher(project); - PathMatcher buildMatcher = ProjectFilePatterns.getBuildFilesPathMatcher(project); + PathMatcher smithyMatcher = FilePatterns.getSmithyFilesPathMatcher(project); + PathMatcher buildMatcher = FilePatterns.getProjectBuildFilesPathMatcher(project); Path root = project.root(); assertThat(smithyMatcher, UtilMatchers.canMatchPath(root.resolve("abc.smithy"))); @@ -48,4 +52,29 @@ public void createsPathMatchers() { assertThat(buildMatcher, UtilMatchers.canMatchPath(root.resolve("smithy-build.json"))); assertThat(buildMatcher, UtilMatchers.canMatchPath(root.resolve(".smithy-project.json"))); } + + @Test + public void createsWorkspacePathMatchers() throws IOException { + Path workspaceRoot = Files.createTempDirectory("test"); + workspaceRoot.toFile().deleteOnExit(); + + TestWorkspace fooWorkspace = TestWorkspace.builder() + .withRoot(workspaceRoot) + .withPath("foo") + .build(); + + // Set up a project outside the 'foo' root. + workspaceRoot.resolve("bar").toFile().mkdir(); + workspaceRoot.resolve("bar/smithy-build.json").toFile().createNewFile(); + + Project fooProject = ProjectLoader.load(fooWorkspace.getRoot(), new ProjectManager(), new HashSet<>()).unwrap(); + + PathMatcher fooBuildMatcher = FilePatterns.getProjectBuildFilesPathMatcher(fooProject); + PathMatcher workspaceBuildMatcher = FilePatterns.getWorkspaceBuildFilesPathMatcher(workspaceRoot); + + assertThat(fooBuildMatcher, UtilMatchers.canMatchPath(workspaceRoot.resolve("foo/smithy-build.json"))); + assertThat(fooBuildMatcher, not(UtilMatchers.canMatchPath(workspaceRoot.resolve("bar/smithy-build.json")))); + assertThat(workspaceBuildMatcher, UtilMatchers.canMatchPath(workspaceRoot.resolve("foo/smithy-build.json"))); + assertThat(workspaceBuildMatcher, UtilMatchers.canMatchPath(workspaceRoot.resolve("bar/smithy-build.json"))); + } } diff --git a/src/test/java/software/amazon/smithy/lsp/handler/FileWatcherRegistrationHandlerTest.java b/src/test/java/software/amazon/smithy/lsp/FileWatcherRegistrationsTest.java similarity index 90% rename from src/test/java/software/amazon/smithy/lsp/handler/FileWatcherRegistrationHandlerTest.java rename to src/test/java/software/amazon/smithy/lsp/FileWatcherRegistrationsTest.java index 8b9df90a..2b839e72 100644 --- a/src/test/java/software/amazon/smithy/lsp/handler/FileWatcherRegistrationHandlerTest.java +++ b/src/test/java/software/amazon/smithy/lsp/FileWatcherRegistrationsTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package software.amazon.smithy.lsp.handler; +package software.amazon.smithy.lsp; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasItem; @@ -16,14 +16,12 @@ import org.eclipse.lsp4j.Registration; import org.junit.jupiter.api.Test; import software.amazon.smithy.build.model.SmithyBuildConfig; -import software.amazon.smithy.lsp.TestWorkspace; -import software.amazon.smithy.lsp.UtilMatchers; import software.amazon.smithy.lsp.project.Project; import software.amazon.smithy.lsp.project.ProjectLoader; import software.amazon.smithy.lsp.project.ProjectManager; import software.amazon.smithy.utils.ListUtils; -public class FileWatcherRegistrationHandlerTest { +public class FileWatcherRegistrationsTest { @Test public void createsCorrectRegistrations() { TestWorkspace workspace = TestWorkspace.builder() @@ -45,7 +43,7 @@ public void createsCorrectRegistrations() { .build(); Project project = ProjectLoader.load(workspace.getRoot(), new ProjectManager(), new HashSet<>()).unwrap(); - List matchers = FileWatcherRegistrationHandler.getSmithyFileWatcherRegistrations(List.of(project)) + List matchers = FileWatcherRegistrations.getSmithyFileWatcherRegistrations(List.of(project)) .stream() .map(Registration::getRegisterOptions) .map(o -> (DidChangeWatchedFilesRegistrationOptions) o) diff --git a/src/test/java/software/amazon/smithy/lsp/ProjectRootVisitorTest.java b/src/test/java/software/amazon/smithy/lsp/ProjectRootVisitorTest.java new file mode 100644 index 00000000..d8f9b241 --- /dev/null +++ b/src/test/java/software/amazon/smithy/lsp/ProjectRootVisitorTest.java @@ -0,0 +1,32 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.lsp; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static software.amazon.smithy.lsp.project.ProjectTest.toPath; + +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.Test; + +public class ProjectRootVisitorTest { + @Test + public void findsNestedRoot() throws Exception { + Path root = toPath(getClass().getResource("project/nested")); + List found = ProjectRootVisitor.findProjectRoots(root); + assertThat(found, contains(UtilMatchers.endsWith(Path.of("nested/nested")))); + } + + @Test + public void findsMultiNestedRoots() throws Exception { + Path root = toPath(getClass().getResource("project/multi-nested")); + List found = ProjectRootVisitor.findProjectRoots(root); + assertThat(found, containsInAnyOrder( + UtilMatchers.endsWith(Path.of("multi-nested/nested-a")), + UtilMatchers.endsWith(Path.of("multi-nested/nested-b")))); + } +} diff --git a/src/test/java/software/amazon/smithy/lsp/SmithyLanguageServerTest.java b/src/test/java/software/amazon/smithy/lsp/SmithyLanguageServerTest.java index 22ea3400..25f4e3ed 100644 --- a/src/test/java/software/amazon/smithy/lsp/SmithyLanguageServerTest.java +++ b/src/test/java/software/amazon/smithy/lsp/SmithyLanguageServerTest.java @@ -2,6 +2,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.anEmptyMap; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; @@ -30,6 +31,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; @@ -1766,7 +1768,6 @@ public void useCompletionDoesntAutoImport() throws Exception { @Test public void loadsMultipleRoots() { TestWorkspace workspaceFoo = TestWorkspace.builder() - .withName("foo") .withPath("foo") .withSourceFile("foo.smithy", """ $version: "2" @@ -1776,7 +1777,6 @@ public void loadsMultipleRoots() { .build(); TestWorkspace workspaceBar = TestWorkspace.builder() - .withName("bar") .withPath("bar") .withSourceFile("bar.smithy", """ $version: "2" @@ -1787,14 +1787,14 @@ public void loadsMultipleRoots() { SmithyLanguageServer server = initFromWorkspaces(workspaceFoo, workspaceBar); - assertThat(server.getProjects().attachedProjects(), hasKey("foo")); - assertThat(server.getProjects().attachedProjects(), hasKey("bar")); + assertThat(server.getProjects().attachedProjects(), hasKey(workspaceFoo.getName())); + assertThat(server.getProjects().attachedProjects(), hasKey(workspaceBar.getName())); assertThat(server.getProjects().getDocument(workspaceFoo.getUri("foo.smithy")), notNullValue()); assertThat(server.getProjects().getDocument(workspaceBar.getUri("bar.smithy")), notNullValue()); - Project projectFoo = server.getProjects().getProjectByName("foo"); - Project projectBar = server.getProjects().getProjectByName("bar"); + Project projectFoo = server.getProjects().getProjectByName(workspaceFoo.getName()); + Project projectBar = server.getProjects().getProjectByName(workspaceBar.getName()); assertThat(projectFoo.smithyFiles(), hasKey(endsWith("foo.smithy"))); assertThat(projectBar.smithyFiles(), hasKey(endsWith("bar.smithy"))); @@ -1806,7 +1806,6 @@ public void loadsMultipleRoots() { @Test public void multiRootLifecycleManagement() throws Exception { TestWorkspace workspaceFoo = TestWorkspace.builder() - .withName("foo") .withPath("foo") .withSourceFile("foo.smithy", """ $version: "2" @@ -1816,7 +1815,6 @@ public void multiRootLifecycleManagement() throws Exception { .build(); TestWorkspace workspaceBar = TestWorkspace.builder() - .withName("bar") .withPath("bar") .withSourceFile("bar.smithy", """ $version: "2" @@ -1857,8 +1855,8 @@ public void multiRootLifecycleManagement() throws Exception { server.getLifecycleManager().waitForAllTasks(); - Project projectFoo = server.getProjects().getProjectByName("foo"); - Project projectBar = server.getProjects().getProjectByName("bar"); + Project projectFoo = server.getProjects().getProjectByName(workspaceFoo.getName()); + Project projectBar = server.getProjects().getProjectByName(workspaceBar.getName()); assertThat(projectFoo.modelResult(), SmithyMatchers.hasValue(SmithyMatchers.hasShapeWithId("com.foo#Foo"))); assertThat(projectFoo.modelResult(), SmithyMatchers.hasValue(SmithyMatchers.hasShapeWithId("com.foo#Bar"))); @@ -1870,14 +1868,12 @@ public void multiRootLifecycleManagement() throws Exception { @Test public void multiRootAddingWatchedFile() throws Exception { TestWorkspace workspaceFoo = TestWorkspace.builder() - .withName("foo") .withPath("foo") .withSourceDir(new TestWorkspace.Dir() .withPath("model") .withSourceFile("main.smithy", "")) .build(); TestWorkspace workspaceBar = TestWorkspace.builder() - .withName("bar") .withPath("bar") .withSourceDir(new TestWorkspace.Dir() .withPath("model") @@ -1930,8 +1926,8 @@ public void multiRootAddingWatchedFile() throws Exception { server.getLifecycleManager().waitForAllTasks(); - Project projectFoo = server.getProjects().getProjectByName("foo"); - Project projectBar = server.getProjects().getProjectByName("bar"); + Project projectFoo = server.getProjects().getProjectByName(workspaceFoo.getName()); + Project projectBar = server.getProjects().getProjectByName(workspaceBar.getName()); assertThat(projectFoo.smithyFiles(), hasKey(endsWith("main.smithy"))); assertThat(projectBar.smithyFiles(), hasKey(endsWith("main.smithy"))); @@ -1945,14 +1941,12 @@ public void multiRootAddingWatchedFile() throws Exception { @Test public void multiRootChangingBuildFile() throws Exception { TestWorkspace workspaceFoo = TestWorkspace.builder() - .withName("foo") .withPath("foo") .withSourceDir(new TestWorkspace.Dir() .withPath("model") .withSourceFile("main.smithy", "")) .build(); TestWorkspace workspaceBar = TestWorkspace.builder() - .withName("bar") .withPath("bar") .withSourceDir(new TestWorkspace.Dir() .withPath("model") @@ -2017,8 +2011,8 @@ public void multiRootChangingBuildFile() throws Exception { assertThat(server.getProjects().getProject(workspaceBar.getUri("model/main.smithy")), notNullValue()); assertThat(server.getProjects().getProject(workspaceFoo.getUri("model/main.smithy")), notNullValue()); - Project projectFoo = server.getProjects().getProjectByName("foo"); - Project projectBar = server.getProjects().getProjectByName("bar"); + Project projectFoo = server.getProjects().getProjectByName(workspaceFoo.getName()); + Project projectBar = server.getProjects().getProjectByName(workspaceBar.getName()); assertThat(projectFoo.smithyFiles(), hasKey(endsWith("main.smithy"))); assertThat(projectBar.smithyFiles(), hasKey(endsWith("main.smithy"))); @@ -2038,7 +2032,6 @@ public void addingWorkspaceFolder() throws Exception { structure Foo {} """; TestWorkspace workspaceFoo = TestWorkspace.builder() - .withName("foo") .withPath("foo") .withSourceFile("foo.smithy", fooModel) .build(); @@ -2051,7 +2044,6 @@ public void addingWorkspaceFolder() throws Exception { structure Bar {} """; TestWorkspace workspaceBar = TestWorkspace.builder() - .withName("bar") .withPath("bar") .withSourceFile("bar.smithy", barModel) .build(); @@ -2072,14 +2064,14 @@ public void addingWorkspaceFolder() throws Exception { server.getLifecycleManager().waitForAllTasks(); - assertThat(server.getProjects().attachedProjects(), hasKey("foo")); - assertThat(server.getProjects().attachedProjects(), hasKey("bar")); + assertThat(server.getProjects().attachedProjects(), hasKey(workspaceFoo.getName())); + assertThat(server.getProjects().attachedProjects(), hasKey(workspaceBar.getName())); assertThat(server.getProjects().getDocument(workspaceFoo.getUri("foo.smithy")), notNullValue()); assertThat(server.getProjects().getDocument(workspaceBar.getUri("bar.smithy")), notNullValue()); - Project projectFoo = server.getProjects().getProjectByName("foo"); - Project projectBar = server.getProjects().getProjectByName("bar"); + Project projectFoo = server.getProjects().getProjectByName(workspaceFoo.getName()); + Project projectBar = server.getProjects().getProjectByName(workspaceBar.getName()); assertThat(projectFoo.smithyFiles(), hasKey(endsWith("foo.smithy"))); assertThat(projectBar.smithyFiles(), hasKey(endsWith("bar.smithy"))); @@ -2096,7 +2088,6 @@ public void removingWorkspaceFolder() { structure Foo {} """; TestWorkspace workspaceFoo = TestWorkspace.builder() - .withName("foo") .withPath("foo") .withSourceFile("foo.smithy", fooModel) .build(); @@ -2107,7 +2098,6 @@ public void removingWorkspaceFolder() { structure Bar {} """; TestWorkspace workspaceBar = TestWorkspace.builder() - .withName("bar") .withPath("bar") .withSourceFile("bar.smithy", barModel) .build(); @@ -2128,15 +2118,15 @@ public void removingWorkspaceFolder() { .removed(workspaceBar.getRoot().toUri().toString(), "bar") .build()); - assertThat(server.getProjects().attachedProjects(), hasKey("foo")); - assertThat(server.getProjects().attachedProjects(), not(hasKey("bar"))); + assertThat(server.getProjects().attachedProjects(), hasKey(workspaceFoo.getName())); + assertThat(server.getProjects().attachedProjects(), not(hasKey(workspaceBar.getName()))); assertThat(server.getProjects().detachedProjects(), hasKey(endsWith("bar.smithy"))); assertThat(server.getProjects().isDetached(workspaceBar.getUri("bar.smithy")), is(true)); assertThat(server.getProjects().getDocument(workspaceFoo.getUri("foo.smithy")), notNullValue()); assertThat(server.getProjects().getDocument(workspaceBar.getUri("bar.smithy")), notNullValue()); - Project projectFoo = server.getProjects().getProjectByName("foo"); + Project projectFoo = server.getProjects().getProjectByName(workspaceFoo.getName()); Project projectBar = server.getProjects().getProject(workspaceBar.getUri("bar.smithy")); assertThat(projectFoo.smithyFiles(), hasKey(endsWith("foo.smithy"))); @@ -2145,6 +2135,247 @@ public void removingWorkspaceFolder() { assertThat(projectFoo.modelResult(), SmithyMatchers.hasValue(SmithyMatchers.hasShapeWithId("com.foo#Foo"))); assertThat(projectBar.modelResult(), SmithyMatchers.hasValue(SmithyMatchers.hasShapeWithId("com.bar#Bar"))); } + + @Test + public void singleWorkspaceMultiRoot() throws Exception { + Path root = Files.createTempDirectory("test"); + root.toFile().deleteOnExit(); + + String fooModel = """ + $version: "2" + namespace com.foo + structure Foo {} + """; + TestWorkspace workspaceFoo = TestWorkspace.builder() + .withRoot(root) + .withPath("foo") + .withSourceFile("foo.smithy", fooModel) + .build(); + + String barModel = """ + $version: "2" + namespace com.bar + structure Bar {} + """; + TestWorkspace workspaceBar = TestWorkspace.builder() + .withRoot(root) + .withPath("bar") + .withSourceFile("bar.smithy", barModel) + .build(); + + SmithyLanguageServer server = initFromRoot(root); + + assertThat(server.getProjects().attachedProjects().keySet(), containsInAnyOrder( + workspaceFoo.getName(), + workspaceBar.getName())); + assertThat(server.getWorkspacePaths(), contains(root)); + } + + @Test + public void addingRootsToWorkspace() throws Exception { + Path root = Files.createTempDirectory("test"); + root.toFile().deleteOnExit(); + + SmithyLanguageServer server = initFromRoot(root); + + String fooModel = """ + $version: "2" + namespace com.foo + structure Foo {} + """; + TestWorkspace workspaceFoo = TestWorkspace.builder() + .withRoot(root) + .withPath("foo") + .withSourceFile("foo.smithy", fooModel) + .build(); + + String barModel = """ + $version: "2" + namespace com.bar + structure Bar {} + """; + TestWorkspace workspaceBar = TestWorkspace.builder() + .withRoot(root) + .withPath("bar") + .withSourceFile("bar.smithy", barModel) + .build(); + + server.didChangeWatchedFiles(RequestBuilders.didChangeWatchedFiles() + .event(workspaceFoo.getUri("smithy-build.json"), FileChangeType.Created) + .event(workspaceBar.getUri("smithy-build.json"), FileChangeType.Created) + .build()); + + assertThat(server.getWorkspacePaths(), contains(root)); + assertThat(server.getProjects().attachedProjects().keySet(), containsInAnyOrder( + workspaceFoo.getName(), + workspaceBar.getName())); + } + + @Test + public void removingRootsFromWorkspace() throws Exception { + Path root = Files.createTempDirectory("test"); + root.toFile().deleteOnExit(); + + String fooModel = """ + $version: "2" + namespace com.foo + structure Foo {} + """; + TestWorkspace workspaceFoo = TestWorkspace.builder() + .withRoot(root) + .withPath("foo") + .withSourceFile("foo.smithy", fooModel) + .build(); + + String barModel = """ + $version: "2" + namespace com.bar + structure Bar {} + """; + TestWorkspace workspaceBar = TestWorkspace.builder() + .withRoot(root) + .withPath("bar") + .withSourceFile("bar.smithy", barModel) + .build(); + + SmithyLanguageServer server = initFromRoot(root); + + assertThat(server.getProjects().attachedProjects().keySet(), containsInAnyOrder( + workspaceFoo.getName(), + workspaceBar.getName())); + assertThat(server.getWorkspacePaths(), contains(root)); + + workspaceFoo.deleteModel("smithy-build.json"); + + server.didChangeWatchedFiles(RequestBuilders.didChangeWatchedFiles() + .event(workspaceFoo.getUri("smithy-build.json"), FileChangeType.Deleted) + .build()); + + assertThat(server.getWorkspacePaths(), contains(root)); + assertThat(server.getProjects().attachedProjects().keySet(), contains(workspaceBar.getName())); + } + + @Test + public void addingConfigFile() throws Exception { + Path root = Files.createTempDirectory("test"); + root.toFile().deleteOnExit(); + + String fooModel = """ + $version: "2" + namespace com.foo + structure Foo {} + """; + TestWorkspace workspaceFoo = TestWorkspace.builder() + .withRoot(root) + .withPath("foo") + .withSourceFile("foo.smithy", fooModel) + .build(); + + String barModel = """ + $version: "2" + namespace com.bar + structure Bar {} + """; + TestWorkspace workspaceBar = TestWorkspace.builder() + .withRoot(root) + .withPath("bar") + .withSourceFile("bar.smithy", barModel) + .build(); + + SmithyLanguageServer server = initFromRoot(root); + + String bazModel = """ + $version: "2" + namespace com.baz + structure Baz {} + """; + workspaceFoo.addModel("baz.smithy", bazModel); + server.didOpen(RequestBuilders.didOpen() + .uri(workspaceFoo.getUri("baz.smithy")) + .text(bazModel) + .build()); + + assertThat(server.getWorkspacePaths(), contains(root)); + assertThat(server.getProjects().attachedProjects().keySet(), containsInAnyOrder( + workspaceFoo.getName(), + workspaceBar.getName())); + assertThat(server.getProjects().detachedProjects().keySet(), contains(workspaceFoo.getUri("baz.smithy"))); + + workspaceFoo.addModel(".smithy-project.json", """ + { + "sources": ["baz.smithy"] + }"""); + server.didChangeWatchedFiles(RequestBuilders.didChangeWatchedFiles() + .event(workspaceFoo.getUri(".smithy-project.json"), FileChangeType.Created) + .build()); + + assertThat(server.getProjects().attachedProjects().keySet(), containsInAnyOrder( + workspaceFoo.getName(), + workspaceBar.getName())); + assertThat(server.getProjects().detachedProjects().keySet(), empty()); + } + + @Test + public void removingConfigFile() throws Exception { + Path root = Files.createTempDirectory("test"); + root.toFile().deleteOnExit(); + + String fooModel = """ + $version: "2" + namespace com.foo + structure Foo {} + """; + TestWorkspace workspaceFoo = TestWorkspace.builder() + .withRoot(root) + .withPath("foo") + .withSourceFile("foo.smithy", fooModel) + .build(); + String bazModel = """ + $version: "2" + namespace com.baz + structure Baz {} + """; + workspaceFoo.addModel("baz.smithy", bazModel); + workspaceFoo.addModel(".smithy-project.json", """ + { + "sources": ["baz.smithy"] + }"""); + + String barModel = """ + $version: "2" + namespace com.bar + structure Bar {} + """; + TestWorkspace workspaceBar = TestWorkspace.builder() + .withRoot(root) + .withPath("bar") + .withSourceFile("bar.smithy", barModel) + .build(); + + SmithyLanguageServer server = initFromRoot(root); + + server.didOpen(RequestBuilders.didOpen() + .uri(workspaceFoo.getUri("baz.smithy")) + .text(bazModel) + .build()); + + assertThat(server.getWorkspacePaths(), contains(root)); + assertThat(server.getProjects().attachedProjects().keySet(), containsInAnyOrder( + workspaceFoo.getName(), + workspaceBar.getName())); + assertThat(server.getProjects().detachedProjects().keySet(), empty()); + + workspaceFoo.deleteModel(".smithy-project.json"); + server.didChangeWatchedFiles(RequestBuilders.didChangeWatchedFiles() + .event(workspaceFoo.getUri(".smithy-project.json"), FileChangeType.Deleted) + .build()); + + assertThat(server.getProjects().attachedProjects().keySet(), containsInAnyOrder( + workspaceFoo.getName(), + workspaceBar.getName())); + assertThat(server.getProjects().detachedProjects().keySet(), contains(workspaceFoo.getUri("baz.smithy"))); + } + public static SmithyLanguageServer initFromWorkspace(TestWorkspace workspace) { return initFromWorkspace(workspace, new StubClient()); } diff --git a/src/test/java/software/amazon/smithy/lsp/TestWorkspace.java b/src/test/java/software/amazon/smithy/lsp/TestWorkspace.java index f95c0fe3..ab6946fb 100644 --- a/src/test/java/software/amazon/smithy/lsp/TestWorkspace.java +++ b/src/test/java/software/amazon/smithy/lsp/TestWorkspace.java @@ -25,10 +25,10 @@ public final class TestWorkspace { private SmithyBuildConfig config; private final String name; - private TestWorkspace(Path root, SmithyBuildConfig config, String name) { + private TestWorkspace(Path root, SmithyBuildConfig config) { this.root = root; this.config = config; - this.name = name; + this.name = root.toString(); } /** @@ -183,7 +183,7 @@ private static void writeModels(Path toDir, Map models) throws E public static final class Builder extends Dir { private SmithyBuildConfig config = null; - private String name = ""; + private Path root = null; private Builder() {} @@ -222,8 +222,8 @@ public Builder withConfig(SmithyBuildConfig config) { return this; } - public Builder withName(String name) { - this.name = name; + public Builder withRoot(Path root) { + this.root = root; return this; } @@ -232,8 +232,13 @@ public TestWorkspace build() { if (path == null) { path = "test"; } - Path root = Files.createTempDirectory(path); - root.toFile().deleteOnExit(); + Path projectRoot; + if (this.root != null) { + projectRoot = Files.createDirectory(this.root.resolve(path)); + } else { + projectRoot = Files.createTempDirectory(path); + projectRoot.toFile().deleteOnExit(); + } List sources = new ArrayList<>(); sources.addAll(sourceModels.keySet()); @@ -250,11 +255,11 @@ public TestWorkspace build() { .imports(imports) .build(); } - writeConfig(root, config); + writeConfig(projectRoot, config); - writeModels(root); + writeModels(projectRoot); - return new TestWorkspace(root, config, name); + return new TestWorkspace(projectRoot, config); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/src/test/java/software/amazon/smithy/lsp/UtilMatchers.java b/src/test/java/software/amazon/smithy/lsp/UtilMatchers.java index f4dc1103..58b3f6d0 100644 --- a/src/test/java/software/amazon/smithy/lsp/UtilMatchers.java +++ b/src/test/java/software/amazon/smithy/lsp/UtilMatchers.java @@ -53,4 +53,18 @@ protected void describeMismatchSafely(PathMatcher item, Description mismatchDesc } }; } + + public static Matcher endsWith(Path path) { + return new CustomTypeSafeMatcher("A path that ends with " + path.toString()) { + @Override + protected boolean matchesSafely(Path item) { + return item.endsWith(path); + } + + @Override + protected void describeMismatchSafely(Path item, Description mismatchDescription) { + mismatchDescription.appendText(item.toString() + " did not end with " + path.toString()); + } + }; + } } diff --git a/src/test/resources/software/amazon/smithy/lsp/project/multi-nested/nested-a/smithy-build.json b/src/test/resources/software/amazon/smithy/lsp/project/multi-nested/nested-a/smithy-build.json new file mode 100644 index 00000000..e69de29b diff --git a/src/test/resources/software/amazon/smithy/lsp/project/multi-nested/nested-b/.smithy-project.json b/src/test/resources/software/amazon/smithy/lsp/project/multi-nested/nested-b/.smithy-project.json new file mode 100644 index 00000000..e69de29b diff --git a/src/test/resources/software/amazon/smithy/lsp/project/nested/nested/smithy-build.json b/src/test/resources/software/amazon/smithy/lsp/project/nested/nested/smithy-build.json new file mode 100644 index 00000000..e69de29b From 24a93bbc4eae9a2b5427d8e4da40b035d744d670 Mon Sep 17 00:00:00 2001 From: Miles Ziemer Date: Thu, 26 Sep 2024 10:36:02 -0400 Subject: [PATCH 2/3] Fix deadlock in didChangeWorkspaceFolders Blocking on the future creating the progress token with the client means the server can't actually receive the response from the client for that request. Tests don't catch this because the mock client is called directly, rather than through the server proxy. I decided to just remove the progress token code for now so didChangeWorkspaceFolders can work at all, rather than trying to make the method work asynchronously, which is a larger lift considering it mutates the server state. That change is coming though. --- .../smithy/lsp/SmithyLanguageServer.java | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/src/main/java/software/amazon/smithy/lsp/SmithyLanguageServer.java b/src/main/java/software/amazon/smithy/lsp/SmithyLanguageServer.java index d80d4a9a..f2efd72b 100644 --- a/src/main/java/software/amazon/smithy/lsp/SmithyLanguageServer.java +++ b/src/main/java/software/amazon/smithy/lsp/SmithyLanguageServer.java @@ -33,9 +33,7 @@ import java.util.Optional; import java.util.Properties; import java.util.Set; -import java.util.UUID; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -84,7 +82,6 @@ import org.eclipse.lsp4j.Unregistration; import org.eclipse.lsp4j.UnregistrationParams; import org.eclipse.lsp4j.WorkDoneProgressBegin; -import org.eclipse.lsp4j.WorkDoneProgressCreateParams; import org.eclipse.lsp4j.WorkDoneProgressEnd; import org.eclipse.lsp4j.WorkspaceFolder; import org.eclipse.lsp4j.WorkspaceFoldersOptions; @@ -471,20 +468,6 @@ public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) { public void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) { LOGGER.finest("DidChangeWorkspaceFolders"); - Either progressToken = Either.forLeft(UUID.randomUUID().toString()); - try { - client.createProgress(new WorkDoneProgressCreateParams(progressToken)).get(); - } catch (ExecutionException | InterruptedException e) { - client.error(String.format("Unable to create work done progress token: %s", e.getMessage())); - progressToken = null; - } - - if (progressToken != null) { - WorkDoneProgressBegin begin = new WorkDoneProgressBegin(); - begin.setTitle("Updating workspace"); - client.notifyProgress(new ProgressParams(progressToken, Either.forLeft(begin))); - } - for (WorkspaceFolder folder : params.getEvent().getAdded()) { loadWorkspace(folder); } @@ -496,11 +479,6 @@ public void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) { unregisterSmithyFileWatchers().thenRun(this::registerSmithyFileWatchers); unregisterWorkspaceBuildFileWatchers().thenRun(this::registerWorkspaceBuildFileWatchers); sendFileDiagnosticsForManagedDocuments(); - - if (progressToken != null) { - WorkDoneProgressEnd end = new WorkDoneProgressEnd(); - client.notifyProgress(new ProgressParams(progressToken, Either.forLeft(end))); - } } private void loadWorkspace(WorkspaceFolder workspaceFolder) { From b2ffd657f614d626233daf814c20690e5101078b Mon Sep 17 00:00:00 2001 From: Miles Ziemer Date: Mon, 28 Oct 2024 16:42:47 -0400 Subject: [PATCH 3/3] Address comments --- .../software/amazon/smithy/lsp/FilePatterns.java | 13 +------------ .../amazon/smithy/lsp/ProjectRootVisitor.java | 5 ++++- .../smithy/lsp/project/ProjectConfigLoader.java | 12 +++++++++++- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/main/java/software/amazon/smithy/lsp/FilePatterns.java b/src/main/java/software/amazon/smithy/lsp/FilePatterns.java index 34d407e6..23bab11f 100644 --- a/src/main/java/software/amazon/smithy/lsp/FilePatterns.java +++ b/src/main/java/software/amazon/smithy/lsp/FilePatterns.java @@ -9,7 +9,6 @@ import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.PathMatcher; -import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -21,8 +20,6 @@ * or build files in Projects and workspaces. */ final class FilePatterns { - private static final int BUILD_FILE_COUNT = 2 + ProjectConfigLoader.SMITHY_BUILD_EXTS.length; - private FilePatterns() { } @@ -81,14 +78,6 @@ private static PathMatcher toPathMatcher(String globPattern) { // whereas patterns for projects only look at the top level (because project locations // are defined by the presence of these build files). private static String getBuildFilesPattern(Path root, boolean isWorkspacePattern) { - List patterns = new ArrayList<>(BUILD_FILE_COUNT); - patterns.add(ProjectConfigLoader.SMITHY_BUILD); - patterns.add(ProjectConfigLoader.SMITHY_PROJECT); - for (String buildExt : ProjectConfigLoader.SMITHY_BUILD_EXTS) { - Path extPath = Path.of(buildExt); // buildExt may have file separators - patterns.add(extPath.toString()); - } - String rootString = root.toString(); if (!rootString.endsWith(File.separator)) { rootString += File.separator; @@ -98,7 +87,7 @@ private static String getBuildFilesPattern(Path root, boolean isWorkspacePattern rootString += "**" + File.separator; } - return escapeBackslashes(rootString + "{" + String.join(",", patterns) + "}"); + return escapeBackslashes(rootString + "{" + String.join(",", ProjectConfigLoader.PROJECT_BUILD_FILES) + "}"); } // When computing the pattern used for telling the client which files to watch, we want diff --git a/src/main/java/software/amazon/smithy/lsp/ProjectRootVisitor.java b/src/main/java/software/amazon/smithy/lsp/ProjectRootVisitor.java index d99f75e4..a1016b9d 100644 --- a/src/main/java/software/amazon/smithy/lsp/ProjectRootVisitor.java +++ b/src/main/java/software/amazon/smithy/lsp/ProjectRootVisitor.java @@ -7,6 +7,7 @@ import java.io.IOException; import java.nio.file.FileSystems; +import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; @@ -14,6 +15,7 @@ import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; import software.amazon.smithy.lsp.project.ProjectConfigLoader; @@ -23,6 +25,7 @@ final class ProjectRootVisitor extends SimpleFileVisitor { private static final PathMatcher PROJECT_ROOT_MATCHER = FileSystems.getDefault().getPathMatcher( "glob:{" + ProjectConfigLoader.SMITHY_BUILD + "," + ProjectConfigLoader.SMITHY_PROJECT + "}"); + private static final int MAX_VISIT_DEPTH = 10; private final List roots = new ArrayList<>(); @@ -36,7 +39,7 @@ final class ProjectRootVisitor extends SimpleFileVisitor { */ static List findProjectRoots(Path workspaceRoot) throws IOException { ProjectRootVisitor visitor = new ProjectRootVisitor(); - Files.walkFileTree(workspaceRoot, visitor); + Files.walkFileTree(workspaceRoot, EnumSet.noneOf(FileVisitOption.class), MAX_VISIT_DEPTH, visitor); return visitor.roots; } diff --git a/src/main/java/software/amazon/smithy/lsp/project/ProjectConfigLoader.java b/src/main/java/software/amazon/smithy/lsp/project/ProjectConfigLoader.java index 77844694..71c12433 100644 --- a/src/main/java/software/amazon/smithy/lsp/project/ProjectConfigLoader.java +++ b/src/main/java/software/amazon/smithy/lsp/project/ProjectConfigLoader.java @@ -5,9 +5,11 @@ package software.amazon.smithy.lsp.project; +import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.logging.Logger; import software.amazon.smithy.build.model.SmithyBuildConfig; @@ -63,13 +65,21 @@ */ public final class ProjectConfigLoader { public static final String SMITHY_BUILD = "smithy-build.json"; - public static final String[] SMITHY_BUILD_EXTS = {"build/smithy-dependencies.json", ".smithy.json"}; + public static final String[] SMITHY_BUILD_EXTS = { + "build" + File.separator + "smithy-dependencies.json", ".smithy.json"}; public static final String SMITHY_PROJECT = ".smithy-project.json"; + public static final List PROJECT_BUILD_FILES = new ArrayList<>(2 + SMITHY_BUILD_EXTS.length); private static final Logger LOGGER = Logger.getLogger(ProjectConfigLoader.class.getName()); private static final SmithyBuildConfig DEFAULT_SMITHY_BUILD = SmithyBuildConfig.builder().version("1").build(); private static final NodeMapper NODE_MAPPER = new NodeMapper(); + static { + PROJECT_BUILD_FILES.add(SMITHY_BUILD); + PROJECT_BUILD_FILES.add(SMITHY_PROJECT); + PROJECT_BUILD_FILES.addAll(Arrays.asList(SMITHY_BUILD_EXTS)); + } + private ProjectConfigLoader() { }