diff --git a/build.gradle b/build.gradle index 2438d87..dc88ce8 100644 --- a/build.gradle +++ b/build.gradle @@ -41,7 +41,8 @@ java { dependencies { implementation 'io.github.copilot-community-sdk:copilot-sdk:1.0.5' implementation 'com.networknt:json-schema-validator:1.0.87' - implementation 'ch.qos.logback:logback-classic:1.5.13' + implementation 'ch.qos.logback:logback-classic:1.5.17' + implementation 'org.eclipse.jgit:org.eclipse.jgit:6.10.0.202406032230-r' implementation 'com.brunomnsilva:smartgraph:2.3.0' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0' diff --git a/directLibs/smartgraph-2.0.0.jar b/directLibs/smartgraph-2.0.0.jar deleted file mode 100644 index 197e848..0000000 Binary files a/directLibs/smartgraph-2.0.0.jar and /dev/null differ diff --git a/src/main/java/com/daniel/jsoneditor/model/ReadableModel.java b/src/main/java/com/daniel/jsoneditor/model/ReadableModel.java index d70878c..ec41ebf 100644 --- a/src/main/java/com/daniel/jsoneditor/model/ReadableModel.java +++ b/src/main/java/com/daniel/jsoneditor/model/ReadableModel.java @@ -1,6 +1,7 @@ package com.daniel.jsoneditor.model; import com.daniel.jsoneditor.model.commands.CommandFactory; +import com.daniel.jsoneditor.model.git.GitBlameInfo; import com.daniel.jsoneditor.model.impl.graph.NodeGraph; import com.daniel.jsoneditor.model.json.schema.reference.ReferenceToObject; import com.daniel.jsoneditor.model.json.schema.reference.ReferenceToObjectInstance; @@ -106,4 +107,19 @@ public interface ReadableModel extends ReadableState String getIdentifier(String pathOfParent, JsonNode child); + /** + * Get git blame information for a JSON path. + * + * @param jsonPath JSON path (e.g., "/root/child/property") + * @return blame info or null if not available or not in git repo + */ + GitBlameInfo getBlameForPath(String jsonPath); + + /** + * Check if git blame is available for the current file. + * + * @return true if file is in a git repository + */ + boolean isGitBlameAvailable(); + } diff --git a/src/main/java/com/daniel/jsoneditor/model/git/GitBlameInfo.java b/src/main/java/com/daniel/jsoneditor/model/git/GitBlameInfo.java new file mode 100644 index 0000000..0d8aea3 --- /dev/null +++ b/src/main/java/com/daniel/jsoneditor/model/git/GitBlameInfo.java @@ -0,0 +1,82 @@ +package com.daniel.jsoneditor.model.git; + +import java.time.Instant; + +public class GitBlameInfo +{ + private final String authorName; + private final String authorEmail; + private final String commitHash; + private final Instant commitTime; + private final String commitMessage; + private final String commitColor; + + public GitBlameInfo(String authorName, String authorEmail, String commitHash, Instant commitTime, String commitMessage) + { + this.authorName = authorName; + this.authorEmail = authorEmail; + this.commitHash = commitHash; + this.commitTime = commitTime; + this.commitMessage = commitMessage; + this.commitColor = calculateCommitColor(); + } + + public String getAuthorName() + { + return authorName; + } + + public String getAuthorEmail() + { + return authorEmail; + } + + public String getShortCommitHash() + { + return commitHash != null && commitHash.length() > 7 ? commitHash.substring(0, 7) : commitHash; + } + + public Instant getCommitTime() + { + return commitTime; + } + + public String getShortCommitMessage() + { + if (commitMessage == null) return ""; + final int newlineIndex = commitMessage.indexOf('\n'); + return newlineIndex > 0 ? commitMessage.substring(0, newlineIndex) : commitMessage; + } + + public String getCommitColor() + { + return commitColor; + } + + private String calculateCommitColor() + { + if (commitHash == null || commitHash.isEmpty()) + { + return "#808080"; + } + + final int hash = commitHash.hashCode(); + final int r = (hash & 0xFF0000) >> 16; + final int g = (hash & 0x00FF00) >> 8; + final int b = hash & 0x0000FF; + + final int minBrightness = 80; + final int maxBrightness = 200; + final int adjustedR = minBrightness + (r * (maxBrightness - minBrightness) / 255); + final int adjustedG = minBrightness + (g * (maxBrightness - minBrightness) / 255); + final int adjustedB = minBrightness + (b * (maxBrightness - minBrightness) / 255); + + return String.format("#%02X%02X%02X", adjustedR, adjustedG, adjustedB); + } + + @Override + public String toString() + { + return String.format("%s (%s)", authorName, getShortCommitHash()); + } +} diff --git a/src/main/java/com/daniel/jsoneditor/model/git/GitBlameIntegration.java b/src/main/java/com/daniel/jsoneditor/model/git/GitBlameIntegration.java new file mode 100644 index 0000000..70a12b3 --- /dev/null +++ b/src/main/java/com/daniel/jsoneditor/model/git/GitBlameIntegration.java @@ -0,0 +1,129 @@ +package com.daniel.jsoneditor.model.git; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * External tool integration for git blame functionality. + * Combines GitBlameService and JsonPathToLineMapper to provide + * blame information for JSON paths. + */ +public class GitBlameIntegration +{ + private static final Logger logger = LoggerFactory.getLogger(GitBlameIntegration.class); + + private final GitBlameService blameService = new GitBlameService(); + private final JsonPathToLineMapper lineMapper = new JsonPathToLineMapper(); + private final Map pathCache = new HashMap<>(); + + private String relativeFilePath; + private final AtomicBoolean initialized = new AtomicBoolean(false); + private final AtomicBoolean initializing = new AtomicBoolean(false); + + /** + * Initialize with a JSON file asynchronously. + * Returns immediately, loading happens in background thread. + * + * @param jsonFilePath absolute path to JSON file + * @return CompletableFuture that completes when initialization is done + */ + public CompletableFuture initialize(Path jsonFilePath) + { + close(); + initializing.set(true); + + return CompletableFuture.runAsync(() -> { + try + { + if (!blameService.initialize(jsonFilePath)) + { + logger.debug("Git blame not available for: {}", jsonFilePath); + return; + } + + relativeFilePath = blameService.getRelativePath(jsonFilePath); + if (relativeFilePath == null) + { + logger.warn("Could not determine relative path for: {}", jsonFilePath); + close(); + return; + } + + lineMapper.buildMapping(jsonFilePath); + synchronized (pathCache) + { + pathCache.clear(); + } + initialized.set(true); + logger.info("Git blame initialized for: {} ({})", jsonFilePath, relativeFilePath); + } + finally + { + initializing.set(false); + } + }); + } + + /** + * Get blame information for a JSON path. + * + * @param jsonPath JSON path (e.g., "/root/child/property") + * @return blame info or null if not available or still loading + */ + public GitBlameInfo getBlameForPath(String jsonPath) + { + if (!initialized.get()) + { + return null; + } + + synchronized (pathCache) + { + if (pathCache.containsKey(jsonPath)) + { + return pathCache.get(jsonPath); + } + } + + final int lineNumber = lineMapper.getLineForPathOrParent(jsonPath); + if (lineNumber < 0) + { + logger.debug("No line number found for path: {}", jsonPath); + synchronized (pathCache) + { + pathCache.put(jsonPath, null); + } + return null; + } + + final GitBlameInfo blameInfo = blameService.getBlameForLine(relativeFilePath, lineNumber); + synchronized (pathCache) + { + pathCache.put(jsonPath, blameInfo); + } + return blameInfo; + } + + public boolean isAvailable() + { + return initialized.get() || initializing.get(); + } + + public void close() + { + blameService.close(); + synchronized (pathCache) + { + pathCache.clear(); + } + initialized.set(false); + initializing.set(false); + relativeFilePath = null; + } +} diff --git a/src/main/java/com/daniel/jsoneditor/model/git/GitBlameService.java b/src/main/java/com/daniel/jsoneditor/model/git/GitBlameService.java new file mode 100644 index 0000000..ed7d8fa --- /dev/null +++ b/src/main/java/com/daniel/jsoneditor/model/git/GitBlameService.java @@ -0,0 +1,160 @@ +package com.daniel.jsoneditor.model.git; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.blame.BlameResult; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +/** + * Service for retrieving git blame information for JSON file lines. + * Provides information about who last modified each line in a file. + */ +public class GitBlameService +{ + private static final Logger logger = LoggerFactory.getLogger(GitBlameService.class); + + private Repository repository; + private Git git; + private final Map blameCache = new HashMap<>(); + + /** + * Initialize the service with a JSON file path. + * Attempts to find the git repository containing the file. + * + * @param jsonFilePath path to the JSON file + * @return true if git repository was found and initialized + */ + public boolean initialize(Path jsonFilePath) + { + close(); + + try + { + final File file = jsonFilePath.toFile(); + final FileRepositoryBuilder builder = new FileRepositoryBuilder(); + repository = builder + .findGitDir(file) + .setMustExist(true) + .build(); + + git = new Git(repository); + logger.info("Git repository found: {}", repository.getDirectory()); + return true; + } + catch (IOException e) + { + logger.debug("No git repository found for file: {}", jsonFilePath); + return false; + } + } + + /** + * Get blame information for a specific line in a file. + * + * @param relativeFilePath path relative to git repository root + * @param lineNumber 0-based line number + * @return blame info or null if not available + */ + public GitBlameInfo getBlameForLine(String relativeFilePath, int lineNumber) + { + if (git == null || repository == null) + { + return null; + } + + try + { + final BlameResult blameResult = getOrComputeBlame(relativeFilePath); + if (blameResult == null) + { + return null; + } + + final RevCommit commit = blameResult.getSourceCommit(lineNumber); + if (commit == null) + { + return null; + } + + final PersonIdent author = blameResult.getSourceAuthor(lineNumber); + final Instant commitTime = Instant.ofEpochSecond(commit.getCommitTime()); + + return new GitBlameInfo( + author.getName(), + author.getEmailAddress(), + commit.getName(), + commitTime, + commit.getFullMessage() + ); + } + catch (Exception e) + { + logger.error("Error getting blame info for {}:{}", relativeFilePath, lineNumber, e); + return null; + } + } + + private BlameResult getOrComputeBlame(String relativeFilePath) throws GitAPIException + { + if (!blameCache.containsKey(relativeFilePath)) + { + logger.debug("Computing blame for file: {}", relativeFilePath); + final BlameResult result = git.blame() + .setFilePath(relativeFilePath) + .call(); + blameCache.put(relativeFilePath, result); + } + return blameCache.get(relativeFilePath); + } + + /** + * Get the relative path of a file from the repository root. + * + * @param absolutePath absolute file path + * @return relative path or null if file is not in repository + */ + public String getRelativePath(Path absolutePath) + { + if (repository == null) + { + return null; + } + + final Path workTree = repository.getWorkTree().toPath(); + if (absolutePath.startsWith(workTree)) + { + return workTree.relativize(absolutePath).toString().replace('\\', '/'); + } + return null; + } + + /** + * Close the git repository and release resources. + */ + public void close() + { + blameCache.clear(); + if (git != null) + { + git.close(); + git = null; + } + if (repository != null) + { + repository.close(); + repository = null; + } + } +} diff --git a/src/main/java/com/daniel/jsoneditor/model/git/JsonPathToLineMapper.java b/src/main/java/com/daniel/jsoneditor/model/git/JsonPathToLineMapper.java new file mode 100644 index 0000000..af38d61 --- /dev/null +++ b/src/main/java/com/daniel/jsoneditor/model/git/JsonPathToLineMapper.java @@ -0,0 +1,193 @@ +package com.daniel.jsoneditor.model.git; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; +import java.util.Map; + +/** + * Maps JSON paths to line numbers by parsing the JSON file with Jackson's streaming API. + * Builds a complete path-to-line mapping during initialization for fast lookups. + */ +public class JsonPathToLineMapper +{ + private static final Logger logger = LoggerFactory.getLogger(JsonPathToLineMapper.class); + + private final Map pathToLineMap = new HashMap<>(); + private final JsonFactory jsonFactory = new JsonFactory(); + + /** + * Parse JSON file and build complete path-to-line mapping. + */ + public void buildMapping(Path jsonFilePath) + { + pathToLineMap.clear(); + + try (JsonParser parser = jsonFactory.createParser(jsonFilePath.toFile())) + { + final Deque pathStack = new ArrayDeque<>(); + final Deque arrayIndexStack = new ArrayDeque<>(); + String currentFieldName = null; + + while (parser.nextToken() != null) + { + final JsonToken token = parser.currentToken(); + final int lineNumber = parser.getTokenLocation().getLineNr() - 1; + + switch (token) + { + case FIELD_NAME: + currentFieldName = parser.currentName(); + break; + + case START_OBJECT: + if (currentFieldName != null) + { + pathStack.push(currentFieldName); + final String path = buildPath(pathStack); + pathToLineMap.put(path, lineNumber); + currentFieldName = null; + } + else if (!pathStack.isEmpty() && isArrayContext(arrayIndexStack)) + { + final int arrayIndex = arrayIndexStack.pop(); + pathStack.push(String.valueOf(arrayIndex)); + final String path = buildPath(pathStack); + pathToLineMap.put(path, lineNumber); + arrayIndexStack.push(arrayIndex + 1); + } + arrayIndexStack.push(0); + break; + + case END_OBJECT: + if (!pathStack.isEmpty()) + { + pathStack.pop(); + } + if (!arrayIndexStack.isEmpty()) + { + arrayIndexStack.pop(); + } + break; + + case START_ARRAY: + if (currentFieldName != null) + { + pathStack.push(currentFieldName); + final String path = buildPath(pathStack); + pathToLineMap.put(path, lineNumber); + currentFieldName = null; + } + arrayIndexStack.push(0); + break; + + case END_ARRAY: + if (!pathStack.isEmpty()) + { + pathStack.pop(); + } + if (!arrayIndexStack.isEmpty()) + { + arrayIndexStack.pop(); + } + break; + + default: + if (currentFieldName != null) + { + pathStack.push(currentFieldName); + final String path = buildPath(pathStack); + pathToLineMap.put(path, lineNumber); + pathStack.pop(); + currentFieldName = null; + } + else if (!pathStack.isEmpty() && isArrayContext(arrayIndexStack)) + { + final int arrayIndex = arrayIndexStack.pop(); + pathStack.push(String.valueOf(arrayIndex)); + final String path = buildPath(pathStack); + pathToLineMap.put(path, lineNumber); + pathStack.pop(); + arrayIndexStack.push(arrayIndex + 1); + } + break; + } + } + + logger.debug("Built path-to-line mapping with {} entries", pathToLineMap.size()); + } + catch (IOException e) + { + logger.error("Failed to parse JSON file: {}", jsonFilePath, e); + } + } + + /** + * Get line number for a JSON path, or search for parent paths if not found. + * + * @param fullPath JSON path like "/root/child/property" + * @return 0-based line number or -1 if not found + */ + public int getLineForPathOrParent(String fullPath) + { + if (fullPath == null || fullPath.isEmpty()) + { + return 0; + } + + String searchPath = fullPath.startsWith("/") ? fullPath.substring(1) : fullPath; + + if (pathToLineMap.containsKey(searchPath)) + { + return pathToLineMap.get(searchPath); + } + + while (searchPath.contains("/")) + { + final int lastSlash = searchPath.lastIndexOf('/'); + searchPath = searchPath.substring(0, lastSlash); + + if (pathToLineMap.containsKey(searchPath)) + { + return pathToLineMap.get(searchPath); + } + } + + return pathToLineMap.getOrDefault(searchPath, -1); + } + + private String buildPath(Deque pathStack) + { + if (pathStack.isEmpty()) + { + return ""; + } + + final StringBuilder sb = new StringBuilder(); + final Object[] pathParts = pathStack.toArray(); + + for (int i = pathParts.length - 1; i >= 0; i--) + { + sb.append(pathParts[i]); + if (i > 0) + { + sb.append('/'); + } + } + + return sb.toString(); + } + + private boolean isArrayContext(Deque arrayIndexStack) + { + return !arrayIndexStack.isEmpty() && arrayIndexStack.peek() >= 0; + } +} diff --git a/src/main/java/com/daniel/jsoneditor/model/impl/ModelImpl.java b/src/main/java/com/daniel/jsoneditor/model/impl/ModelImpl.java index c7ee2c3..015ea59 100644 --- a/src/main/java/com/daniel/jsoneditor/model/impl/ModelImpl.java +++ b/src/main/java/com/daniel/jsoneditor/model/impl/ModelImpl.java @@ -2,6 +2,8 @@ import com.daniel.jsoneditor.model.WritableModelInternal; import com.daniel.jsoneditor.model.commands.CommandFactory; +import com.daniel.jsoneditor.model.git.GitBlameIntegration; +import com.daniel.jsoneditor.model.git.GitBlameInfo; import com.daniel.jsoneditor.model.impl.graph.NodeGraph; import com.daniel.jsoneditor.model.impl.graph.NodeGraphCreator; import com.daniel.jsoneditor.model.json.schema.paths.PathHelper; @@ -60,6 +62,8 @@ public class ModelImpl implements ReadableModel, WritableModelInternal private Settings settings; + private final GitBlameIntegration gitBlameIntegration = new GitBlameIntegration(); + public ModelImpl(EventSender eventSender) { this.eventSender = eventSender; @@ -136,6 +140,13 @@ public void setSettings(Settings settings) public void setCurrentJSONFile(File json) { this.jsonFile = json; + if (json != null) + { + gitBlameIntegration.initialize(json.toPath()).thenRun(() -> { + logger.info("Git blame loading completed"); + sendEvent(new Event(EventEnum.GIT_BLAME_LOADED)); + }); + } } private void setCurrentSchemaFile(File schema) @@ -761,4 +772,16 @@ public String getIdentifier(String pathOfParentNode, JsonNode childNode) } return null; } + + @Override + public GitBlameInfo getBlameForPath(String jsonPath) + { + return gitBlameIntegration.getBlameForPath(jsonPath); + } + + @Override + public boolean isGitBlameAvailable() + { + return gitBlameIntegration.isAvailable(); + } } diff --git a/src/main/java/com/daniel/jsoneditor/model/json/schema/reference/ReferenceToObjectInstance.java b/src/main/java/com/daniel/jsoneditor/model/json/schema/reference/ReferenceToObjectInstance.java index ce83b03..78effab 100644 --- a/src/main/java/com/daniel/jsoneditor/model/json/schema/reference/ReferenceToObjectInstance.java +++ b/src/main/java/com/daniel/jsoneditor/model/json/schema/reference/ReferenceToObjectInstance.java @@ -61,6 +61,9 @@ public String getRemarks() public boolean refersToObject(ReferenceableObjectInstance objectInstance) { - return objectInstance.getKey().equals(key) && objectInstance.getReferencingKey().equals(referencingKey); + final String objectKey = objectInstance.getKey(); + final String objectReferencingKey = objectInstance.getReferencingKey(); + return objectKey != null && objectKey.equals(key) + && objectReferencingKey != null && objectReferencingKey.equals(referencingKey); } } diff --git a/src/main/java/com/daniel/jsoneditor/model/statemachine/impl/EventEnum.java b/src/main/java/com/daniel/jsoneditor/model/statemachine/impl/EventEnum.java index 35228b3..48b3d9b 100644 --- a/src/main/java/com/daniel/jsoneditor/model/statemachine/impl/EventEnum.java +++ b/src/main/java/com/daniel/jsoneditor/model/statemachine/impl/EventEnum.java @@ -32,5 +32,7 @@ public enum EventEnum SAVING_FAILED, - COMMAND_APPLIED // new command (execute/undo/redo) with metadata + COMMAND_APPLIED, // new command (execute/undo/redo) with metadata + + GIT_BLAME_LOADED } diff --git a/src/main/java/com/daniel/jsoneditor/view/impl/jfx/buttons/WindowGitBlameToggleButton.java b/src/main/java/com/daniel/jsoneditor/view/impl/jfx/buttons/WindowGitBlameToggleButton.java new file mode 100644 index 0000000..4a48bd3 --- /dev/null +++ b/src/main/java/com/daniel/jsoneditor/view/impl/jfx/buttons/WindowGitBlameToggleButton.java @@ -0,0 +1,28 @@ +package com.daniel.jsoneditor.view.impl.jfx.buttons; + +import com.daniel.jsoneditor.view.impl.jfx.impl.scenes.impl.editor.components.editorwindow.JsonEditorEditorWindow; +import javafx.scene.control.Button; +import javafx.scene.control.Tooltip; + +public class WindowGitBlameToggleButton extends Button +{ + private final JsonEditorEditorWindow window; + + public WindowGitBlameToggleButton(JsonEditorEditorWindow window) + { + super(); + this.window = window; + ButtonHelper.setButtonImage(this, "/icons/material/darkmode/outline_git_blame_white_24dp.png"); + updateState(); + setOnAction(event -> { + window.toggleGitBlameForAllTables(); + updateState(); + }); + } + + private void updateState() + { + final boolean visible = window.isGitBlameVisible(); + setTooltip(new Tooltip(visible ? "Hide Git Blame" : "Show Git Blame")); + } +} diff --git a/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/JsonEditorEditorWindow.java b/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/JsonEditorEditorWindow.java index abbceae..77c991c 100644 --- a/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/JsonEditorEditorWindow.java +++ b/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/JsonEditorEditorWindow.java @@ -49,16 +49,18 @@ public class JsonEditorEditorWindow extends VBox private List childTableViews; + private boolean gitBlameVisible = false; + public JsonEditorEditorWindow(EditorWindowManager manager, ReadableModel model, Controller controller) { this.model = model; this.manager = manager; this.controller = controller; - nameBar = new JsonEditorNamebar(manager, this, model, controller); childTableViews = new ArrayList<>(); editorTables = new AutoAdjustingSplitPane(); editorTables.setOrientation(javafx.geometry.Orientation.VERTICAL); mainTableView = new EditorTableViewImpl(manager, this, model, controller); + nameBar = new JsonEditorNamebar(manager, this, model, controller); buttonBar = new TableViewButtonBar(model, controller, mainTableView::getCurrentlyDisplayedPaths, () -> selectedPath); VBox.setVgrow(buttonBar, Priority.NEVER); @@ -336,6 +338,27 @@ public void handleSettingsChanged() setSelectedPath(selectedPath); } + /** + * Toggle git blame column visibility for all tables in this window + */ + public void toggleGitBlameForAllTables() + { + gitBlameVisible = !gitBlameVisible; + mainTableView.toggleGitBlameColumn(); + for (TableViewWithCompactNamebar childTable : childTableViews) + { + childTable.toggleGitBlame(); + } + } + + /** + * @return true if git blame columns are currently visible + */ + public boolean isGitBlameVisible() + { + return gitBlameVisible; + } + @Override protected double computePrefWidth(double v) { diff --git a/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/components/JsonEditorNamebar.java b/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/components/JsonEditorNamebar.java index 62fe456..7513665 100644 --- a/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/components/JsonEditorNamebar.java +++ b/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/components/JsonEditorNamebar.java @@ -6,6 +6,7 @@ import com.daniel.jsoneditor.model.ReadableModel; import com.daniel.jsoneditor.view.impl.jfx.buttons.ButtonHelper; import com.daniel.jsoneditor.view.impl.jfx.buttons.ShowUsagesButton; +import com.daniel.jsoneditor.view.impl.jfx.buttons.WindowGitBlameToggleButton; import com.daniel.jsoneditor.view.impl.jfx.dialogs.AreYouSureDialog; import com.daniel.jsoneditor.view.impl.jfx.impl.scenes.impl.editor.components.editorwindow.EditorWindowManager; import com.daniel.jsoneditor.view.impl.jfx.impl.scenes.impl.editor.components.editorwindow.JsonEditorEditorWindow; @@ -48,8 +49,15 @@ public JsonEditorNamebar(EditorWindowManager manager, JsonEditorEditorWindow edi nameLabel.setAlignment(Pos.CENTER); HBox.setHgrow(nameLabel, Priority.ALWAYS); showUsagesButton = new ShowUsagesButton(model, manager); - this.getChildren().addAll(makeSelectInNavbarButton(), makeGoToParentButton(), nameLabel, showUsagesButton, makeDeleteItemButton(), - makeCloseWindowButton()); + + this.getChildren().addAll(makeSelectInNavbarButton(), makeGoToParentButton(), nameLabel, showUsagesButton); + + if (model.isGitBlameAvailable()) + { + this.getChildren().add(new WindowGitBlameToggleButton(editorWindow)); + } + + this.getChildren().addAll(makeDeleteItemButton(), makeCloseWindowButton()); } public void setSelection(JsonNodeWithPath selection) diff --git a/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/components/TableViewWithCompactNamebar.java b/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/components/TableViewWithCompactNamebar.java index e818d1f..4bc330c 100644 --- a/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/components/TableViewWithCompactNamebar.java +++ b/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/components/TableViewWithCompactNamebar.java @@ -197,4 +197,9 @@ public void handleSorted(String path) { tableView.handleSorted(path); } + + public void toggleGitBlame() + { + tableView.toggleGitBlameColumn(); + } } diff --git a/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/components/tableview/EditorTableView.java b/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/components/tableview/EditorTableView.java index fb24687..de2a428 100644 --- a/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/components/tableview/EditorTableView.java +++ b/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/components/tableview/EditorTableView.java @@ -49,4 +49,14 @@ public abstract class EditorTableView extends TableView * Returns true if all columns are currently shown due to temporary override */ public abstract boolean isTemporaryShowAllColumns(); + + /** + * Toggles the visibility of the git blame column + */ + public abstract void toggleGitBlameColumn(); + + /** + * Returns true if git blame column is currently visible + */ + public abstract boolean isGitBlameColumnVisible(); } diff --git a/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/components/tableview/impl/EditorTableViewImpl.java b/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/components/tableview/impl/EditorTableViewImpl.java index 0ca42f6..0d56f09 100644 --- a/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/components/tableview/impl/EditorTableViewImpl.java +++ b/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/components/tableview/impl/EditorTableViewImpl.java @@ -12,6 +12,7 @@ import com.daniel.jsoneditor.view.impl.jfx.impl.scenes.impl.editor.components.editorwindow.JsonEditorEditorWindow; import com.daniel.jsoneditor.view.impl.jfx.impl.scenes.impl.editor.components.editorwindow.components.tableview.EditorTableView; import com.daniel.jsoneditor.view.impl.jfx.impl.scenes.impl.editor.components.editorwindow.components.tableview.impl.columns.EditorTableColumn; +import com.daniel.jsoneditor.view.impl.jfx.impl.scenes.impl.editor.components.editorwindow.components.tableview.impl.columns.GitBlameColumn; import com.fasterxml.jackson.databind.JsonNode; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; @@ -22,7 +23,6 @@ import javafx.scene.input.KeyEvent; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; -import javafx.util.Callback; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -60,6 +60,8 @@ public class EditorTableViewImpl extends EditorTableView // temporary override for hide empty columns setting private boolean temporaryShowAllColumns = false; + private boolean gitBlameColumnVisible = false; + private void refreshTable() { final JsonNodeWithPath parentNode = model.getNodeForPath(parentPath); @@ -233,7 +235,7 @@ private void setView(TableSchemaProcessor.TableData tableData) this.allItems = tableData.getNodes(); // Use the column factory to create columns - final List> columns = columnFactory.createColumns(tableData.getProperties(), + final List> columns = columnFactory.createColumns(tableData.getProperties(), tableData.isArray(), this); filteredItems = new FilteredList<>(allItems); @@ -445,4 +447,28 @@ private void updatePathsAfterIndex(int startListIndex, int startArrayIndex) } } } + + @Override + public void toggleGitBlameColumn() + { + gitBlameColumnVisible = !gitBlameColumnVisible; + refreshGitBlameColumnVisibility(); + } + + @Override + public boolean isGitBlameColumnVisible() + { + return gitBlameColumnVisible; + } + + private void refreshGitBlameColumnVisibility() + { + for (TableColumn column : getColumns()) + { + if (column instanceof GitBlameColumn) + { + column.setVisible(gitBlameColumnVisible); + } + } + } } diff --git a/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/components/tableview/impl/TableColumnFactory.java b/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/components/tableview/impl/TableColumnFactory.java index b7c09f9..d96fa40 100644 --- a/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/components/tableview/impl/TableColumnFactory.java +++ b/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/components/tableview/impl/TableColumnFactory.java @@ -8,6 +8,7 @@ import com.daniel.jsoneditor.view.impl.jfx.impl.scenes.impl.editor.components.editorwindow.JsonEditorEditorWindow; import com.daniel.jsoneditor.view.impl.jfx.impl.scenes.impl.editor.components.editorwindow.components.tableview.impl.columns.EditorTableColumn; import com.daniel.jsoneditor.view.impl.jfx.impl.scenes.impl.editor.components.editorwindow.components.tableview.impl.columns.FollowRefOrOpenColumn; +import com.daniel.jsoneditor.view.impl.jfx.impl.scenes.impl.editor.components.editorwindow.components.tableview.impl.columns.GitBlameColumn; import com.fasterxml.jackson.databind.JsonNode; import javafx.beans.property.SimpleStringProperty; import javafx.scene.control.Button; @@ -46,12 +47,19 @@ public TableColumnFactory(EditorWindowManager manager, Controller controller, * @param parentTableView reference to the parent table view * @return list of created columns */ - public List> createColumns( + public List> createColumns( List, JsonNode>> properties, boolean isArray, EditorTableViewImpl parentTableView) { - final List> columns = new ArrayList<>(createPropertyColumns(properties, parentTableView)); + final List> columns = new ArrayList<>(createPropertyColumns(properties, parentTableView)); + + if (model.isGitBlameAvailable()) + { + final GitBlameColumn blameColumn = new GitBlameColumn(model); + blameColumn.setVisible(false); + columns.add(blameColumn); + } if (isArray) { diff --git a/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/components/tableview/impl/columns/GitBlameColumn.java b/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/components/tableview/impl/columns/GitBlameColumn.java new file mode 100644 index 0000000..1547346 --- /dev/null +++ b/src/main/java/com/daniel/jsoneditor/view/impl/jfx/impl/scenes/impl/editor/components/editorwindow/components/tableview/impl/columns/GitBlameColumn.java @@ -0,0 +1,119 @@ +package com.daniel.jsoneditor.view.impl.jfx.impl.scenes.impl.editor.components.editorwindow.components.tableview.impl.columns; + +import com.daniel.jsoneditor.model.ReadableModel; +import com.daniel.jsoneditor.model.git.GitBlameInfo; +import com.daniel.jsoneditor.model.json.JsonNodeWithPath; +import com.daniel.jsoneditor.model.observe.Observer; +import com.daniel.jsoneditor.model.statemachine.impl.EventEnum; +import javafx.application.Platform; +import javafx.beans.property.SimpleObjectProperty; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.HBox; +import javafx.scene.shape.Rectangle; +import javafx.util.Duration; + +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +/** + * Table column showing git blame information (last author and commit). + */ +public class GitBlameColumn extends TableColumn implements Observer +{ + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") + .withZone(ZoneId.systemDefault()); + + private final ReadableModel model; + + public GitBlameColumn(ReadableModel model) + { + super("Last Modified"); + + this.model = model; + + setMinWidth(100); + setPrefWidth(150); + setSortable(false); + + setCellValueFactory(data -> { + final JsonNodeWithPath nodeWithPath = data.getValue(); + final GitBlameInfo blameInfo = model.getBlameForPath(nodeWithPath.getPath()); + return new SimpleObjectProperty<>(blameInfo); + }); + + setCellFactory(column -> new TableCell<>() + { + private final Rectangle colorIndicator = new Rectangle(8, 16); + private final Label textLabel = new Label(); + private final HBox content = new HBox(5); + + { + colorIndicator.setArcWidth(3); + colorIndicator.setArcHeight(3); + content.setAlignment(Pos.CENTER_LEFT); + content.setPadding(new Insets(2, 0, 2, 0)); + content.getChildren().addAll(colorIndicator, textLabel); + } + + @Override + protected void updateItem(GitBlameInfo blameInfo, boolean empty) + { + super.updateItem(blameInfo, empty); + + if (empty || blameInfo == null) + { + setText(null); + setGraphic(null); + setTooltip(null); + return; + } + + setText(null); + textLabel.setText(blameInfo.toString()); + colorIndicator.setStyle("-fx-fill: " + blameInfo.getCommitColor() + ";"); + + final String tooltipText = String.format( + "Author: %s <%s>\nCommit: %s\nDate: %s\n\n%s", + blameInfo.getAuthorName(), + blameInfo.getAuthorEmail(), + blameInfo.getShortCommitHash(), + DATE_FORMATTER.format(blameInfo.getCommitTime()), + blameInfo.getShortCommitMessage() + ); + + final Tooltip tooltip = new Tooltip(tooltipText); + tooltip.setShowDelay(Duration.millis(300)); + setTooltip(tooltip); + + setGraphic(content); + } + }); + + model.getForObservation().registerObserver(this); + } + + @Override + public void observe(com.daniel.jsoneditor.model.observe.Subject subjectToObserve) + { + subjectToObserve.registerObserver(this); + } + + @Override + public void update() + { + if (model.getLatestEvent().getEvent() == EventEnum.GIT_BLAME_LOADED) + { + Platform.runLater(() -> { + if (getTableView() != null) + { + getTableView().refresh(); + } + }); + } + } +} diff --git a/src/main/resources/icons/material/darkmode/outline_git_blame_white_24dp.png b/src/main/resources/icons/material/darkmode/outline_git_blame_white_24dp.png new file mode 100644 index 0000000..9b2a9b2 Binary files /dev/null and b/src/main/resources/icons/material/darkmode/outline_git_blame_white_24dp.png differ