Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -55,6 +55,7 @@
import com.microsoft.copilot.eclipse.core.lsp.protocol.LanguageModelToolResult.ToolInvocationStatus;
import com.microsoft.copilot.eclipse.core.lsp.protocol.OnChangeMcpServerToolsParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.RateLimitWarningParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.ReadDirectoryResult;
import com.microsoft.copilot.eclipse.core.lsp.protocol.ReadFileResult;
import com.microsoft.copilot.eclipse.core.lsp.protocol.codingagent.CodingAgentMessageRequestParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.codingagent.CodingAgentMessageResult;
Expand Down Expand Up @@ -335,6 +336,15 @@ public CompletableFuture<ReadFileResult> readFile(String uri) {
return CompletableFuture.supplyAsync(() -> FileUtils.readFileWithStats(uri));
}

/**
* Reads the contents of a directory given its URI. Used by the language server to list directory entries for URIs
* with external content provider schemes (e.g., semanticfs://) that cannot be read from the local file system.
*/
@JsonRequest("workspace/readDirectory")
public CompletableFuture<ReadDirectoryResult> readDirectory(String uri) {
return CompletableFuture.supplyAsync(() -> FileUtils.readDirectoryEntries(uri));
}
Comment thread
xinyi-gong marked this conversation as resolved.

/**
* Handles the progress notification for chat replies.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package com.microsoft.copilot.eclipse.core.lsp.protocol;

import java.util.List;
import java.util.Objects;

import org.apache.commons.lang3.builder.ToStringBuilder;

/**
* Result of reading a directory, containing the directory entries.
*/
public class ReadDirectoryResult {

private List<DirectoryEntry> entries;

/**
* Constructor with entries.
*
* @param entries the directory entries
*/
public ReadDirectoryResult(List<DirectoryEntry> entries) {
this.entries = entries;
}

/**
* Gets the directory entries.
*
* @return the directory entries
*/
public List<DirectoryEntry> getEntries() {
return entries;
}

/**
* Sets the directory entries.
*
* @param entries the directory entries
*/
public void setEntries(List<DirectoryEntry> entries) {
this.entries = entries;
}

@Override
public int hashCode() {
return Objects.hash(entries);
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
ReadDirectoryResult other = (ReadDirectoryResult) obj;
return Objects.equals(entries, other.entries);
}

@Override
public String toString() {
return new ToStringBuilder(this).append("entries", entries).toString();
}

/**
* A single directory entry with name and file type.
*/
public static class DirectoryEntry {

/** VS Code FileType constants. */
public static final int FILE_TYPE_UNKNOWN = 0;
public static final int FILE_TYPE_FILE = 1;
public static final int FILE_TYPE_DIRECTORY = 2;

private String name;
private int type;

/**
* Constructor with name and type.
*
* @param name the entry name
* @param type the file type (0=Unknown, 1=File, 2=Directory)
*/
public DirectoryEntry(String name, int type) {
this.name = name;
this.type = type;
}

/**
* Gets the entry name.
*/
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

/**
* Gets the file type.
*/
public int getType() {
return type;
}

public void setType(int type) {
this.type = type;
}

@Override
public int hashCode() {
return Objects.hash(name, type);
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
DirectoryEntry other = (DirectoryEntry) obj;
return Objects.equals(name, other.name) && type == other.type;
}

@Override
public String toString() {
return new ToStringBuilder(this).append("name", name).append("type", type).toString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IResource;
Expand All @@ -33,6 +37,8 @@
import com.microsoft.copilot.eclipse.core.lsp.protocol.DirectoryChatReference;
import com.microsoft.copilot.eclipse.core.lsp.protocol.FileChatReference;
import com.microsoft.copilot.eclipse.core.lsp.protocol.FileStat;
import com.microsoft.copilot.eclipse.core.lsp.protocol.ReadDirectoryResult;
import com.microsoft.copilot.eclipse.core.lsp.protocol.ReadDirectoryResult.DirectoryEntry;
import com.microsoft.copilot.eclipse.core.lsp.protocol.ReadFileResult;

/**
Expand Down Expand Up @@ -302,6 +308,80 @@ public static ReadFileResult readFileWithStats(String uri) {

}

/**
* Reads the contents of a directory given its URI. Used by workspace/readDirectory API to list directory entries.
* Works with local filesystem URIs, platform:/resource URIs (produced by {@link #getResourceUri(IResource)}), and
* virtual URIs (e.g., semanticfs://) through Eclipse's IResource API.
*
* @param uri the directory URI
* @return ReadDirectoryResult containing the directory entries
*/
public static ReadDirectoryResult readDirectoryEntries(String uri) {
if (StringUtils.isBlank(uri)) {
return new ReadDirectoryResult(Collections.emptyList());
}

try {
URI parsedUri = new URI(uri);
IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();

IContainer container = null;
if ("platform".equals(parsedUri.getScheme())) {
// Handle platform:/resource/... URIs by resolving via workspace path
String path = parsedUri.getPath();
String prefix = "/resource";
if (path != null && path.startsWith(prefix)) {
String workspacePath = path.substring(prefix.length());
IResource resource = root.findMember(workspacePath);
if (resource instanceof IContainer c && c.isAccessible()) {
container = c;
}
}
} else {
// For file://, semanticfs://, and other URIs, use location URI lookup
IContainer[] containers = root.findContainersForLocationURI(parsedUri);
if (containers != null) {
for (IContainer c : containers) {
if (c.isAccessible()) {
container = c;
break;
}
}
}
}
Comment thread
xinyi-gong marked this conversation as resolved.

if (container == null) {
return new ReadDirectoryResult(Collections.emptyList());
}

IResource[] members = container.members();
Comment thread
xinyi-gong marked this conversation as resolved.
List<DirectoryEntry> entries = new ArrayList<>();
for (IResource member : members) {
int type;
switch (member.getType()) {
case IResource.FILE:
type = DirectoryEntry.FILE_TYPE_FILE;
break;
case IResource.FOLDER:
case IResource.PROJECT:
type = DirectoryEntry.FILE_TYPE_DIRECTORY;
break;
default:
type = DirectoryEntry.FILE_TYPE_UNKNOWN;
break;
}
entries.add(new DirectoryEntry(member.getName(), type));
}
return new ReadDirectoryResult(entries);
} catch (URISyntaxException e) {
CopilotCore.LOGGER.error("Invalid directory URI: " + uri, e);
return new ReadDirectoryResult(Collections.emptyList());
} catch (CoreException e) {
CopilotCore.LOGGER.error("Failed to read directory: " + uri, e);
return new ReadDirectoryResult(Collections.emptyList());
}
}

private static String readFileContent(IFile file) throws CoreException, IOException {
try (InputStream is = file.getContents()) {
return new String(is.readAllBytes(), file.getCharset());
Expand Down
Loading