-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Extract references from related work section (Part 1) #15316
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
Merged
Merged
Changes from 1 commit
Commits
Show all changes
52 commits
Select commit
Hold shift + click to select a range
b4f1af6
Extract text about papers from "related work" sections
pluto-han 0f5e61f
Update comments
pluto-han a39f109
small refactor
pluto-han b29e400
Merge remote-tracking branch 'origin/fix-for-issue-14085' into fix-fo…
pluto-han 08df1f3
small refactor of comments
pluto-han 2e16150
Merge branch 'main' into fix-for-issue-14085
pluto-han 960a263
Support more citation format
pluto-han 89c8f94
Merge branch 'main' into fix-for-issue-14085
pluto-han b9cc731
Modify logic for UI
pluto-han a294b9e
Add UI
pluto-han dd186e1
make `RelatedWorkAction` inline
pluto-han 8e7c5d1
add empty check before getting optional value
pluto-han dd0ace9
Merge branch 'main' into fix-for-issue-14085
koppor 71adb96
add javadoc and example to RelatedWorkSnippet.java
pluto-han 98175dc
Update jabgui/src/main/java/org/jabref/gui/relatedwork/RelatedWorkRes…
pluto-han 1ec6337
Merge remote-tracking branch 'origin/fix-for-issue-14085' into fix-fo…
pluto-han f3b12b2
Fix comment style
pluto-han 3aebab4
Add backticks around `comment-{username}``
pluto-han 703995b
Add @NullMarked
pluto-han ff0491b
Change parameter order
pluto-han a95837d
Change variable order
pluto-han f564f63
remove copyOf
pluto-han 0bf0af3
remove copyOf
pluto-han fa587bd
remove `skipped`
pluto-han b1dca63
change comment style
pluto-han 846875b
add comment
pluto-han 22543d7
Add backticks around `comment-{username}`
pluto-han be7c4ee
Add more explanation in the comment
pluto-han 9688e8b
Add "undefined" to `getCitationKey`
pluto-han 0957641
get owner from preference
pluto-han 9944d01
Add action helper to check if PDF file is present
pluto-han b2fe2a6
Hihglight non-editabel fileds differently
pluto-han d62b984
Update CHANGELOG.md
pluto-han f773117
Fix failed test
pluto-han 1c65b9c
Focus related work text field, auto insert clipboard content
pluto-han 559fb01
Use sealed interface instead of enum
pluto-han 66dfb99
Merge branch 'main' into fix-for-issue-14085
pluto-han 72a01c5
Fix failed tests
pluto-han 5be52f3
Merge remote-tracking branch 'origin/fix-for-issue-14085' into fix-fo…
pluto-han 8294c6f
Add logger
pluto-han a9226c3
Trigger CI
pluto-han 39a2943
Replace all page separators to "-"
pluto-han 3c2c6a3
Split RelatedWorkService to matcher and inserter
pluto-han 5a0e3b3
Replace "[^a-z0-9]" as a constant NON_ALPHANUMERIC
pluto-han 0ac6dc8
Merge branch 'main' into fix-for-issue-14085
pluto-han 43d579d
use switch to replace ifelse
pluto-han 6fd763e
use mock to create filepreference
pluto-han 0cfce22
Merge remote-tracking branch 'origin/fix-for-issue-14085' into fix-fo…
pluto-han 5d85486
Normalize linebreak
pluto-han b3a76a6
Add requirement
pluto-han 1d95e03
Add requirement
pluto-han 6540124
Fix md format fail
pluto-han File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
18 changes: 18 additions & 0 deletions
18
jablib/src/main/java/org/jabref/logic/relatedwork/RelatedWorkInsertionResult.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package org.jabref.logic.relatedwork; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| import org.jabref.model.FieldChange; | ||
|
|
||
| import org.jspecify.annotations.NullMarked; | ||
|
|
||
| @NullMarked | ||
| public record RelatedWorkInsertionResult( | ||
| RelatedWorkMatchResult matchResult, | ||
| RelatedWorkInsertionStatus status, | ||
| Optional<FieldChange> fieldChange | ||
| ) { | ||
| public boolean success() { | ||
| return status == RelatedWorkInsertionStatus.INSERTED || status == RelatedWorkInsertionStatus.UNCHANGED; | ||
| } | ||
| } |
10 changes: 10 additions & 0 deletions
10
jablib/src/main/java/org/jabref/logic/relatedwork/RelatedWorkInsertionStatus.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package org.jabref.logic.relatedwork; | ||
|
|
||
| /// INSERTED: Insert successfully | ||
| /// UNCHANGED: [citation-key]: xxxxx already exists, do not insert | ||
| /// SKIPPED: Cannot find a matched related work | ||
| public enum RelatedWorkInsertionStatus { | ||
| INSERTED, | ||
| UNCHANGED, | ||
| SKIPPED, | ||
| } | ||
21 changes: 21 additions & 0 deletions
21
jablib/src/main/java/org/jabref/logic/relatedwork/RelatedWorkMatchResult.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package org.jabref.logic.relatedwork; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| import org.jabref.model.entry.BibEntry; | ||
|
|
||
| import org.jspecify.annotations.NullMarked; | ||
|
|
||
| @NullMarked | ||
| /// parsedReference: parsed bib entry in Reference section | ||
| /// matchedLibraryBibEntry: matched bib entry in the library | ||
| public record RelatedWorkMatchResult( | ||
| String contextText, | ||
| String citationKey, | ||
| BibEntry parsedReference, | ||
| Optional<BibEntry> matchedLibraryBibEntry | ||
| ) { | ||
| public boolean hasMatchedLibraryEntry() { | ||
| return matchedLibraryBibEntry.isPresent(); | ||
| } | ||
| } |
72 changes: 72 additions & 0 deletions
72
jablib/src/main/java/org/jabref/logic/relatedwork/RelatedWorkReferenceResolver.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| package org.jabref.logic.relatedwork; | ||
|
|
||
| import java.io.IOException; | ||
| import java.nio.file.Path; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
|
|
||
| import org.jabref.logic.FilePreferences; | ||
| import org.jabref.logic.importer.ParserResult; | ||
| import org.jabref.logic.importer.fileformat.pdf.RuleBasedBibliographyPdfImporter; | ||
| import org.jabref.model.database.BibDatabaseContext; | ||
| import org.jabref.model.entry.BibEntry; | ||
| import org.jabref.model.entry.LinkedFile; | ||
|
|
||
| import org.apache.pdfbox.Loader; | ||
| import org.apache.pdfbox.pdmodel.PDDocument; | ||
| import org.jspecify.annotations.NullMarked; | ||
|
|
||
| @NullMarked | ||
| public class RelatedWorkReferenceResolver { | ||
|
|
||
| private final RuleBasedBibliographyPdfImporter bibliographyPdfImporter; | ||
|
|
||
| public RelatedWorkReferenceResolver() { | ||
| this.bibliographyPdfImporter = new RuleBasedBibliographyPdfImporter(); | ||
| } | ||
|
|
||
| /// Parse references from the given PDF file | ||
| /// | ||
| /// @param linkedFile The attached PDF file of the selected bib entry | ||
| /// @return Map from citation key to the corresponding reference entries | ||
| public Map<String, BibEntry> parseReferences(LinkedFile linkedFile, | ||
| BibDatabaseContext databaseContext, | ||
| FilePreferences filePreferences) throws IOException { | ||
| Path pdfPath = resolvePdfPath(linkedFile, databaseContext, filePreferences); | ||
| List<BibEntry> parsedEntries = parseReferenceEntries(pdfPath); | ||
|
|
||
| Map<String, BibEntry> entriesByMarker = new HashMap<>(); | ||
| for (BibEntry parsedEntry : parsedEntries) { | ||
| Optional<String> citationKey = parsedEntry.getCitationKey(); | ||
| if (citationKey.isPresent()) { | ||
|
pluto-han marked this conversation as resolved.
Outdated
|
||
| String citationMarker = "[" + citationKey.get() + "]"; | ||
|
pluto-han marked this conversation as resolved.
Outdated
|
||
| entriesByMarker.putIfAbsent(citationMarker, parsedEntry); | ||
| } | ||
| } | ||
|
|
||
| return entriesByMarker; | ||
| } | ||
|
|
||
| /// Get absolute path of PDF file | ||
| private Path resolvePdfPath(LinkedFile linkedFile, | ||
|
pluto-han marked this conversation as resolved.
Outdated
|
||
| BibDatabaseContext databaseContext, | ||
| FilePreferences filePreferences) { | ||
| Optional<Path> resolvedPath = linkedFile.findIn(databaseContext, filePreferences); | ||
|
|
||
| return resolvedPath.get(); | ||
| } | ||
|
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| /// Locate Reference section and parse references | ||
| /// | ||
| /// @param pdfPath Absolute path of PDF file | ||
| /// @return List of reference entries | ||
| public List<BibEntry> parseReferenceEntries(Path pdfPath) throws IOException { | ||
| try (PDDocument document = Loader.loadPDF(pdfPath.toFile())) { | ||
| ParserResult parserResult = bibliographyPdfImporter.importDatabase(pdfPath, document); | ||
|
|
||
| return List.copyOf(parserResult.getDatabase().getEntries()); | ||
| } | ||
| } | ||
| } | ||
138 changes: 138 additions & 0 deletions
138
jablib/src/main/java/org/jabref/logic/relatedwork/RelatedWorkService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| package org.jabref.logic.relatedwork; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Locale; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
|
|
||
| import org.jabref.logic.FilePreferences; | ||
| import org.jabref.logic.database.DuplicateCheck; | ||
| import org.jabref.logic.os.OS; | ||
| import org.jabref.model.FieldChange; | ||
| import org.jabref.model.database.BibDatabaseContext; | ||
| import org.jabref.model.entry.BibEntry; | ||
| import org.jabref.model.entry.LinkedFile; | ||
| import org.jabref.model.entry.field.UserSpecificCommentField; | ||
|
|
||
| public class RelatedWorkService { | ||
|
calixtus marked this conversation as resolved.
Outdated
calixtus marked this conversation as resolved.
Outdated
|
||
|
|
||
| private final RelatedWorkTextParser relatedWorkTextParser; | ||
| private final RelatedWorkReferenceResolver relatedWorkReferenceResolver; | ||
| private final DuplicateCheck duplicateCheck; | ||
|
|
||
| public RelatedWorkService(RelatedWorkTextParser relatedWorkTextParser, RelatedWorkReferenceResolver relatedWorkReferenceResolver, DuplicateCheck duplicateCheck) { | ||
| this.relatedWorkTextParser = relatedWorkTextParser; | ||
| this.relatedWorkReferenceResolver = relatedWorkReferenceResolver; | ||
| this.duplicateCheck = duplicateCheck; | ||
| } | ||
|
|
||
| /// Try to find a matched bib entry in library | ||
| /// | ||
| /// @param sourceEntry selected bib entry in library | ||
| /// @return List of matched result | ||
| public List<RelatedWorkMatchResult> matchRelatedWork(BibEntry sourceEntry, | ||
| String relatedWorkText, | ||
| LinkedFile linkedFile, | ||
| BibDatabaseContext databaseContext, | ||
|
calixtus marked this conversation as resolved.
Outdated
|
||
| FilePreferences filePreferences) throws IOException { | ||
|
calixtus marked this conversation as resolved.
Outdated
|
||
| List<RelatedWorkSnippet> relatedWorkSnippets = relatedWorkTextParser.parseRelatedWork(relatedWorkText); | ||
| if (relatedWorkSnippets.isEmpty()) { | ||
| return List.of(); | ||
| } | ||
|
|
||
| Map<String, BibEntry> parsedReferencesByMarker = relatedWorkReferenceResolver.parseReferences(linkedFile, databaseContext, filePreferences); | ||
|
|
||
| return createMatchResults(sourceEntry, relatedWorkSnippets, parsedReferencesByMarker, databaseContext); | ||
| } | ||
|
|
||
| /// Insert {comment-username} to bib entry | ||
|
calixtus marked this conversation as resolved.
Outdated
|
||
| /// | ||
| /// @return List of insertion result | ||
| public List<RelatedWorkInsertionResult> insertMatchedRelatedWork(BibEntry sourceEntry, | ||
| List<RelatedWorkMatchResult> matchResults, | ||
| String userName) { | ||
| String sourceCitationKey = sourceEntry.getCitationKey().get(); | ||
|
|
||
| UserSpecificCommentField userSpecificCommentField = new UserSpecificCommentField(normalizeOwner(userName)); | ||
| List<RelatedWorkInsertionResult> insertionResults = new ArrayList<>(matchResults.size()); | ||
|
|
||
| for (RelatedWorkMatchResult matchResult : matchResults) { | ||
| if (matchResult.matchedLibraryBibEntry().isEmpty()) { | ||
| insertionResults.add(new RelatedWorkInsertionResult( | ||
| matchResult, | ||
| RelatedWorkInsertionStatus.SKIPPED, | ||
| Optional.empty() | ||
| )); | ||
| continue; | ||
| } | ||
|
|
||
| Optional<FieldChange> fieldChange = appendRelatedWorkComment( | ||
| sourceCitationKey, | ||
| matchResult.contextText(), | ||
| matchResult.matchedLibraryBibEntry().get(), | ||
| userSpecificCommentField | ||
| ); | ||
| insertionResults.add(new RelatedWorkInsertionResult( | ||
| matchResult, | ||
| fieldChange.isPresent() ? RelatedWorkInsertionStatus.INSERTED : RelatedWorkInsertionStatus.UNCHANGED, | ||
| fieldChange | ||
| )); | ||
| } | ||
|
|
||
| return insertionResults; | ||
| } | ||
|
|
||
| private List<RelatedWorkMatchResult> createMatchResults(BibEntry sourceEntry, | ||
| List<RelatedWorkSnippet> relatedWorkSnippets, | ||
| Map<String, BibEntry> referencesByMarker, | ||
| BibDatabaseContext databaseContext) { | ||
| List<RelatedWorkMatchResult> matchResults = new ArrayList<>(relatedWorkSnippets.size()); | ||
|
|
||
| for (RelatedWorkSnippet relatedWorkSnippet : relatedWorkSnippets) { | ||
| BibEntry parsedReference = referencesByMarker.get(relatedWorkSnippet.citationMarker()); | ||
| Optional<BibEntry> matchedLibraryEntry = findDuplicateBibEntry(sourceEntry, parsedReference, databaseContext); | ||
| matchResults.add(new RelatedWorkMatchResult( | ||
| relatedWorkSnippet.contextText(), | ||
| relatedWorkSnippet.citationMarker(), | ||
| parsedReference, | ||
| matchedLibraryEntry | ||
| )); | ||
|
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| return matchResults; | ||
| } | ||
|
|
||
| /// Find duplicate entry in library by parsedReference | ||
| private Optional<BibEntry> findDuplicateBibEntry(BibEntry sourceEntry, | ||
| BibEntry parsedReference, | ||
| BibDatabaseContext databaseContext) { | ||
| return duplicateCheck.containsDuplicate(databaseContext.getDatabase(), parsedReference, databaseContext.getMode()) | ||
| .filter(duplicateEntry -> !duplicateEntry.getId().equals(sourceEntry.getId())); | ||
| } | ||
|
|
||
| /// Insert comment-{username} to target bib entry | ||
|
calixtus marked this conversation as resolved.
Outdated
|
||
| /// If comment-{username} not exist, then create a new field; Otherwise append it and separate by an empty line. | ||
| /// | ||
| /// @param userSpecificCommentField comment-{username} | ||
| /// @return FieldChange represents if the field is changed | ||
| private Optional<FieldChange> appendRelatedWorkComment(String sourceCitationKey, | ||
| String contextText, | ||
| BibEntry matchedLibraryEntry, | ||
| UserSpecificCommentField userSpecificCommentField) { | ||
| String formattedComment = "[%s]: %s".formatted(sourceCitationKey, contextText); | ||
| String updatedComment = matchedLibraryEntry.getField(userSpecificCommentField) | ||
| .filter(existingComment -> !existingComment.isBlank()) | ||
| .map(existingComment -> existingComment.stripTrailing() + OS.NEWLINE + OS.NEWLINE + formattedComment) | ||
| .orElse(formattedComment); | ||
|
|
||
| return matchedLibraryEntry.setField(userSpecificCommentField, updatedComment); | ||
| } | ||
|
|
||
| /// John Doe -> john-doe | ||
| /// Test -> test | ||
| private String normalizeOwner(String userName) { | ||
| return userName.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9]", "-"); | ||
| } | ||
|
calixtus marked this conversation as resolved.
Outdated
|
||
| } | ||
7 changes: 7 additions & 0 deletions
7
jablib/src/main/java/org/jabref/logic/relatedwork/RelatedWorkSnippet.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package org.jabref.logic.relatedwork; | ||
|
|
||
| public record RelatedWorkSnippet( | ||
| String contextText, | ||
| String citationMarker | ||
| ) { | ||
|
calixtus marked this conversation as resolved.
|
||
| } | ||
64 changes: 64 additions & 0 deletions
64
jablib/src/main/java/org/jabref/logic/relatedwork/RelatedWorkTextParser.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| package org.jabref.logic.relatedwork; | ||
|
|
||
| import java.util.List; | ||
| import java.util.regex.MatchResult; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| import org.jspecify.annotations.NullMarked; | ||
|
|
||
| @NullMarked | ||
| public class RelatedWorkTextParser { | ||
| private static final Pattern SINGLE_CITE_PATTERN = Pattern.compile("\\[(\\d{1,3})\\]"); | ||
| private static final Pattern SEGMENT_SPLIT_PATTERN = Pattern.compile("(?<=[.!?])\\s+"); | ||
| private static final Pattern HYPHENATED_LINE_BREAK_PATTERN = Pattern.compile("-\\R"); | ||
| private static final Pattern NEWLINE_WITHOUT_SENTENCE_END_PATTERN = Pattern.compile("(?<![.:])\\R"); | ||
| private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+"); | ||
| private static final Pattern SPACE_BEFORE_PUNCTUATION_PATTERN = Pattern.compile("\\s+([,.;:!?])"); | ||
| private static final Pattern TRAILING_CONJUNCTION_WITH_PUNCTUATION_PATTERN = Pattern.compile("\\s+(?:and|or)([.?!])$", Pattern.CASE_INSENSITIVE); | ||
| private static final Pattern TRAILING_CONJUNCTION_PATTERN = Pattern.compile("\\s+(?:and|or)$", Pattern.CASE_INSENSITIVE); | ||
|
|
||
| /// This method first cuts long texts into separate sentence segments, and then parse these sentences. | ||
| public List<RelatedWorkSnippet> parseRelatedWork(String text) { | ||
| String normalizedText = getNormalText(text); | ||
|
|
||
| return SEGMENT_SPLIT_PATTERN.splitAsStream(normalizedText) | ||
| .filter(segment -> !segment.isBlank()) | ||
| .map(this::parseTextSegment) | ||
| .flatMap(List::stream) | ||
| .toList(); | ||
| } | ||
|
|
||
| /// Parse a sentence into citationKeys and text. | ||
| public List<RelatedWorkSnippet> parseTextSegment(String text) { | ||
| List<String> citationMarkers = SINGLE_CITE_PATTERN.matcher(text) | ||
| .results() | ||
| .map(MatchResult::group) | ||
| .toList(); | ||
|
|
||
| if (citationMarkers.isEmpty()) { | ||
| return List.of(); | ||
| } | ||
|
|
||
| String contextText = extractContextText(text); | ||
|
|
||
| return citationMarkers.stream() | ||
| .map(citationMarker -> new RelatedWorkSnippet(contextText, citationMarker)) | ||
| .toList(); | ||
| } | ||
|
|
||
| /// Remove citationKeys | ||
| private String extractContextText(String text) { | ||
| text = SINGLE_CITE_PATTERN.matcher(text).replaceAll(" "); | ||
| text = WHITESPACE_PATTERN.matcher(text.trim()).replaceAll(" "); | ||
| text = SPACE_BEFORE_PUNCTUATION_PATTERN.matcher(text).replaceAll("$1"); | ||
| text = TRAILING_CONJUNCTION_WITH_PUNCTUATION_PATTERN.matcher(text).replaceAll("$1"); | ||
| return TRAILING_CONJUNCTION_PATTERN.matcher(text).replaceAll("").trim(); | ||
| } | ||
|
|
||
| /// Some long words may contain a hyphen and a newline symbol. This method transforms these words into normal ones. | ||
| public String getNormalText(String text) { | ||
| text = HYPHENATED_LINE_BREAK_PATTERN.matcher(text).replaceAll(""); | ||
| text = NEWLINE_WITHOUT_SENTENCE_END_PATTERN.matcher(text).replaceAll(" "); | ||
| return WHITESPACE_PATTERN.matcher(text.trim()).replaceAll(" "); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.