Skip to content

Add SourceViewer#computeStyleRanges() to highlight external documents#3868

Open
tobiasmelcher wants to merge 4 commits intoeclipse-platform:masterfrom
tobiasmelcher:computeStyleRanges
Open

Add SourceViewer#computeStyleRanges() to highlight external documents#3868
tobiasmelcher wants to merge 4 commits intoeclipse-platform:masterfrom
tobiasmelcher:computeStyleRanges

Conversation

@tobiasmelcher
Copy link
Copy Markdown
Contributor

Introduces computeStyleRanges(IDocument, IRegion) on SourceViewer, which applies the viewer's own presentation reconciler and partitioner to an external document, returning the resulting SWT StyleRanges for the given region without affecting the viewer's current document.
This method will be used by the unified diff implementation to calculate the syntax coloring for the parts rendered inside the code mining which are not part of the original editor document (see eclipse-platform/eclipse.platform#2560).

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 6, 2026

Test Results

   852 files  ± 0     852 suites  ±0   53m 33s ⏱️ -47s
 7 908 tests + 9   7 665 ✅ + 9  243 💤 ±0  0 ❌ ±0 
20 226 runs  +27  19 571 ✅ +27  655 💤 ±0  0 ❌ ±0 

Results for commit 392609f. ± Comparison against base commit 9b991c8.

♻️ This comment has been updated with latest results.

@tobiasmelcher
Copy link
Copy Markdown
Contributor Author

test error org.eclipse.e4.ui.tests.workbench.PartRenderingEngineTests#ensureCleanUpAddonCleansUp not related to this change (#3581)

@vogella
Copy link
Copy Markdown
Contributor

vogella commented Apr 17, 2026

test error org.eclipse.e4.ui.tests.workbench.PartRenderingEngineTests#ensureCleanUpAddonCleansUp not related to this change (#3581)

This test is fixed now (since yesterday)

Copy link
Copy Markdown
Contributor

@vogella vogella left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idea is sound and SourceViewer is a reasonable place for this. Main concern is exception-safety of the partitioner swap (see inline on the final connect(originalDocument)); the rest are tightenings.

* The viewer's partitioner is temporarily connected to the given document so that its
* presentation repairers can compute the correct highlighting. The viewer's own document is not
* affected.
* </p>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Javadoc says "The viewer's own document is not affected", but this method temporarily disconnects the original document's partitioner and reconnects it only on the happy path. Worth documenting the side effect on the original document's partitioner state, and the UI-thread-only assumption.

* @throws BadLocationException if {@code damage} is outside the bounds of {@code document}
* @since 3.31
*/
public List<StyleRange> computeStyleRanges(IDocument document, IRegion damage) throws BadLocationException {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: damage leaks reconciler/repairer jargon into a public API. For callers who aren't thinking in damage/repair terms, region would read more naturally.

}
IDocument originalDocument= getDocument();
IDocumentPartitioner partitioner= originalDocument.getDocumentPartitioner();
document.setDocumentPartitioner(partitioner);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This document.setDocumentPartitioner(partitioner) is redundant with the IDocumentExtension3 branch below, which sets a partitioner on document again via ext.setDocumentPartitioner(partition, ...). In the common case the partitioner is set twice. Consider picking one path.

Comment on lines +1448 to +1452
try {
originalDocumentPartitioner.connect(document);
} finally {
ext.setDocumentPartitioner(partition, originalDocumentPartitioner);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The finally here runs ext.setDocumentPartitioner(partition, originalDocumentPartitioner) on the external document even when connect(document) throws. That isn't a safety pattern — it's guaranteed cleanup pointed at the wrong object. If connect fails, the original document's partitioner is already disconnected and nothing restores it.

Comment on lines +1466 to +1468
if (originalDocumentPartitioner != null) {
originalDocumentPartitioner.connect(originalDocument);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not exception-safe. If TextUtilities.computePartitioning or any repairer.createPresentation above throws, this connect(originalDocument) never runs and the viewer is left with its own document's partitioner connected to the external document. The whole swap-and-use block should be wrapped in try { ... } finally { if (originalDocumentPartitioner != null) originalDocumentPartitioner.connect(originalDocument); } (plus restoring any partitioner mapping that was changed on the original document).

Introduces computeStyleRanges(IDocument, IRegion) on SourceViewer,
which applies the viewer's own presentation reconciler and partitioner
to an external document, returning the resulting SWT StyleRanges for
the given region without affecting the viewer's current document.
@tobiasmelcher
Copy link
Copy Markdown
Contributor Author

c47d26a

thanks a lot @vogella for the detailed review. I fixed the issues with c47d26a - could you please verify? Thanks a lot.

@danthe1st
Copy link
Copy Markdown
Contributor

I'm not a committer but I guess it might be useful to add some tests ensuring there won't be any regressions here?

@trancexpress
Copy link
Copy Markdown
Contributor

I'm not a committer but I guess it might be useful to add some tests ensuring there won't be any regressions here?

For API especially.

@tobiasmelcher
Copy link
Copy Markdown
Contributor Author

I'm not a committer but I guess it might be useful to add some tests ensuring there won't be any regressions here?

For API especially.

@danthe1st @trancexpress test added with d6456a7

@vogella
Copy link
Copy Markdown
Contributor

vogella commented Apr 18, 2026

Thanks for the iteration @tobiasmelcher. Two remaining concerns before I'm comfortable approving. The unit-test angle can be handled in a follow-up.

  1. External document left with a dangling partitioner reference. In the IDocumentExtension3 branch you call ((IDocumentExtension3) document).setDocumentPartitioner(partition, originalDocumentPartitioner) but never clear it. After the method returns, the caller's external document still holds a reference to a partitioner that the finally block has (correctly) reconnected to originalDocument. The else branch has the same leak via document.setDocumentPartitioner(originalDocument.getDocumentPartitioner()). Could you null out / restore the external document's partitioner in finally so the caller's document is left in the state we found it?

  2. originalDocument may be null. If computeStyleRanges is called on a viewer whose document hasn't been set yet, originalExt.getDocumentPartitioner(partition) and originalDocument.getDocumentPartitioner() will NPE. A small guard (early return or Assert.isNotNull) would make the contract explicit.

SourceViewer#computeStyleRanges()
@tobiasmelcher
Copy link
Copy Markdown
Contributor Author

Thanks for the iteration @tobiasmelcher. Two remaining concerns before I'm comfortable approving. The unit-test angle can be handled in a follow-up.

  1. External document left with a dangling partitioner reference. In the IDocumentExtension3 branch you call ((IDocumentExtension3) document).setDocumentPartitioner(partition, originalDocumentPartitioner) but never clear it. After the method returns, the caller's external document still holds a reference to a partitioner that the finally block has (correctly) reconnected to originalDocument. The else branch has the same leak via document.setDocumentPartitioner(originalDocument.getDocumentPartitioner()). Could you null out / restore the external document's partitioner in finally so the caller's document is left in the state we found it?
  2. originalDocument may be null. If computeStyleRanges is called on a viewer whose document hasn't been set yet, originalExt.getDocumentPartitioner(partition) and originalDocument.getDocumentPartitioner() will NPE. A small guard (early return or Assert.isNotNull) would make the contract explicit.

@vogella thanks a lot for the review - should be now done with 392609f

documentExt.setDocumentPartitioner(partition, originalDocumentPartitioner);
} else {
externalDocPartitioner= document.getDocumentPartitioner();
document.setDocumentPartitioner(originalDocument.getDocumentPartitioner());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This else branch is inconsistent with the IDocumentExtension3 path above: it swaps the partitioner reference on the external document via setDocumentPartitioner but never calls disconnect() / connect(document). The partitioner is still connected to originalDocument while being asked to partition document, which can produce wrong results or corrupt the partitioner's internal state. The matching finally branch has the same issue.

Since Document (the standard implementation) always implements IDocumentExtension3, one clean option is to drop the else branch entirely and require IDocumentExtension3 on both documents (throwing or returning an empty list otherwise). Alternatively, mirror the disconnect/connect dance from the happy path:

Suggested change
document.setDocumentPartitioner(originalDocument.getDocumentPartitioner());
} else {
externalDocPartitioner= document.getDocumentPartitioner();
IDocumentPartitioner origPartitioner= originalDocument.getDocumentPartitioner();
if (origPartitioner != null) {
origPartitioner.disconnect();
origPartitioner.connect(document);
}
document.setDocumentPartitioner(origPartitioner);
}

The finally branch would need the symmetric disconnect/reconnect back to originalDocument. Also, please add a test that exercises a non-IDocumentExtension3 document so this path isn't untested.

Assert.isTrue(Display.getCurrent() != null, "computeStyleRanges must be called from SWT UI thread"); //$NON-NLS-1$
IDocument originalDocument= getDocument();
Assert.isNotNull(originalDocument, "viewer must have a document before calling computeStyleRanges"); //$NON-NLS-1$
String partition= IDocumentExtension3.DEFAULT_PARTITIONING;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming nit: this variable holds a partitioning name (a namespace like DEFAULT_PARTITIONING), not a partition/content-type. IPresentationReconcilerExtension#getDocumentPartitioning() and IDocumentExtension3#getDocumentPartitioner(String partitioning) use the term "partitioning". Renaming to partitioning (and extPartitionextPartitioning) would match surrounding API vocabulary and make the calls below read correctly.

Suggested change
String partition= IDocumentExtension3.DEFAULT_PARTITIONING;
String partitioning= IDocumentExtension3.DEFAULT_PARTITIONING;

Requires the same rename at the uses further down (documentExt.getDocumentPartitioner(partition), setDocumentPartitioner(partition, ...), TextUtilities.computePartitioning(document, partition, ...)).

externalDocPartitioner= document.getDocumentPartitioner();
document.setDocumentPartitioner(originalDocument.getDocumentPartitioner());
}
TextPresentation presentation= new TextPresentation(region, 1000);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic number: 1000 is an expected-size hint for TextPresentation. For small regions it over-allocates; for large regions it under-allocates. Derive it from the region length (or drop the hint and use the default constructor) so it scales with actual input.

Suggested change
TextPresentation presentation= new TextPresentation(region, 1000);
TextPresentation presentation= new TextPresentation(region, Math.max(region.getLength() / 10, 16));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got this magic value from PresentationReconciler.createPresentation. There is no constructor which allows to pass only a damage region. I will take over your proposal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants