Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ list (or wait for it to appear), then select the desired OpenFastTrace snippet.

Snippets are based on the [OFT IntelliJ plugin](https://github.com/itsallcode/openfasttrace-intellij-plugin/blob/main/src/main/resources/liveTemplates/OpenFastTrace.xml).

### OFT declaration highlighting

Markdown and reStructuredText OFT declarations, such as `req~example~1`, are
highlighted immediately while retaining their normal document language mode.
This first lexical highlighting slice does not validate declarations. Coverage
tags in source code will be highlighted in a follow-up PR using LSP semantic
tokens.

## Requirements

The extension requires Java 21 or later. It uses `openfasttrace.java.home` when
Expand Down
71 changes: 71 additions & 0 deletions doc/changesets/04-textmate-syntax-highlighting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# GH-4 TextMate Syntax Highlighting

## Goal

Provide immediate, lexical OpenFastTrace declaration highlighting in Markdown
and reStructuredText without changing either document's normal language mode.

## Scope

In scope:

* Contribute one TextMate injection grammar for Markdown and reStructuredText.
* Highlight standalone OFT declarations in the `type~name~revision` form.
* Add automated grammar contribution checks and user documentation.

Out of scope:

* OFT parsing, validation, diagnostics, or semantic tokens.
* Highlighting coverage tags in source, configuration, or markup files.

Coverage tags for source code will be added in a follow-up PR using LSP
semantic tokens.

## Design References

* [System Requirements](../system_requirements.md)
* [LSP-first Architecture](../design/architecture.md)
* [Quality Requirements](../design/quality_requirements.md)

## Strategy

Inject one grammar into VS Code's `text.html.markdown` and `source.rst` host
scopes. It supplies fast lexical scopes for OFT declaration components while
leaving the host grammar responsible for all ordinary Markdown and
reStructuredText content.

## Task List

### Requirements And Design

- [x] Revise the editor-presentation design item to describe TextMate-only
declaration highlighting in Markdown and reStructuredText, and justify
LSP semantic tokens for cross-language coverage-tag styling.
- [x] Preserve the existing backlog design classification and confirm the MVP
trace remains clean.

### Implementation

- [x] Contribute the TextMate injection grammar and target the Markdown and
reStructuredText host scopes.
- [x] Scope the declaration, artifact type, name, and revision independently
for theme authors.

### Verification

- [x] Add tests for the grammar contribution, its host scopes, valid OFT
declaration forms, and near-miss input.
- [x] Run extension compile, lint, unit/integration tests, and the
OpenFastTrace requirements trace.

### Update User Documentation

- [x] Document immediate declaration highlighting and the deferred
source-code coverage-tag highlighting follow-up.

## Version and Changelog Update

- [ ] Check the current version against the latest GitHub release.
- [ ] Increment the semantic version for this feature.
- [ ] Add the release changelog entry, including the bundled OpenFastTrace
version from resolved Gradle dependency metadata.
18 changes: 13 additions & 5 deletions doc/design/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,19 @@ Covers:
- `impl~workspace-symbol-protocol-slice~1`

### Editor Presentation
`dsn~editor-presentation~1`

The VS Code adapter contributes a Markdown TextMate injection grammar for
immediate declaration coloring and maps LSP semantic tokens to OFT coverage-tag
styling. It preserves the host language of every document.
`dsn~editor-presentation~3`

The VS Code adapter contributes a TextMate injection grammar for immediate OFT
declaration coloring in Markdown and reStructuredText. It preserves the host
language of every document. A later change maps LSP semantic tokens to OFT
coverage-tag styling in source, configuration, and markup files.

TextMate grammar injection is intentionally not used for coverage tags. The
set of supported source, configuration, and markup languages would require
per-host grammar injection rules and lexical matching cannot reliably separate
valid coverage tags from lookalike text. The language server instead owns OFT
tag parsing and supplies semantic tokens, so coverage-tag styling is consistent
across host languages and can later reflect parsed validity and link resolution.

Tags: backlog

Expand Down
10 changes: 10 additions & 0 deletions extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@
"path": "./snippets/openfasttrace-restructuredtext.code-snippets"
}
],
"grammars": [
{
"scopeName": "source.openfasttrace.injection",
"path": "./syntaxes/openfasttrace.tmLanguage.json",
"injectTo": [
"text.html.markdown",
"source.rst"
]
}
],
"configuration": {
"title": "OpenFastTrace",
"properties": {
Expand Down
88 changes: 88 additions & 0 deletions extension/src/test/integration/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,28 @@ const templateLabels = [
];

const supportedDocumentLanguages = ['markdown', 'restructuredtext'];
const matchingOftIds = [
'arch~system-architecture~1',
'arch~System.Architecture~1',
'arch~initial-revision~0',
'arch~long-revision~123',
'bconstr~budget~2',
'constr~java-version~3',
'custom_artifact~highlighting~1',
'CUSTOM~UPPER.Case~1',
'dsn~editor-presentation~2',
'feat~recognize-oft-content~1',
'qg~quality~1',
'qs~fast-feedback~1',
'req~highlight-oft-declarations~1',
'scn~recognize-and-highlight-oft-content~1',
];
const nonMatchingOftIds = [
'req~not-kebab_case~1',
'req~missing-revision~one',
'req~negative-revision~-1',
`[impl${'->'}dsn~editor-presentation~2]`,
];

interface SnippetDefinition {
body: string[];
Expand All @@ -35,6 +57,21 @@ interface SnippetContribution {
path: string;
}

interface GrammarContribution {
scopeName: string;
path: string;
injectTo: string[];
}

interface TextMateGrammar {
scopeName: string;
injectionSelector: string;
patterns: Array<{
match: string;
captures: Record<string, { name: string }>;
}>;
}

suite('OpenFastTrace extension', () => {
test('starts the language server and returns an empty workspace symbol result', async () => {
const extension = vscode.extensions.getExtension<OpenFastTraceExports>('itsallcode.openfasttrace');
Expand Down Expand Up @@ -73,6 +110,38 @@ suite('OpenFastTrace extension', () => {
});
}
}

test('contributes a declaration grammar for Markdown and reStructuredText', async () => {
const grammar = await openFastTraceGrammar();

assert.deepStrictEqual(grammar.injectTo, ['text.html.markdown', 'source.rst']);
assert.equal(grammar.definition.scopeName, grammar.scopeName);
assert.equal(grammar.definition.injectionSelector, 'L:text.html.markdown, L:markup.inline.raw.string.markdown, L:source.rst');

const captures = grammar.definition.patterns[1].captures;
assert.equal(captures['1'].name, 'entity.name.openfasttrace.declaration');
assert.equal(captures['2'].name, 'keyword.control.openfasttrace.artifact-type');
assert.equal(captures['3'].name, 'entity.name.openfasttrace.artifact-name');
assert.equal(captures['4'].name, 'constant.numeric.openfasttrace.revision');
});

for (const id of matchingOftIds) {
test(`recognizes ${id}`, async () => {
const grammar = await openFastTraceGrammar();

assert.match(id, declarationPattern(grammar));
assert.match(`\`${id}\``, backtickedDeclarationPattern(grammar));
});
}

for (const id of nonMatchingOftIds) {
test(`does not recognize ${id}`, async () => {
const grammar = await openFastTraceGrammar();

assert.doesNotMatch(id, declarationPattern(grammar));
assert.doesNotMatch(`\`${id}\``, backtickedDeclarationPattern(grammar));
});
}
});

async function snippetsFor(language: string): Promise<Record<string, SnippetDefinition>> {
Expand All @@ -85,3 +154,22 @@ async function snippetsFor(language: string): Promise<Record<string, SnippetDefi
const contents = await vscode.workspace.fs.readFile(vscode.Uri.joinPath(extension.extensionUri, contribution.path));
return JSON.parse(new TextDecoder().decode(contents)) as Record<string, SnippetDefinition>;
}

async function openFastTraceGrammar(): Promise<{ scopeName: string; injectTo: string[]; definition: TextMateGrammar }> {
const extension = vscode.extensions.getExtension('itsallcode.openfasttrace');
assert.ok(extension, 'OpenFastTrace extension must be available to the test host');
const contributions = extension.packageJSON.contributes.grammars as GrammarContribution[];
const grammar = contributions.find(candidate => candidate.scopeName === 'source.openfasttrace.injection');

assert.ok(grammar, 'OpenFastTrace must contribute its TextMate injection grammar');
const contents = await vscode.workspace.fs.readFile(vscode.Uri.joinPath(extension.extensionUri, grammar.path));
return { scopeName: grammar.scopeName, injectTo: grammar.injectTo, definition: JSON.parse(new TextDecoder().decode(contents)) as TextMateGrammar };
}

function backtickedDeclarationPattern(grammar: { definition: TextMateGrammar }): RegExp {
return new RegExp(grammar.definition.patterns[0].match);
}

function declarationPattern(grammar: { definition: TextMateGrammar }): RegExp {
return new RegExp(grammar.definition.patterns[1].match);
}
46 changes: 46 additions & 0 deletions extension/syntaxes/openfasttrace.tmLanguage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"scopeName": "source.openfasttrace.injection",
"injectionSelector": "L:text.html.markdown, L:markup.inline.raw.string.markdown, L:source.rst",
"patterns": [
{
"match": "(`+)(([^~\\s>\\[\\]]+)~([A-Za-z][A-Za-z0-9.-]*)~([0-9]+))(\\1)",
"captures": {
"1": {
"name": "punctuation.definition.raw.markdown"
},
"2": {
"name": "entity.name.openfasttrace.declaration"
},
"3": {
"name": "keyword.control.openfasttrace.artifact-type"
},
"4": {
"name": "entity.name.openfasttrace.artifact-name"
},
"5": {
"name": "constant.numeric.openfasttrace.revision"
},
"6": {
"name": "punctuation.definition.raw.markdown"
}
}
},
{
"match": "(?<!->)\\b(([^~\\s>\\[\\]]+)~([A-Za-z][A-Za-z0-9.-]*)~([0-9]+))\\b",
"captures": {
"1": {
"name": "entity.name.openfasttrace.declaration"
},
"2": {
"name": "keyword.control.openfasttrace.artifact-type"
},
"3": {
"name": "entity.name.openfasttrace.artifact-name"
},
"4": {
"name": "constant.numeric.openfasttrace.revision"
}
}
}
]
}