diff --git a/README.md b/README.md index 3d7ef9f..23eb81c 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ For installation and workflow instructions, see the [OpenFastTrace IntelliJ Plug The plugin currently focuses on: * syntax highlighting for OFT items in supported files * navigation support (`Go To Declaration`, symbol search, and related navigation paths) -* completion for existing specification item IDs while editing `Covers:` entries in supported specification documents and coverage-tag targets in files supported by the OpenFastTrace Tag Importer +* completion for existing specification item IDs while editing Markdown declaration ID fields, `Covers:` entries in supported specification documents, and coverage-tag targets in files supported by the OpenFastTrace Tag Importer * bundled OFT live templates for common specification items, including a scenario template * project-wide or selected-resource OFT tracing through custom OpenFastTrace run configurations, including pre-configured templates * clickable specification item IDs directly from the trace output window, including generated IDs created from coverage tags @@ -90,7 +90,7 @@ Run configurations can select plain text output instead. In that mode, the plugi ## Live-Templates Bundled With the Plugin -The plugin also bundles an `OpenFastTrace` live-template group under `Settings | Editor | Live Templates`. Use abbreviations such as `feat`, `req`, `dsn`, and `scn` in a supported editing context, then press `Tab` to insert an OFT item skeleton. The `scn` template inserts a scenario stub with placeholders for `Given`, `When`, `Then`, and the covered requirement. While the caret is still in a template's covered-item field, use basic completion to select an existing specification item ID from the project index. +The plugin also bundles an `OpenFastTrace` live-template group under `Settings | Editor | Live Templates`. Use abbreviations such as `feat`, `req`, `dsn`, and `scn` in a supported editing context, then press `Tab` to insert an OFT item skeleton. The `scn` template inserts a scenario stub with placeholders for `Given`, `When`, `Then`, and the covered requirement. While the caret is still in a template's covered-item field, use basic completion to select an existing specification item ID from the project index. In Markdown specification documents, basic completion also works while editing the declaration ID field of an OFT item, and it stays off Markdown link destinations nested inside `Covers:` entries. ## Installation ### Runtime Dependencies diff --git a/doc/changes/changes_0.10.0.md b/doc/changes/changes_0.10.0.md index e36ad03..6b1a8f9 100644 --- a/doc/changes/changes_0.10.0.md +++ b/doc/changes/changes_0.10.0.md @@ -7,7 +7,10 @@ The Test Runner UI now marks transitive defects with a leading `↳` so they sta OpenFastTrace 4.8.0 +## Feature + +* #66: Transitive defects now use a visible `↳` prefix in the Test Runner UI + ## Bugfix * #39: Re-enabled tag filter test -* #66: Transitive defects now use a visible `↳` prefix in the Test Runner UI diff --git a/doc/changesets/68-restrict-markdown-specification-item-id-completion-to-actual-id-field.md b/doc/changesets/68-restrict-markdown-specification-item-id-completion-to-actual-id-field.md new file mode 100644 index 0000000..2e4549e --- /dev/null +++ b/doc/changesets/68-restrict-markdown-specification-item-id-completion-to-actual-id-field.md @@ -0,0 +1,86 @@ +# GH-68 Restrict Markdown specification item ID completion to the actual ID field + +## Goal + +Make Markdown specification-item ID completion fire only while the caret is in the actual declaration ID field, not in the title field or other non-ID text. + +Keep the existing `Covers:` and coverage-tag completion behavior, ranking, and candidate source unchanged while tightening the Markdown declaration context and standardizing the ID/name-part terminology in user-facing text. + +## Scope + +In scope: + +* add Markdown declaration-ID completion scenarios that activate only in the actual specification-item ID field +* suppress completion in the title field, surrounding body text, and other non-ID Markdown text +* cover live-template expansion states where the title is still being edited and the template also contains a derived ID field +* keep the existing `Covers:` and coverage-tag completion paths unchanged +* normalize user-facing wording to use `ID` for the technical anchor and `name-part` for the human-readable part where this issue touches text + +Out of scope: + +* changing completion ranking or the declaration index +* changing completion behavior for `Covers:` entries or coverage-tag targets +* adding completion to new file types or new OFT contexts +* changing bundled live-template placeholder order or the generated skeleton structure unless a wording-only rename is needed +* touching unrelated historic changesets + +## Design References + +* [System Requirements](../system_requirements.md) +* [Quality Requirements](../design/quality_requirements.md) +* [Solution Strategy](../design/solution_strategy.md) +* [Building Block View](../design/building_block_view.md) +* [Runtime View](../design/runtime_view.md) +* [GH-24 Auto-completion for "Covers" section](24-auto-completion-for-covers-section.md) +* [GH-29 Completion in Covers Sections During Active Live-Template Expansion](29-completion-in-covers-sections-during-live-template-expansion.md) + +## Strategy + +Treat GH-68 as a completion-context refinement for Markdown declaration authoring rather than a change to the shared completion ranking or indexed candidate source. + +The implementation should reuse the existing declaration index and completion lookup path, but add a Markdown declaration-ID field detector that can distinguish the actual ID anchor from the title field and any other non-ID text in the same specification item. If the current live-template structure makes the ID field and title field ambiguous to completion, tighten the field detection there instead of broadening completion to unrelated text. + +The terminology cleanup should follow the same rule: prefer `ID` for the canonical technical anchor and `name-part` for the human-readable portion of the identifier wherever this issue updates user-facing text, template text, or test names. + +## Task List + +- [ ] Create and checkout a new Git branch `bugfix/68-restrict-markdown-specification-item-id-completion-to-actual-id-field` + +### Requirements And Design + +- [x] Add a feature-level requirement and user requirement in `doc/system_requirements.md` for Markdown specification item ID completion that activates only in the actual declaration ID field +- [x] Add scenarios for completion in the Markdown declaration ID field +- [x] Stop and ask user for a review of the system requirements +- [x] Update `doc/design/solution_strategy.md` so Markdown declaration-ID completion is described as a separate authoring context that still reuses IntelliJ completion and the existing declaration index +- [x] Update `doc/design/building_block_view.md` and `doc/design/runtime_view.md` with the Markdown declaration-ID completion responsibilities, field detection, and negative activation cases +- [x] Stop and ask user for a review of the design + +### Implementation + +- [x] Refine the Markdown completion context detection in `src/main/java/org/itsallcode/openfasttrace/intellijplugin/navigation/OftSpecificationCompletionProvider.java` so the shared completion path only activates in the actual declaration ID field +- [x] Keep the existing `Covers:` and coverage-tag completion paths unchanged while the Markdown declaration-ID path is tightened +- [ ] Update bundled live-template text or helper metadata in `src/main/resources/liveTemplates/OpenFastTrace.xml` and `src/main/java/org/itsallcode/openfasttrace/intellijplugin/templates/OftLiveTemplates.java` only if wording needs to be aligned to `ID` and `name-part` +- [x] Add or adjust implementation coverage tags for the new design items and terminology updates + +### Verification + +- [x] Add platform tests that prove completion appears in the Markdown declaration ID field and does not appear in the title field, surrounding body text, or an active live-template title entry +- [x] Add regression coverage showing that existing `Covers:` and coverage-tag completion behavior, ranking, and candidate selection remain unchanged +- [x] Add focused unit tests for the Markdown completion context detector and any live-template field helpers introduced by the fix +- [x] Keep the OpenFastTrace trace clean for the requirement and design artifacts in scope +- [x] Keep path coverage at or above the documented threshold +- [x] Run `./gradlew test` +- [x] Run `./gradlew traceRequirements` +- [x] Run `./gradlew buildPlugin` +- [x] Run `./gradlew verifyPlugin` +- [ ] Keep SonarQube Cloud quality-gate checks green +- [ ] Keep OSS Index audit results clean + +### Update User Documentation + +- [x] Update `README.md` and any authoring guidance touched by this issue so the terminology uses `ID` and `name-part` consistently + +## Version And Changelog Update + +- [ ] Check whether the current project version needs a bugfix release bump +- [ ] Write the changelog entry if this issue is included in a release diff --git a/doc/design/building_block_view.md b/doc/design/building_block_view.md index 17524bc..601bf7b 100644 --- a/doc/design/building_block_view.md +++ b/doc/design/building_block_view.md @@ -269,11 +269,13 @@ Needs: impl ### Specification Item Completion `dsn~specification-item-completion~1` -The plugin provides a specification-item completion component that activates IntelliJ basic completion for supported OFT reference authoring contexts, reads declared specification item IDs from the project-local declaration index, and presents those IDs in a deterministic order based on full-ID prefix, name-prefix, name-substring, and artifact-type prefix matches. Supported contexts include OFT item references under `Covers:` in supported specification documents, completion requests started from an active live-template placeholder when the placeholder expands inside a `Covers:` entry, and the target side of likely OFT coverage tags in source-code comments for the default file extensions supported by the upstream OpenFastTrace Tag Importer after a left-hand artifact type and arrow. +The plugin provides a specification-item completion component that activates IntelliJ basic completion for supported OFT reference authoring contexts, reads declared specification item IDs from the project-local declaration index, and presents those IDs in a deterministic order based on full-ID prefix, name-prefix, name-substring, and artifact-type prefix matches. Supported contexts include the actual Markdown specification-item ID field, OFT item references under `Covers:` in supported specification documents, completion requests started from an active live-template placeholder when the placeholder expands inside a `Covers:` entry, and the target side of likely OFT coverage tags in source-code comments for the default file extensions supported by the upstream OpenFastTrace Tag Importer after a left-hand artifact type and arrow. The component excludes the Markdown title field and other non-ID text by using a context detector that identifies the declaration anchor explicitly rather than relying only on document structure, keeps the active live-template session in place while ignoring the title placeholder until the caret reaches the actual declaration ID field, and skips Markdown link destinations nested inside `Covers:` entries so link-target anchor completion stays available there. Covers: +- `scn~complete-markdown-specification-item-id-in-declaration-id-field~1` - `scn~complete-specification-item-id-in-covers-section~1` - `scn~complete-specification-item-id-in-active-live-template-covers-field~1` +- `scn~suppress-specification-item-id-completion-in-markdown-link-targets-inside-covers-entries~1` - `scn~complete-specification-item-id-in-coverage-tag-target~1` - `scn~complete-specification-item-id-in-spaced-coverage-tag-target~1` - `scn~complete-specification-item-id-in-incomplete-coverage-tag-target~1` diff --git a/doc/design/runtime_view.md b/doc/design/runtime_view.md index 309441b..11ecd7f 100644 --- a/doc/design/runtime_view.md +++ b/doc/design/runtime_view.md @@ -226,6 +226,19 @@ Needs: impl, itest ## Completion +### Complete Markdown Specification Item ID in Declaration ID Field +`dsn~complete-markdown-specification-item-id-in-declaration-id-field~1` + +**Given** a supported Markdown specification document contains a specification-item declaration and the caret is in the actual declaration ID field +**When** a user invokes IntelliJ basic completion while editing that declaration ID +**Then** the completion component confirms the Markdown declaration-ID context, loads declared specification item IDs from the declaration index, and shows the ordered suggestions in the standard IDE completion popup +**And** the same Markdown context detector rejects the title field, other non-ID text, and a still-active live-template title placeholder until the caret reaches the actual declaration ID field. + +Covers: +- `scn~complete-markdown-specification-item-id-in-declaration-id-field~1` + +Needs: impl, itest + ### Complete Specification Item ID in Covers Section `dsn~complete-specification-item-id-in-covers-section~1` @@ -240,6 +253,18 @@ Covers: Needs: impl, itest +### Suppress Specification Item ID Completion in Markdown Link Targets Inside Covers Entries +`dsn~suppress-specification-item-id-completion-in-markdown-link-targets-inside-covers-entries~1` + +**Given** a supported specification document contains a `Covers:` entry with a Markdown link and the caret is inside the link destination +**When** a user invokes IntelliJ basic completion there +**Then** the completion component does not add OpenFastTrace specification item ID suggestions inside that Markdown link destination so anchor completion can remain in control. + +Covers: +- `scn~suppress-specification-item-id-completion-in-markdown-link-targets-inside-covers-entries~1` + +Needs: impl, itest, utest + ### Complete Specification Item ID in Active Live Template Covers Field `dsn~complete-specification-item-id-in-active-live-template-covers-field~1` diff --git a/doc/design/solution_strategy.md b/doc/design/solution_strategy.md index 59574ac..35ae81a 100644 --- a/doc/design/solution_strategy.md +++ b/doc/design/solution_strategy.md @@ -22,7 +22,7 @@ Parsing and syntax-aware editor behavior use the IntelliJ parsing, PSI, lexer, a Authoring shortcuts use IntelliJ's live-template infrastructure instead of custom OpenFastTrace-specific insertion dialogs or wizards. The plugin bundles a repository-owned OFT live-template XML file, registers it through the standard live-template extension point, and keeps template behavior within the declarative capabilities that IntelliJ already provides by default. Template placeholders that create `Covers:` entries remain ordinary editable live-template fields, while the completion component serves user-invoked reference completion in those fields while a live-template session is still active. -Reference authoring assistance for `Covers:` entries and coverage-tag targets likewise reuses IntelliJ's standard completion infrastructure. The plugin activates completion only in supported OFT reference authoring contexts, then fills the suggestion list from the existing declaration index instead of maintaining a second source of specification-item identities. +Reference authoring assistance for Markdown declaration IDs, `Covers:` entries, and coverage-tag targets likewise reuses IntelliJ's standard completion infrastructure. The plugin activates completion only in supported OFT reference authoring contexts, then fills the suggestion list from the existing declaration index instead of maintaining a second source of specification-item identities. Markdown declaration-ID completion is deliberately narrower than the broader `Covers:` and coverage-tag cases: the completion context detector must identify the actual declaration anchor and not the surrounding title field or other non-ID text. This strategy reduces custom code, lowers maintenance effort, and improves cross-IDE compatibility because the implementation stays aligned with the platform abstractions that JetBrains supports across products. diff --git a/doc/system_requirements.md b/doc/system_requirements.md index 05fec3a..cc306fc 100644 --- a/doc/system_requirements.md +++ b/doc/system_requirements.md @@ -320,6 +320,28 @@ Covers: Needs: scn +#### Suppress Specification Item ID Completion in Markdown Link Targets Inside Covers Entries +`req~suppress-specification-item-id-completion-in-markdown-link-targets-inside-covers-entries~1` + +The plugin does not suggest existing OpenFastTrace specification item IDs when a user invokes completion inside the destination part of a Markdown link nested in a `Covers:` entry. Users can keep the link destination available for Markdown anchor completion without OFT IDs taking over the popup. + +Covers: +- `feat~oft-reference-completion~1` + +Needs: scn + +### Suppress Specification Item ID Completion in Markdown Link Targets Inside Covers Entries +`scn~suppress-specification-item-id-completion-in-markdown-link-targets-inside-covers-entries~1` + +**Given** a supported specification document contains a `Covers:` entry with a Markdown link and the caret is inside the link destination +**When** a user invokes completion there +**Then** the plugin does not suggest OpenFastTrace specification item IDs inside that Markdown link destination + +Covers: +- `req~suppress-specification-item-id-completion-in-markdown-link-targets-inside-covers-entries~1` + +Needs: dsn + #### Complete Specification Item IDs in Coverage Tag Target `req~complete-specification-item-ids-in-coverage-tag-target~1` @@ -1008,7 +1030,36 @@ Needs: dsn ### OFT Reference Completion -The following scenarios describe completion support while editing OFT references in `Covers:` sections and coverage-tag targets. +The following scenarios describe completion support while editing OFT references in `Covers:` sections and coverage-tag targets. Markdown declaration ID completion is described in the feature block below. + +### Markdown Specification Item ID Completion +`feat~markdown-specification-item-id-completion~1` + +The plugin suggests existing OpenFastTrace specification item IDs while users edit the actual declaration ID field in supported Markdown specification documents. Users can complete declaration anchors from indexed declarations. + +Needs: req + +#### Complete Markdown Specification Item ID in Declaration ID Field +`req~complete-markdown-specification-item-ids-in-declaration-id-field~1` + +The plugin suggests existing OpenFastTrace specification item IDs while users invoke completion in the actual specification item ID field of a Markdown declaration. + +Covers: +- `feat~markdown-specification-item-id-completion~1` + +Needs: scn + +### Complete Markdown Specification Item ID in Declaration ID Field +`scn~complete-markdown-specification-item-id-in-declaration-id-field~1` + +**Given** a project contains declared OpenFastTrace specification items and a user edits the actual ID field of a Markdown specification item declaration +**When** the user types a partial specification item ID and invokes completion +**Then** the IDE suggests existing declared specification item IDs from the project index and orders the suggestion list by full-ID prefix match, then name-prefix match, then name-substring match, and finally artifact-type prefix match + +Covers: +- `req~complete-markdown-specification-item-ids-in-declaration-id-field~1` + +Needs: dsn ### Complete Specification Item ID in Covers Section `scn~complete-specification-item-id-in-covers-section~1` diff --git a/doc/user_guide.md b/doc/user_guide.md index 918ae94..8188729 100644 --- a/doc/user_guide.md +++ b/doc/user_guide.md @@ -100,7 +100,7 @@ The `scn` template inserts a Given-When-Then scenario skeleton. Templates with a ## Complete OFT References -Use basic completion while editing a `Covers:` entry to select an existing specification item ID from the project index. +Use basic completion while editing a Markdown declaration ID field or the OFT item text inside a `Covers:` entry to select an existing specification item ID from the project index. Markdown link destinations nested inside `Covers:` entries stay available for Markdown anchor completion instead. ![OpenFastTrace reference completion in a Covers entry](user_guide/images/reference-completion.png) diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/OftSupportedFiles.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/OftSupportedFiles.java index 3bd9b4d..fd8a0d8 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/OftSupportedFiles.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/OftSupportedFiles.java @@ -55,6 +55,11 @@ public static boolean isSpecificationFileName(final String fileName) { return SPECIFICATION_EXTENSIONS.contains(extensionOf(fileName)); } + public static boolean isMarkdownSpecificationFileName(final String fileName) { + final String extension = extensionOf(fileName); + return "md".equals(extension) || "markdown".equals(extension); + } + public static boolean isCoverageTagFile(final VirtualFile file) { return file != null && isCoverageTagFileName(file.getName()); } diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/navigation/OftMarkdownLinkDestinationContext.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/navigation/OftMarkdownLinkDestinationContext.java new file mode 100644 index 0000000..ecc3384 --- /dev/null +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/navigation/OftMarkdownLinkDestinationContext.java @@ -0,0 +1,54 @@ +package org.itsallcode.openfasttrace.intellijplugin.navigation; + +import java.util.Optional; + +final class OftMarkdownLinkDestinationContext { + private static final int NOT_FOUND = -1; + + private OftMarkdownLinkDestinationContext() { + } + + static Optional findAt(final CharSequence text, final int offset) { + final int boundedOffset = Math.clamp(offset, 0, text.length()); + final int lineStart = findLineStart(text, boundedOffset); + final int linkDestinationStart = findLastLinkDestinationStartBefore(text, lineStart, boundedOffset); + if (linkDestinationStart == NOT_FOUND || closesBeforeCaret(text, linkDestinationStart, boundedOffset)) { + return Optional.empty(); + } + return Optional.of(linkDestinationStart); + } + + private static int findLineStart(final CharSequence text, final int offset) { + int lineStart = offset; + while (lineStart > 0 && text.charAt(lineStart - 1) != '\n') { + lineStart--; + } + return lineStart; + } + + private static int findLastLinkDestinationStartBefore( + final CharSequence text, + final int startOffset, + final int endOffset + ) { + for (int index = endOffset - 2; index >= startOffset; index--) { + if (text.charAt(index) == ']' && text.charAt(index + 1) == '(') { + return index + 2; + } + } + return NOT_FOUND; + } + + private static boolean closesBeforeCaret( + final CharSequence text, + final int startOffset, + final int endOffset + ) { + for (int index = startOffset; index < endOffset; index++) { + if (text.charAt(index) == ')') { + return true; + } + } + return false; + } +} diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/navigation/OftMarkdownSpecificationCompletionContext.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/navigation/OftMarkdownSpecificationCompletionContext.java new file mode 100644 index 0000000..58559d9 --- /dev/null +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/navigation/OftMarkdownSpecificationCompletionContext.java @@ -0,0 +1,23 @@ +package org.itsallcode.openfasttrace.intellijplugin.navigation; + +import org.itsallcode.openfasttrace.intellijplugin.syntax.OftSpecificationItemMatch; +import org.itsallcode.openfasttrace.intellijplugin.syntax.OftSyntaxCore; + +import java.util.Optional; + +// [impl->dsn~complete-markdown-specification-item-id-in-declaration-id-field~1] +final class OftMarkdownSpecificationCompletionContext { + private OftMarkdownSpecificationCompletionContext() { + } + + static Optional findAt(final CharSequence text, final int offset) { + return OftSyntaxCore.findDefinitionSpecificationItems(text).stream() + .filter(match -> contains(match, offset)) + .map(match -> OftSpecificationCompletionSupport.specificationPrefixAt(text, offset)) + .findFirst(); + } + + private static boolean contains(final OftSpecificationItemMatch match, final int offset) { + return match.span().startOffset() <= offset && offset < match.span().endOffset(); + } +} diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/navigation/OftSpecificationCompletionProvider.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/navigation/OftSpecificationCompletionProvider.java index 43f2fec..f3ff296 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/navigation/OftSpecificationCompletionProvider.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/navigation/OftSpecificationCompletionProvider.java @@ -25,6 +25,8 @@ // [impl->dsn~specification-item-completion~1] // [impl->dsn~complete-specification-item-id-in-covers-section~1] // [impl->dsn~complete-specification-item-id-in-active-live-template-covers-field~1] +// [impl->dsn~complete-markdown-specification-item-id-in-declaration-id-field~1] +// [impl->dsn~suppress-specification-item-id-completion-in-markdown-link-targets-inside-covers-entries~1] // [impl->dsn~complete-specification-item-id-in-coverage-tag-target~1] // [impl->dsn~complete-specification-item-id-in-spaced-coverage-tag-target~1] // [impl->dsn~complete-specification-item-id-in-incomplete-coverage-tag-target~1] @@ -75,9 +77,19 @@ private static Optional completionPrefix( final int offset ) { final PsiFile originalFile = parameters.getOriginalFile(); + if (OftSupportedFiles.isMarkdownSpecificationFileName(originalFile.getName())) { + final Optional markdownPrefix = + OftMarkdownSpecificationCompletionContext.findAt(fileText, offset); + if (markdownPrefix.isPresent()) { + return markdownPrefix; + } + } if (OftSupportedFiles.isSpecificationFileName(originalFile.getName()) && OftDeclarationResolver.isInsideCoversSection(fileText, offset)) { - return Optional.of(OftSpecificationCompletionSupport.specificationPrefixAt(fileText, offset)); + if (OftMarkdownLinkDestinationContext.findAt(fileText, offset).isEmpty()) { + return Optional.of(OftSpecificationCompletionSupport.specificationPrefixAt(fileText, offset)); + } + return Optional.empty(); } if (OftSupportedFiles.isCoverageTagFileName(originalFile.getName())) { return OftCoverageTagCompletionContext.findAt(fileText, offset) diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/AbstractOftPlatformTestCase.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/AbstractOftPlatformTestCase.java index 355bfb8..3697955 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/AbstractOftPlatformTestCase.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/AbstractOftPlatformTestCase.java @@ -2,7 +2,12 @@ import com.intellij.codeInsight.daemon.impl.HighlightInfo; import com.intellij.openapi.editor.colors.TextAttributesKey; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.testFramework.LightProjectDescriptor; import com.intellij.testFramework.fixtures.BasePlatformTestCase; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.nio.file.Files; @@ -10,6 +15,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.UUID; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThanOrEqualTo; @@ -18,6 +24,17 @@ public abstract class AbstractOftPlatformTestCase extends BasePlatformTestCase { private final List managedTestArtifactDirectories = new ArrayList<>(); + @Override + protected LightProjectDescriptor getProjectDescriptor() { + return new LightProjectDescriptor() { + @Override + public @Nullable VirtualFile createDirForSources(@NotNull final Module module) { + // Give each light test a fresh temp source root so IntelliJ does not reuse deleted VFS entries. + return createSourceRoot(module, "src-" + UUID.randomUUID()); + } + }; + } + protected boolean hasHighlight(final List infos, final String fragment, final TextAttributesKey key) { final String text = myFixture.getEditor().getDocument().getText(); final int startOffset = text.indexOf(fragment); diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/navigation/OftMarkdownLinkDestinationContextTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/navigation/OftMarkdownLinkDestinationContextTest.java new file mode 100644 index 0000000..a4a11e1 --- /dev/null +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/navigation/OftMarkdownLinkDestinationContextTest.java @@ -0,0 +1,49 @@ +package org.itsallcode.openfasttrace.intellijplugin.navigation; + +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +class OftMarkdownLinkDestinationContextTest { + // [utest->dsn~suppress-specification-item-id-completion-in-markdown-link-targets-inside-covers-entries~1] + @Test + void givenMarkdownLinkDestinationWhenFindingContextThenItReturnsTheDestinationStart() { + final String text = """ + Covers: + * [req~live-template-alpha.feature~1](#) + """; + + assertThat(findContext(text).isPresent(), is(true)); + } + + @Test + void givenMarkdownLinkTextWhenFindingContextThenItReturnsNoContext() { + final String text = """ + Covers: + * [req~live-template-alpha.feature~1](#feature) + """; + + assertThat(findContext(text), is(Optional.empty())); + } + + @Test + void givenNonLinkCoversTextWhenFindingContextThenItReturnsNoContext() { + final String text = """ + Covers: + * req~live-template-alpha.feature~1 + """; + + assertThat(findContext(text), is(Optional.empty())); + } + + private static Optional findContext(final String text) { + final int offset = text.indexOf(""); + if (offset < 0) { + throw new IllegalArgumentException("Missing marker in test text: " + text); + } + return OftMarkdownLinkDestinationContext.findAt(text.replace("", ""), offset); + } +} diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/navigation/OftMarkdownSpecificationCompletionContextTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/navigation/OftMarkdownSpecificationCompletionContextTest.java new file mode 100644 index 0000000..663d707 --- /dev/null +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/navigation/OftMarkdownSpecificationCompletionContextTest.java @@ -0,0 +1,92 @@ +package org.itsallcode.openfasttrace.intellijplugin.navigation; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Optional; +import java.util.stream.Stream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +class OftMarkdownSpecificationCompletionContextTest { + // [utest->dsn~specification-item-completion~1] + @ParameterizedTest + @MethodSource("markdownDeclarationIdContexts") + void givenMarkdownDeclarationIdWhenFindingContextThenItReturnsThePrefix( + final String text, + final String expectedPrefix + ) { + final MarkedText markedText = markedText(text); + + final Optional context = OftMarkdownSpecificationCompletionContext.findAt( + markedText.text(), + markedText.offset() + ); + + assertThat(context, is(Optional.of(expectedPrefix))); + } + + // [utest->dsn~specification-item-completion~1] + @ParameterizedTest + @MethodSource("nonMarkdownDeclarationIdContexts") + void givenNonMarkdownDeclarationIdWhenFindingContextThenItReturnsNoContext(final String text) { + final MarkedText markedText = markedText(text); + + final Optional context = OftMarkdownSpecificationCompletionContext.findAt( + markedText.text(), + markedText.offset() + ); + + assertThat(context, is(Optional.empty())); + } + + private static Stream markdownDeclarationIdContexts() { + return Stream.of( + Arguments.of(""" + ### Title + req~markdown-completion.feature~1 + """, "req~markdown-completion.fea"), + Arguments.of(""" + ### Title + `req~markdown-completion.feature~1` + """, "req~markdown-completion.fea"), + Arguments.of(""" + ### Title + req~markdown-completion.feature~1 + """, "req~markdown-completion") + ); + } + + private static Stream nonMarkdownDeclarationIdContexts() { + return Stream.of( + """ + ### Title + req~markdown-completion.feature~1 + """, + """ + ### Title + Body text outside the ID field. + req~markdown-completion.feature~1 + """, + """ + ### Title + req~markdown-completion.feature~1 + + More body text below the declaration. + """ + ); + } + + private static MarkedText markedText(final String text) { + final int offset = text.indexOf(""); + if (offset < 0) { + throw new IllegalArgumentException("Missing marker in test text: " + text); + } + return new MarkedText(text.replace("", ""), offset); + } + + private record MarkedText(String text, int offset) { + } +} diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/navigation/OftSpecificationCompletionTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/navigation/OftSpecificationCompletionTest.java index dce6d66..ccfa431 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/navigation/OftSpecificationCompletionTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/navigation/OftSpecificationCompletionTest.java @@ -8,9 +8,73 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.is; public class OftSpecificationCompletionTest extends AbstractOftPlatformTestCase { + // [itest->dsn~complete-markdown-specification-item-id-in-declaration-id-field~1] + public void testGivenMarkdownDeclarationIdFieldWhenBasicCompletionInvokesThenItSuggestsDeclaredSpecificationIds() { + myFixture.addFileToProject("doc/markdown-completion.md", """ + req~markdown-completion-target.feature~1 + Needs: scn + """); + myFixture.configureByText("current.md", """ + ### Markdown Completion + req~markdown-completion-source.feature~1 + + Body text. + """); + + final LookupElement[] elements = myFixture.completeBasic(); + + org.junit.jupiter.api.Assertions.assertAll( + () -> assertThat(elements.length, is(2)), + () -> assertThat( + myFixture.getLookupElementStrings(), + hasItems( + "req~markdown-completion-source.feature~1", + "req~markdown-completion-target.feature~1" + ) + ) + ); + } + + // [itest->dsn~complete-markdown-specification-item-id-in-declaration-id-field~1] + public void testGivenMarkdownTitleFieldWhenBasicCompletionInvokesThenItDoesNotSuggestSpecificationIds() { + myFixture.addFileToProject("doc/markdown-completion.md", """ + req~markdown-completion-target.feature~1 + Needs: scn + """); + myFixture.configureByText("current.md", """ + ### Markdown Completion + req~markdown-completion-target.feature~1 + + Body text. + """); + + myFixture.completeBasic(); + + assertThat(lookupStrings(), is(empty())); + } + + // [itest->dsn~complete-markdown-specification-item-id-in-declaration-id-field~1] + public void testGivenMarkdownBodyTextWhenBasicCompletionInvokesThenItDoesNotSuggestSpecificationIds() { + myFixture.addFileToProject("doc/markdown-completion.md", """ + req~markdown-completion-target.feature~1 + Needs: scn + """); + myFixture.configureByText("current.md", """ + ### Markdown Completion + req~markdown-completion-target.feature~1 + + Body text outside the declaration ID field. + """); + + myFixture.completeBasic(); + + assertThat(lookupStrings(), is(empty())); + } + // [itest->dsn~complete-specification-item-id-in-covers-section~1] public void testGivenCoversSectionWhenBasicCompletionInvokesThenItSuggestsDeclaredSpecificationIds() { myFixture.addFileToProject("doc/spec.md", """ diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/templates/OftLiveTemplatesTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/templates/OftLiveTemplatesTest.java index 34e4e06..a94ca28 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/templates/OftLiveTemplatesTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/templates/OftLiveTemplatesTest.java @@ -23,6 +23,7 @@ import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.not; public class OftLiveTemplatesTest extends AbstractOftPlatformTestCase { private static final String DESIGN_TEMPLATE_KEY = "dsn"; @@ -140,6 +141,68 @@ public void testGivenActiveLiveTemplateCoveredFieldWhenCompletionInvokesThenItSu ); } + // [itest->dsn~complete-markdown-specification-item-id-in-declaration-id-field~1] + public void testGivenActiveLiveTemplateTitleFieldWhenCompletionInvokesThenItDoesNotSuggestDeclaredSpecificationIds() { + myFixture.addFileToProject("doc/spec.md", """ + req~live-template-alpha.feature~1 + Needs: scn + """); + myFixture.configureByText("current.md", ""); + + final TemplateImpl designTemplate = TemplateSettings.getInstance() + .getTemplate(DESIGN_TEMPLATE_KEY, OftLiveTemplates.GROUP_NAME); + TemplateManagerImpl.setTemplateTesting(getTestRootDisposable()); + TemplateManager.getInstance(getProject()).startTemplate(myFixture.getEditor(), designTemplate); + + final TemplateState templateState = Objects.requireNonNull(TemplateManagerImpl.getTemplateState(myFixture.getEditor())); + advanceTemplateToVariable(getProject(), templateState, "TITLE"); + myFixture.type("Markdown Completion"); + + myFixture.completeBasic(); + + Assertions.assertAll( + () -> assertThat(templateState, notNullValue()), + () -> assertThat(activeVariableName(templateState), is("TITLE")), + () -> assertThat(lookupStrings(), is(List.of())), + () -> assertThat( + TemplateManager.getInstance(getProject()).getActiveTemplate(myFixture.getEditor()), + notNullValue(Template.class) + ) + ); + } + + // [itest->dsn~suppress-specification-item-id-completion-in-markdown-link-targets-inside-covers-entries~1] + public void testGivenActiveLiveTemplateMarkdownLinkFieldWhenCompletionInvokesThenItDoesNotSuggestDeclaredSpecificationIds() { + myFixture.addFileToProject("doc/spec.md", """ + req~live-template-alpha.feature~1 + Needs: scn + """); + myFixture.configureByText("current.md", ""); + + final TemplateImpl requestTemplate = TemplateSettings.getInstance() + .getTemplate("req", OftLiveTemplates.GROUP_NAME); + TemplateManagerImpl.setTemplateTesting(getTestRootDisposable()); + TemplateManager.getInstance(getProject()).startTemplate(myFixture.getEditor(), requestTemplate); + + final TemplateState templateState = Objects.requireNonNull(TemplateManagerImpl.getTemplateState(myFixture.getEditor())); + advanceTemplateToVariable(getProject(), templateState, "MARKDOWN_LINK"); + + myFixture.completeBasic(); + + Assertions.assertAll( + () -> assertThat(templateState, notNullValue()), + () -> assertThat(activeVariableName(templateState), is("MARKDOWN_LINK")), + () -> assertThat( + lookupStrings(), + not(hasItems("req~live-template-alpha.feature~1")) + ), + () -> assertThat( + TemplateManager.getInstance(getProject()).getActiveTemplate(myFixture.getEditor()), + notNullValue(Template.class) + ) + ); + } + private static void advanceTemplateToVariable( final Project project, final TemplateState templateState, @@ -167,4 +230,9 @@ private static String activeVariableName(final TemplateState templateState) { final int currentVariableNumber = templateState.getCurrentVariableNumber(); return templateState.getTemplate().getVariableNameAt(currentVariableNumber); } + + private List lookupStrings() { + final List lookupElementStrings = myFixture.getLookupElementStrings(); + return lookupElementStrings == null ? List.of() : lookupElementStrings; + } } diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfigurationTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfigurationTest.java index aaf5aa0..06a891f 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfigurationTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfigurationTest.java @@ -12,10 +12,6 @@ import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceSettingsSnapshot; import org.jdom.Element; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.api.function.Executable; import java.util.Arrays; @@ -27,18 +23,7 @@ import static org.hamcrest.Matchers.sameInstance; // [itest->dsn~openfasttrace-run-configuration~2] -@SuppressWarnings("JUnitMixedFramework") public class OftRunConfigurationTest extends AbstractOftPlatformTestCase { - @BeforeEach - void initPlatformFixture() throws Exception { - super.setUp(); - } - - @AfterEach - void releasePlatformFixture() throws Exception { - super.tearDown(); - } - // [itest->dsn~test-runner-as-default-run-configuration-result-view~1] // [itest->dsn~trace-configuration-integration~2] public void testGivenNewRunConfigurationWhenReadingSnapshotThenItDefaultsToTestRunner() { @@ -178,14 +163,51 @@ public void testGivenRunConfigurationTypeWhenCheckingFactorySingletonPolicyThenI ); } - @ParameterizedTest(name = "{0}") - @CsvSource({ - "'User requirements', false, false, false, 'doc/', 'feat, req, scn, bconstr'", - "'Design and above', false, false, false, 'doc/', 'feat, req, scn, bconstr, arch, dsn, constr, bld'", - "'Typical project', false, true, true, 'doc/', ''", - "'Unfiltered', true, false, false, '.', ''" - }) - void testGivenRunConfigurationTemplateWhenCreatingConfigurationThenItHasCorrectSettings( + public void testGivenUserRequirementsTemplateWhenCreatingConfigurationThenItHasCorrectSettings() { + assertTemplateSettings( + "User requirements", + false, + false, + false, + "doc/", + "feat, req, scn, bconstr" + ); + } + + public void testGivenDesignAndAboveTemplateWhenCreatingConfigurationThenItHasCorrectSettings() { + assertTemplateSettings( + "Design and above", + false, + false, + false, + "doc/", + "feat, req, scn, bconstr, arch, dsn, constr, bld" + ); + } + + public void testGivenTypicalProjectTemplateWhenCreatingConfigurationThenItHasCorrectSettings() { + assertTemplateSettings( + "Typical project", + false, + true, + true, + "doc/", + "" + ); + } + + public void testGivenUnfilteredTemplateWhenCreatingConfigurationThenItHasCorrectSettings() { + assertTemplateSettings( + "Unfiltered", + true, + false, + false, + ".", + "" + ); + } + + private void assertTemplateSettings( final String templateName, final boolean wholeProject, final boolean includeSourceRoots,