Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support projects in subdirectories #167

Merged
merged 3 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<String> getSmithyFileWatchPatterns(Project project) {
static List<String> getSmithyFileWatchPatterns(Project project) {
return Stream.concat(project.sources().stream(), project.imports().stream())
.map(path -> getSmithyFilePattern(path, true))
.toList();
Expand All @@ -38,45 +40,71 @@ public static List<String> 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<String> 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<String> 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
hpmellema marked this conversation as resolved.
Show resolved Hide resolved
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")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -32,25 +32,31 @@
* 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";
private static final String WATCH_FILES_METHOD = "workspace/didChangeWatchedFiles";
private static final List<Unregistration> SMITHY_FILE_WATCHER_UNREGISTRATIONS = List.of(new Unregistration(
WATCH_SMITHY_FILES_ID,
WATCH_FILES_METHOD));
private static final List<Unregistration> 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<Registration> getSmithyFileWatcherRegistrations(Collection<Project> projects) {
static List<Registration> getSmithyFileWatcherRegistrations(Collection<Project> projects) {
List<FileSystemWatcher> 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();

Expand All @@ -63,17 +69,20 @@ public static List<Registration> getSmithyFileWatcherRegistrations(Collection<Pr
/**
* @return The unregistrations to stop watching for Smithy file changes
*/
public static List<Unregistration> getSmithyFileWatcherUnregistrations() {
static List<Unregistration> 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<Registration> getBuildFileWatcherRegistrations(Collection<Project> projects) {
List<FileSystemWatcher> watchers = projects.stream()
.map(ProjectFilePatterns::getBuildFilesWatchPattern)
static List<Registration> getBuildFileWatcherRegistrations(Collection<Path> workspaceRoots) {
List<FileSystemWatcher> watchers = workspaceRoots.stream()
.map(FilePatterns::getWorkspaceBuildFilesWatchPattern)
.map(pattern -> new FileSystemWatcher(Either.forLeft(pattern)))
.toList();

Expand All @@ -82,4 +91,8 @@ public static List<Registration> getBuildFileWatcherRegistrations(Collection<Pro
WATCH_FILES_METHOD,
new DidChangeWatchedFilesRegistrationOptions(watchers)));
}

static List<Unregistration> getBuildFileWatcherUnregistrations() {
return BUILD_FILE_WATCHER_UNREGISTRATIONS;
}
}
Original file line number Diff line number Diff line change
@@ -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<Path> {
private static final PathMatcher PROJECT_ROOT_MATCHER = FileSystems.getDefault().getPathMatcher(
"glob:{" + ProjectConfigLoader.SMITHY_BUILD + "," + ProjectConfigLoader.SMITHY_PROJECT + "}");

private final List<Path> 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<Path> findProjectRoots(Path workspaceRoot) throws IOException {
ProjectRootVisitor visitor = new ProjectRootVisitor();
Files.walkFileTree(workspaceRoot, visitor);
hpmellema marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
Loading
Loading