implements PLN-0020
This commit is contained in:
parent
2b7aafaa5c
commit
f9a47bbdbf
@ -11,4 +11,4 @@
|
|||||||
{"type":"discussion","id":"DSC-0010","status":"done","ticket":"studio-code-editor-workspace-foundations","title":"Establish Code Editor workspace foundations in Studio without LSP","created_at":"2026-03-30","updated_at":"2026-03-31","tags":["studio","editor","workspace","multi-frontend","lsp-deferred"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0026","file":"discussion/lessons/DSC-0010-studio-code-editor-workspace-foundations/LSN-0026-read-only-editor-foundations-and-semantic-deferral.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31"}]}
|
{"type":"discussion","id":"DSC-0010","status":"done","ticket":"studio-code-editor-workspace-foundations","title":"Establish Code Editor workspace foundations in Studio without LSP","created_at":"2026-03-30","updated_at":"2026-03-31","tags":["studio","editor","workspace","multi-frontend","lsp-deferred"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0026","file":"discussion/lessons/DSC-0010-studio-code-editor-workspace-foundations/LSN-0026-read-only-editor-foundations-and-semantic-deferral.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31"}]}
|
||||||
{"type":"discussion","id":"DSC-0011","status":"done","ticket":"compiler-analyze-compile-build-pipeline-split","title":"Split compiler pipeline into analyze, compile, and build entrypoints","created_at":"2026-03-30","updated_at":"2026-03-30","tags":["compiler","pipeline","artifacts","build","analysis"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0025","file":"discussion/lessons/DSC-0011-compiler-analyze-compile-build-pipeline-split/LSN-0025-compiler-pipeline-entrypoints-and-result-boundaries.md","status":"done","created_at":"2026-03-30","updated_at":"2026-03-30"}]}
|
{"type":"discussion","id":"DSC-0011","status":"done","ticket":"compiler-analyze-compile-build-pipeline-split","title":"Split compiler pipeline into analyze, compile, and build entrypoints","created_at":"2026-03-30","updated_at":"2026-03-30","tags":["compiler","pipeline","artifacts","build","analysis"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0025","file":"discussion/lessons/DSC-0011-compiler-analyze-compile-build-pipeline-split/LSN-0025-compiler-pipeline-entrypoints-and-result-boundaries.md","status":"done","created_at":"2026-03-30","updated_at":"2026-03-30"}]}
|
||||||
{"type":"discussion","id":"DSC-0012","status":"done","ticket":"studio-editor-document-vfs-boundary","title":"Definir um boundary de VFS documental para tree/view/open files no Code Editor do Studio","created_at":"2026-03-31","updated_at":"2026-03-31","tags":["studio","editor","workspace","vfs","filesystem","boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0027","file":"discussion/lessons/DSC-0012-studio-editor-document-vfs-boundary/LSN-0027-project-document-vfs-and-session-owned-editor-boundary.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31"}]}
|
{"type":"discussion","id":"DSC-0012","status":"done","ticket":"studio-editor-document-vfs-boundary","title":"Definir um boundary de VFS documental para tree/view/open files no Code Editor do Studio","created_at":"2026-03-31","updated_at":"2026-03-31","tags":["studio","editor","workspace","vfs","filesystem","boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0027","file":"discussion/lessons/DSC-0012-studio-editor-document-vfs-boundary/LSN-0027-project-document-vfs-and-session-owned-editor-boundary.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31"}]}
|
||||||
{"type":"discussion","id":"DSC-0013","status":"open","ticket":"studio-editor-write-wave-supported-non-frontend-files","title":"Definir a wave inicial de edicao no Code Editor apenas para arquivos aceitos e nao relacionados ao FE","created_at":"2026-03-31","updated_at":"2026-03-31","tags":["studio","editor","workspace","write","read-only","vfs","frontend-boundary"],"agendas":[{"id":"AGD-0013","file":"AGD-0013-studio-editor-write-wave-supported-non-frontend-files.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31"},{"id":"AGD-0014","file":"AGD-0014-studio-editor-frontend-edit-rights.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31"}],"decisions":[{"id":"DEC-0010","file":"DEC-0010-studio-controlled-non-frontend-editor-write-wave.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31","ref_agenda":"AGD-0013"},{"id":"DEC-0011","file":"DEC-0011-studio-frontend-read-only-minimum-lsp-phase.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31","ref_agenda":"AGD-0014"}],"plans":[{"id":"PLN-0019","file":"PLN-0019-propagate-dec-0010-into-studio-and-vfs-specs.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0010"]},{"id":"PLN-0020","file":"PLN-0020-build-dec-0010-vfs-access-policy-and-save-core.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0010"]},{"id":"PLN-0021","file":"PLN-0021-integrate-dec-0010-editor-write-ui-and-workflow.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0010"]},{"id":"PLN-0022","file":"PLN-0022-propagate-dec-0011-into-studio-vfs-and-lsp-specs.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]},{"id":"PLN-0023","file":"PLN-0023-scaffold-flat-packed-prometeu-lsp-api-and-session-seams.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]},{"id":"PLN-0024","file":"PLN-0024-implement-read-only-fe-diagnostics-symbols-and-definition.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]},{"id":"PLN-0025","file":"PLN-0025-implement-fe-semantic-highlight-consumption.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]}],"lessons":[]}
|
{"type":"discussion","id":"DSC-0013","status":"open","ticket":"studio-editor-write-wave-supported-non-frontend-files","title":"Definir a wave inicial de edicao no Code Editor apenas para arquivos aceitos e nao relacionados ao FE","created_at":"2026-03-31","updated_at":"2026-03-31","tags":["studio","editor","workspace","write","read-only","vfs","frontend-boundary"],"agendas":[{"id":"AGD-0013","file":"AGD-0013-studio-editor-write-wave-supported-non-frontend-files.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31"},{"id":"AGD-0014","file":"AGD-0014-studio-editor-frontend-edit-rights.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31"}],"decisions":[{"id":"DEC-0010","file":"DEC-0010-studio-controlled-non-frontend-editor-write-wave.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31","ref_agenda":"AGD-0013"},{"id":"DEC-0011","file":"DEC-0011-studio-frontend-read-only-minimum-lsp-phase.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31","ref_agenda":"AGD-0014"}],"plans":[{"id":"PLN-0019","file":"PLN-0019-propagate-dec-0010-into-studio-and-vfs-specs.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0010"]},{"id":"PLN-0020","file":"PLN-0020-build-dec-0010-vfs-access-policy-and-save-core.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0010"]},{"id":"PLN-0021","file":"PLN-0021-integrate-dec-0010-editor-write-ui-and-workflow.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0010"]},{"id":"PLN-0022","file":"PLN-0022-propagate-dec-0011-into-studio-vfs-and-lsp-specs.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]},{"id":"PLN-0023","file":"PLN-0023-scaffold-flat-packed-prometeu-lsp-api-and-session-seams.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]},{"id":"PLN-0024","file":"PLN-0024-implement-read-only-fe-diagnostics-symbols-and-definition.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]},{"id":"PLN-0025","file":"PLN-0025-implement-fe-semantic-highlight-consumption.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]}],"lessons":[]}
|
||||||
|
|||||||
@ -2,9 +2,9 @@
|
|||||||
id: PLN-0020
|
id: PLN-0020
|
||||||
ticket: studio-editor-write-wave-supported-non-frontend-files
|
ticket: studio-editor-write-wave-supported-non-frontend-files
|
||||||
title: Build DEC-0010 VFS access policy and save core
|
title: Build DEC-0010 VFS access policy and save core
|
||||||
status: review
|
status: done
|
||||||
created: 2026-03-31
|
created: 2026-03-31
|
||||||
completed:
|
completed: 2026-03-31
|
||||||
tags: [studio, editor, vfs, write-wave, save, access-policy]
|
tags: [studio, editor, vfs, write-wave, save, access-policy]
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package p.studio.vfs;
|
package p.studio.vfs;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public interface ProjectDocumentVfs extends AutoCloseable {
|
public interface ProjectDocumentVfs extends AutoCloseable {
|
||||||
VfsProjectContext projectContext();
|
VfsProjectContext projectContext();
|
||||||
@ -13,6 +15,26 @@ public interface ProjectDocumentVfs extends AutoCloseable {
|
|||||||
|
|
||||||
VfsDocumentOpenResult openDocument(Path path);
|
VfsDocumentOpenResult openDocument(Path path);
|
||||||
|
|
||||||
|
default VfsDocumentAccessContext accessContext(final Path path) {
|
||||||
|
throw new UnsupportedOperationException("Document access context is not supported by this VFS implementation.");
|
||||||
|
}
|
||||||
|
|
||||||
|
default VfsDocumentAccessContext updateAccessContext(final Path path, final Map<String, String> attributes) {
|
||||||
|
throw new UnsupportedOperationException("Document access context updates are not supported by this VFS implementation.");
|
||||||
|
}
|
||||||
|
|
||||||
|
default VfsTextDocument updateDocument(final Path path, final String content) {
|
||||||
|
throw new UnsupportedOperationException("Document mutation is not supported by this VFS implementation.");
|
||||||
|
}
|
||||||
|
|
||||||
|
default VfsDocumentSaveResult saveDocument(final Path path) {
|
||||||
|
throw new UnsupportedOperationException("Document save is not supported by this VFS implementation.");
|
||||||
|
}
|
||||||
|
|
||||||
|
default List<VfsDocumentSaveResult> saveAllDocuments() {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
default void close() {
|
default void close() {
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
package p.studio.vfs;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record VfsDocumentAccessContext(
|
||||||
|
Path path,
|
||||||
|
String typeId,
|
||||||
|
boolean frontendDocument,
|
||||||
|
VfsDocumentAccessMode accessMode,
|
||||||
|
Map<String, String> attributes) {
|
||||||
|
|
||||||
|
public VfsDocumentAccessContext {
|
||||||
|
path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
|
||||||
|
Objects.requireNonNull(typeId, "typeId");
|
||||||
|
Objects.requireNonNull(accessMode, "accessMode");
|
||||||
|
attributes = Map.copyOf(Objects.requireNonNull(attributes, "attributes"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package p.studio.vfs;
|
||||||
|
|
||||||
|
public enum VfsDocumentAccessMode {
|
||||||
|
READ_ONLY,
|
||||||
|
EDITABLE
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package p.studio.vfs;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record VfsDocumentSaveResult(
|
||||||
|
Path path,
|
||||||
|
String typeId,
|
||||||
|
VfsDocumentSaveStatus status) {
|
||||||
|
|
||||||
|
public VfsDocumentSaveResult {
|
||||||
|
path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
|
||||||
|
Objects.requireNonNull(typeId, "typeId");
|
||||||
|
Objects.requireNonNull(status, "status");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package p.studio.vfs;
|
||||||
|
|
||||||
|
public enum VfsDocumentSaveStatus {
|
||||||
|
SAVED,
|
||||||
|
NO_CHANGES,
|
||||||
|
READ_ONLY
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ package p.studio.vfs;
|
|||||||
public final class VfsDocumentTypeIds {
|
public final class VfsDocumentTypeIds {
|
||||||
public static final String TEXT = "text";
|
public static final String TEXT = "text";
|
||||||
public static final String JSON = "json";
|
public static final String JSON = "json";
|
||||||
|
public static final String NDJSON = "ndjson";
|
||||||
public static final String BASH = "bash";
|
public static final String BASH = "bash";
|
||||||
|
|
||||||
private VfsDocumentTypeIds() {
|
private VfsDocumentTypeIds() {
|
||||||
|
|||||||
@ -8,7 +8,9 @@ public record VfsTextDocument(
|
|||||||
String documentName,
|
String documentName,
|
||||||
String typeId,
|
String typeId,
|
||||||
String content,
|
String content,
|
||||||
String lineSeparator) implements VfsDocumentOpenResult {
|
String lineSeparator,
|
||||||
|
boolean dirty,
|
||||||
|
VfsDocumentAccessContext accessContext) implements VfsDocumentOpenResult {
|
||||||
|
|
||||||
public VfsTextDocument {
|
public VfsTextDocument {
|
||||||
path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
|
path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
|
||||||
@ -16,5 +18,6 @@ public record VfsTextDocument(
|
|||||||
Objects.requireNonNull(typeId, "typeId");
|
Objects.requireNonNull(typeId, "typeId");
|
||||||
Objects.requireNonNull(content, "content");
|
Objects.requireNonNull(content, "content");
|
||||||
Objects.requireNonNull(lineSeparator, "lineSeparator");
|
Objects.requireNonNull(lineSeparator, "lineSeparator");
|
||||||
|
Objects.requireNonNull(accessContext, "accessContext");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,8 +12,11 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@ -21,6 +24,8 @@ import java.util.stream.Stream;
|
|||||||
|
|
||||||
final class FilesystemProjectDocumentVfs implements ProjectDocumentVfs {
|
final class FilesystemProjectDocumentVfs implements ProjectDocumentVfs {
|
||||||
private final VfsProjectContext projectContext;
|
private final VfsProjectContext projectContext;
|
||||||
|
private final Map<Path, EditableDocumentSnapshot> editableSnapshots = new LinkedHashMap<>();
|
||||||
|
private final Map<Path, Map<String, String>> accessContextAttributes = new HashMap<>();
|
||||||
private VfsProjectSnapshot snapshot;
|
private VfsProjectSnapshot snapshot;
|
||||||
|
|
||||||
FilesystemProjectDocumentVfs(final VfsProjectContext projectContext) {
|
FilesystemProjectDocumentVfs(final VfsProjectContext projectContext) {
|
||||||
@ -64,26 +69,90 @@ final class FilesystemProjectDocumentVfs implements ProjectDocumentVfs {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public VfsDocumentOpenResult openDocument(final Path path) {
|
public VfsDocumentOpenResult openDocument(final Path path) {
|
||||||
|
try {
|
||||||
|
final var supportedDocument = requireSupportedDocument(path);
|
||||||
|
return openSupportedDocument(supportedDocument);
|
||||||
|
} catch (UnsupportedDocumentException unsupportedDocumentException) {
|
||||||
|
return new VfsUnsupportedDocument(
|
||||||
|
unsupportedDocumentException.path(),
|
||||||
|
unsupportedDocumentException.reason());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VfsDocumentAccessContext accessContext(final Path path) {
|
||||||
|
return accessContextFor(requireSupportedDocument(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VfsDocumentAccessContext updateAccessContext(final Path path, final Map<String, String> attributes) {
|
||||||
|
final var supportedDocument = requireSupportedDocument(path);
|
||||||
|
accessContextAttributes.put(supportedDocument.path(), Map.copyOf(Objects.requireNonNull(attributes, "attributes")));
|
||||||
|
return accessContextFor(supportedDocument);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VfsTextDocument updateDocument(final Path path, final String content) {
|
||||||
|
final var supportedDocument = requireSupportedDocument(path);
|
||||||
|
if (supportedDocument.accessMode() != VfsDocumentAccessMode.EDITABLE) {
|
||||||
|
throw new IllegalStateException("Document is hard read-only: " + supportedDocument.path());
|
||||||
|
}
|
||||||
|
final var normalizedPath = supportedDocument.path();
|
||||||
|
final var snapshot = editableSnapshots.computeIfAbsent(
|
||||||
|
normalizedPath,
|
||||||
|
ignored -> new EditableDocumentSnapshot(
|
||||||
|
supportedDocument.content(),
|
||||||
|
supportedDocument.content(),
|
||||||
|
supportedDocument.lineSeparator()));
|
||||||
|
snapshot.content = Objects.requireNonNull(content, "content");
|
||||||
|
snapshot.lineSeparator = lineSeparator(snapshot.content);
|
||||||
|
return toVfsTextDocument(supportedDocument.withContent(snapshot.content, snapshot.lineSeparator), snapshot.isDirty(), accessContextFor(supportedDocument));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VfsDocumentSaveResult saveDocument(final Path path) {
|
||||||
|
final var supportedDocument = requireSupportedDocument(path);
|
||||||
|
final var accessContext = accessContextFor(supportedDocument);
|
||||||
|
if (accessContext.accessMode() != VfsDocumentAccessMode.EDITABLE) {
|
||||||
|
return new VfsDocumentSaveResult(supportedDocument.path(), supportedDocument.typeId(), VfsDocumentSaveStatus.READ_ONLY);
|
||||||
|
}
|
||||||
|
final var snapshot = editableSnapshots.computeIfAbsent(
|
||||||
|
supportedDocument.path(),
|
||||||
|
ignored -> new EditableDocumentSnapshot(
|
||||||
|
supportedDocument.content(),
|
||||||
|
supportedDocument.content(),
|
||||||
|
supportedDocument.lineSeparator()));
|
||||||
|
if (!snapshot.isDirty()) {
|
||||||
|
return new VfsDocumentSaveResult(supportedDocument.path(), supportedDocument.typeId(), VfsDocumentSaveStatus.NO_CHANGES);
|
||||||
|
}
|
||||||
|
writeUtf8(supportedDocument.path(), snapshot.content);
|
||||||
|
snapshot.savedContent = snapshot.content;
|
||||||
|
snapshot.lineSeparator = lineSeparator(snapshot.content);
|
||||||
|
return new VfsDocumentSaveResult(supportedDocument.path(), supportedDocument.typeId(), VfsDocumentSaveStatus.SAVED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<VfsDocumentSaveResult> saveAllDocuments() {
|
||||||
|
return editableSnapshots.keySet().stream()
|
||||||
|
.map(this::saveDocument)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private SupportedDocument requireSupportedDocument(final Path path) {
|
||||||
final Path normalizedPath = normalize(path);
|
final Path normalizedPath = normalize(path);
|
||||||
if (!normalizedPath.startsWith(projectContext.rootPath())) {
|
if (!normalizedPath.startsWith(projectContext.rootPath())) {
|
||||||
return new VfsUnsupportedDocument(normalizedPath, VfsUnsupportedReason.OUTSIDE_PROJECT);
|
throw new UnsupportedDocumentException(normalizedPath, VfsUnsupportedReason.OUTSIDE_PROJECT);
|
||||||
}
|
}
|
||||||
if (!Files.exists(normalizedPath)) {
|
if (!Files.exists(normalizedPath)) {
|
||||||
return new VfsUnsupportedDocument(normalizedPath, VfsUnsupportedReason.NOT_FOUND);
|
throw new UnsupportedDocumentException(normalizedPath, VfsUnsupportedReason.NOT_FOUND);
|
||||||
}
|
}
|
||||||
if (!Files.isRegularFile(normalizedPath)) {
|
if (!Files.isRegularFile(normalizedPath)) {
|
||||||
return new VfsUnsupportedDocument(normalizedPath, VfsUnsupportedReason.NOT_A_FILE);
|
throw new UnsupportedDocumentException(normalizedPath, VfsUnsupportedReason.NOT_A_FILE);
|
||||||
}
|
|
||||||
|
|
||||||
final byte[] bytes;
|
|
||||||
try {
|
|
||||||
bytes = Files.readAllBytes(normalizedPath);
|
|
||||||
} catch (IOException ioException) {
|
|
||||||
throw new UncheckedIOException(ioException);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final byte[] bytes = readAllBytes(normalizedPath);
|
||||||
if (containsNulByte(bytes)) {
|
if (containsNulByte(bytes)) {
|
||||||
return new VfsUnsupportedDocument(normalizedPath, VfsUnsupportedReason.BINARY_CONTENT);
|
throw new UnsupportedDocumentException(normalizedPath, VfsUnsupportedReason.BINARY_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String content;
|
final String content;
|
||||||
@ -94,17 +163,51 @@ final class FilesystemProjectDocumentVfs implements ProjectDocumentVfs {
|
|||||||
.decode(ByteBuffer.wrap(bytes))
|
.decode(ByteBuffer.wrap(bytes))
|
||||||
.toString();
|
.toString();
|
||||||
} catch (CharacterCodingException codingException) {
|
} catch (CharacterCodingException codingException) {
|
||||||
return new VfsUnsupportedDocument(normalizedPath, VfsUnsupportedReason.INVALID_UTF8);
|
throw new UnsupportedDocumentException(normalizedPath, VfsUnsupportedReason.INVALID_UTF8, codingException);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new VfsTextDocument(
|
final DocumentKind documentKind = documentKind(normalizedPath, content);
|
||||||
|
return new SupportedDocument(
|
||||||
normalizedPath,
|
normalizedPath,
|
||||||
normalizedPath.getFileName().toString(),
|
normalizedPath.getFileName().toString(),
|
||||||
documentTypeId(normalizedPath, content),
|
documentKind.typeId(),
|
||||||
|
documentKind.frontendDocument(),
|
||||||
|
documentKind.accessMode(),
|
||||||
content,
|
content,
|
||||||
lineSeparator(content));
|
lineSeparator(content));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private VfsDocumentOpenResult openSupportedDocument(final SupportedDocument supportedDocument) {
|
||||||
|
final var accessContext = accessContextFor(supportedDocument);
|
||||||
|
if (supportedDocument.accessMode() == VfsDocumentAccessMode.EDITABLE) {
|
||||||
|
final var snapshot = editableSnapshots.computeIfAbsent(
|
||||||
|
supportedDocument.path(),
|
||||||
|
ignored -> new EditableDocumentSnapshot(
|
||||||
|
supportedDocument.content(),
|
||||||
|
supportedDocument.content(),
|
||||||
|
supportedDocument.lineSeparator()));
|
||||||
|
return toVfsTextDocument(
|
||||||
|
supportedDocument.withContent(snapshot.content, snapshot.lineSeparator),
|
||||||
|
snapshot.isDirty(),
|
||||||
|
accessContext);
|
||||||
|
}
|
||||||
|
return toVfsTextDocument(supportedDocument, false, accessContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private VfsTextDocument toVfsTextDocument(
|
||||||
|
final SupportedDocument supportedDocument,
|
||||||
|
final boolean dirty,
|
||||||
|
final VfsDocumentAccessContext accessContext) {
|
||||||
|
return new VfsTextDocument(
|
||||||
|
supportedDocument.path(),
|
||||||
|
supportedDocument.documentName(),
|
||||||
|
supportedDocument.typeId(),
|
||||||
|
supportedDocument.content(),
|
||||||
|
supportedDocument.lineSeparator(),
|
||||||
|
dirty,
|
||||||
|
accessContext);
|
||||||
|
}
|
||||||
|
|
||||||
private VfsProjectSnapshot buildSnapshot() {
|
private VfsProjectSnapshot buildSnapshot() {
|
||||||
return new VfsProjectSnapshot(
|
return new VfsProjectSnapshot(
|
||||||
projectContext.rootPath(),
|
projectContext.rootPath(),
|
||||||
@ -121,18 +224,21 @@ final class FilesystemProjectDocumentVfs implements ProjectDocumentVfs {
|
|||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String documentTypeId(final Path path, final String content) {
|
private DocumentKind documentKind(final Path path, final String content) {
|
||||||
final String extension = extensionOf(path);
|
final String extension = extensionOf(path);
|
||||||
if ("json".equals(extension) || "ndjson".equals(extension)) {
|
if ("json".equals(extension)) {
|
||||||
return VfsDocumentTypeIds.JSON;
|
return new DocumentKind(VfsDocumentTypeIds.JSON, false, VfsDocumentAccessMode.EDITABLE);
|
||||||
|
}
|
||||||
|
if ("ndjson".equals(extension)) {
|
||||||
|
return new DocumentKind(VfsDocumentTypeIds.NDJSON, false, VfsDocumentAccessMode.EDITABLE);
|
||||||
}
|
}
|
||||||
if (isBashDocument(path, extension, content)) {
|
if (isBashDocument(path, extension, content)) {
|
||||||
return VfsDocumentTypeIds.BASH;
|
return new DocumentKind(VfsDocumentTypeIds.BASH, false, VfsDocumentAccessMode.EDITABLE);
|
||||||
}
|
}
|
||||||
if (isFrontendSourceDocument(extension)) {
|
if (isFrontendSourceDocument(extension)) {
|
||||||
return projectContext.languageId();
|
return new DocumentKind(projectContext.languageId(), true, VfsDocumentAccessMode.READ_ONLY);
|
||||||
}
|
}
|
||||||
return VfsDocumentTypeIds.TEXT;
|
return new DocumentKind(VfsDocumentTypeIds.TEXT, false, VfsDocumentAccessMode.EDITABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private VfsProjectNode buildNode(
|
private VfsProjectNode buildNode(
|
||||||
@ -256,4 +362,104 @@ final class FilesystemProjectDocumentVfs implements ProjectDocumentVfs {
|
|||||||
private String lineSeparator(final String content) {
|
private String lineSeparator(final String content) {
|
||||||
return content.contains("\r\n") ? "CRLF" : "LF";
|
return content.contains("\r\n") ? "CRLF" : "LF";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private byte[] readAllBytes(final Path path) {
|
||||||
|
try {
|
||||||
|
return Files.readAllBytes(path);
|
||||||
|
} catch (IOException ioException) {
|
||||||
|
throw new UncheckedIOException(ioException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeUtf8(final Path path, final String content) {
|
||||||
|
try {
|
||||||
|
Files.writeString(path, content, StandardCharsets.UTF_8);
|
||||||
|
} catch (IOException ioException) {
|
||||||
|
throw new UncheckedIOException(ioException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private VfsDocumentAccessContext accessContextFor(final SupportedDocument supportedDocument) {
|
||||||
|
final var attributes = accessContextAttributes.computeIfAbsent(supportedDocument.path(), ignored -> Map.of());
|
||||||
|
return new VfsDocumentAccessContext(
|
||||||
|
supportedDocument.path(),
|
||||||
|
supportedDocument.typeId(),
|
||||||
|
supportedDocument.frontendDocument(),
|
||||||
|
supportedDocument.accessMode(),
|
||||||
|
attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record DocumentKind(
|
||||||
|
String typeId,
|
||||||
|
boolean frontendDocument,
|
||||||
|
VfsDocumentAccessMode accessMode) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record SupportedDocument(
|
||||||
|
Path path,
|
||||||
|
String documentName,
|
||||||
|
String typeId,
|
||||||
|
boolean frontendDocument,
|
||||||
|
VfsDocumentAccessMode accessMode,
|
||||||
|
String content,
|
||||||
|
String lineSeparator) {
|
||||||
|
|
||||||
|
private SupportedDocument withContent(final String updatedContent, final String updatedLineSeparator) {
|
||||||
|
return new SupportedDocument(
|
||||||
|
path,
|
||||||
|
documentName,
|
||||||
|
typeId,
|
||||||
|
frontendDocument,
|
||||||
|
accessMode,
|
||||||
|
updatedContent,
|
||||||
|
updatedLineSeparator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class EditableDocumentSnapshot {
|
||||||
|
private String savedContent;
|
||||||
|
private String content;
|
||||||
|
private String lineSeparator;
|
||||||
|
|
||||||
|
private EditableDocumentSnapshot(
|
||||||
|
final String savedContent,
|
||||||
|
final String content,
|
||||||
|
final String lineSeparator) {
|
||||||
|
this.savedContent = savedContent;
|
||||||
|
this.content = content;
|
||||||
|
this.lineSeparator = lineSeparator;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDirty() {
|
||||||
|
return !Objects.equals(savedContent, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class UnsupportedDocumentException extends IllegalArgumentException {
|
||||||
|
private final Path path;
|
||||||
|
private final VfsUnsupportedReason reason;
|
||||||
|
|
||||||
|
private UnsupportedDocumentException(final Path path, final VfsUnsupportedReason reason) {
|
||||||
|
super(reason.name() + ": " + path);
|
||||||
|
this.path = path;
|
||||||
|
this.reason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UnsupportedDocumentException(
|
||||||
|
final Path path,
|
||||||
|
final VfsUnsupportedReason reason,
|
||||||
|
final Throwable cause) {
|
||||||
|
super(reason.name() + ": " + path, cause);
|
||||||
|
this.path = path;
|
||||||
|
this.reason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path path() {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VfsUnsupportedReason reason() {
|
||||||
|
return reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,9 +5,12 @@ import org.junit.jupiter.api.io.TempDir;
|
|||||||
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
final class FilesystemProjectDocumentVfsTest {
|
final class FilesystemProjectDocumentVfsTest {
|
||||||
@ -45,6 +48,8 @@ final class FilesystemProjectDocumentVfsTest {
|
|||||||
assertEquals("main.pbs", document.documentName());
|
assertEquals("main.pbs", document.documentName());
|
||||||
assertEquals("pbs", document.typeId());
|
assertEquals("pbs", document.typeId());
|
||||||
assertEquals("LF", document.lineSeparator());
|
assertEquals("LF", document.lineSeparator());
|
||||||
|
assertEquals(VfsDocumentAccessMode.READ_ONLY, document.accessContext().accessMode());
|
||||||
|
assertTrue(document.accessContext().frontendDocument());
|
||||||
assertTrue(document.content().contains("fn main()"));
|
assertTrue(document.content().contains("fn main()"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,6 +64,7 @@ final class FilesystemProjectDocumentVfsTest {
|
|||||||
final VfsTextDocument document = assertInstanceOf(VfsTextDocument.class, result);
|
final VfsTextDocument document = assertInstanceOf(VfsTextDocument.class, result);
|
||||||
|
|
||||||
assertEquals(VfsDocumentTypeIds.JSON, document.typeId());
|
assertEquals(VfsDocumentTypeIds.JSON, document.typeId());
|
||||||
|
assertEquals(VfsDocumentAccessMode.EDITABLE, document.accessContext().accessMode());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -71,7 +77,7 @@ final class FilesystemProjectDocumentVfsTest {
|
|||||||
final VfsDocumentOpenResult result = vfs.openDocument(file);
|
final VfsDocumentOpenResult result = vfs.openDocument(file);
|
||||||
final VfsTextDocument document = assertInstanceOf(VfsTextDocument.class, result);
|
final VfsTextDocument document = assertInstanceOf(VfsTextDocument.class, result);
|
||||||
|
|
||||||
assertEquals(VfsDocumentTypeIds.JSON, document.typeId());
|
assertEquals(VfsDocumentTypeIds.NDJSON, document.typeId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -85,6 +91,74 @@ final class FilesystemProjectDocumentVfsTest {
|
|||||||
final VfsTextDocument document = assertInstanceOf(VfsTextDocument.class, result);
|
final VfsTextDocument document = assertInstanceOf(VfsTextDocument.class, result);
|
||||||
|
|
||||||
assertEquals(VfsDocumentTypeIds.BASH, document.typeId());
|
assertEquals(VfsDocumentTypeIds.BASH, document.typeId());
|
||||||
|
assertEquals(VfsDocumentAccessMode.EDITABLE, document.accessContext().accessMode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateDocumentRejectsFrontendDocumentsAsHardReadOnly() throws Exception {
|
||||||
|
final Path file = tempDir.resolve("main.pbs");
|
||||||
|
Files.writeString(file, "fn main(): void\n");
|
||||||
|
|
||||||
|
final ProjectDocumentVfs vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
|
||||||
|
|
||||||
|
assertThrows(IllegalStateException.class, () -> vfs.updateDocument(file, "fn main(): int\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void editableDocumentsUseInMemorySnapshotsUntilSavePersistsThem() throws Exception {
|
||||||
|
final Path file = tempDir.resolve("notes.txt");
|
||||||
|
Files.writeString(file, "alpha\n");
|
||||||
|
|
||||||
|
final ProjectDocumentVfs vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
|
||||||
|
|
||||||
|
final VfsTextDocument updated = vfs.updateDocument(file, "beta\n");
|
||||||
|
|
||||||
|
assertTrue(updated.dirty());
|
||||||
|
assertEquals("beta\n", updated.content());
|
||||||
|
assertEquals("alpha\n", Files.readString(file));
|
||||||
|
assertEquals("beta\n", assertInstanceOf(VfsTextDocument.class, vfs.openDocument(file)).content());
|
||||||
|
|
||||||
|
final VfsDocumentSaveResult saveResult = vfs.saveDocument(file);
|
||||||
|
|
||||||
|
assertEquals(VfsDocumentSaveStatus.SAVED, saveResult.status());
|
||||||
|
assertEquals("beta\n", Files.readString(file));
|
||||||
|
assertFalse(assertInstanceOf(VfsTextDocument.class, vfs.openDocument(file)).dirty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void saveAllDocumentsPersistsOnlyEditableDirtySnapshots() throws Exception {
|
||||||
|
final Path editable = tempDir.resolve("notes.txt");
|
||||||
|
final Path frontend = tempDir.resolve("main.pbs");
|
||||||
|
Files.writeString(editable, "alpha\n");
|
||||||
|
Files.writeString(frontend, "fn main(): void\n");
|
||||||
|
|
||||||
|
final ProjectDocumentVfs vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
|
||||||
|
vfs.updateDocument(editable, "beta\n");
|
||||||
|
|
||||||
|
final var results = vfs.saveAllDocuments();
|
||||||
|
|
||||||
|
assertEquals(1, results.size());
|
||||||
|
assertEquals(VfsDocumentSaveStatus.SAVED, results.get(0).status());
|
||||||
|
assertEquals(editable.toAbsolutePath().normalize(), results.get(0).path());
|
||||||
|
assertEquals("beta\n", Files.readString(editable));
|
||||||
|
assertEquals("fn main(): void\n", Files.readString(frontend));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void accessContextCanBeReadAndMutatedWithoutChangingCanonicalPolicy() throws Exception {
|
||||||
|
final Path file = tempDir.resolve("prometeu.json");
|
||||||
|
Files.writeString(file, "{\n \"name\": \"Example\"\n}\n");
|
||||||
|
|
||||||
|
final ProjectDocumentVfs vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
|
||||||
|
|
||||||
|
final VfsDocumentAccessContext initialContext = vfs.accessContext(file);
|
||||||
|
final VfsDocumentAccessContext updatedContext = vfs.updateAccessContext(file, Map.of("statusBar", "future-toggle"));
|
||||||
|
|
||||||
|
assertEquals(VfsDocumentAccessMode.EDITABLE, initialContext.accessMode());
|
||||||
|
assertEquals(VfsDocumentTypeIds.JSON, initialContext.typeId());
|
||||||
|
assertEquals(Map.of("statusBar", "future-toggle"), updatedContext.attributes());
|
||||||
|
assertEquals(initialContext.accessMode(), updatedContext.accessMode());
|
||||||
|
assertEquals(initialContext.typeId(), updatedContext.typeId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -131,7 +205,7 @@ final class FilesystemProjectDocumentVfsTest {
|
|||||||
assertEquals("main.pbs", srcNode.children().get(1).displayName());
|
assertEquals("main.pbs", srcNode.children().get(1).displayName());
|
||||||
}
|
}
|
||||||
|
|
||||||
private VfsProjectContext projectContext() {
|
private p.studio.vfs.VfsProjectContext projectContext() {
|
||||||
return new VfsProjectContext("Example", "pbs", tempDir);
|
return new p.studio.vfs.VfsProjectContext("Example", "pbs", tempDir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user