Skip to content

Commit 59c8e70

Browse files
authored
Consider viewer's visible region when preparing underline StyleRange[].
Fixes #1220 (#1221)
1 parent 0f71d64 commit 59c8e70

2 files changed

Lines changed: 361 additions & 23 deletions

File tree

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Contributors to the Eclipse Foundation
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* https://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*
13+
* Contributors:
14+
* Jozef Tomek - initial implementation
15+
*******************************************************************************/
16+
17+
package org.eclipse.lsp4e.test.documentLink;
18+
19+
import static org.junit.Assert.assertEquals;
20+
21+
import java.util.ArrayList;
22+
import java.util.List;
23+
24+
import org.eclipse.jface.text.BadLocationException;
25+
import org.eclipse.jface.text.IDocument;
26+
import org.eclipse.jface.text.Region;
27+
import org.eclipse.jface.text.TextPresentation;
28+
import org.eclipse.jface.text.TextViewer;
29+
import org.eclipse.jface.text.source.projection.ProjectionAnnotation;
30+
import org.eclipse.jface.text.source.projection.ProjectionViewer;
31+
import org.eclipse.lsp4e.test.utils.AbstractTestWithProject;
32+
import org.eclipse.lsp4e.test.utils.TestUtils;
33+
import org.eclipse.lsp4e.tests.mock.MockLanguageServer;
34+
import org.eclipse.lsp4j.DocumentLink;
35+
import org.eclipse.lsp4j.Position;
36+
import org.eclipse.lsp4j.Range;
37+
import org.eclipse.swt.custom.StyleRange;
38+
import org.eclipse.swt.graphics.Color;
39+
import org.junit.Test;
40+
41+
public class DocumentLinkReconcilingTest extends AbstractTestWithProject {
42+
43+
private static final String CONTENT = """
44+
1st-line #LINK1 1st-line
45+
2nd-line #LINK2_START
46+
#LINK2_END 3rd-line #LINK3 3rd-line
47+
#LINK4
48+
5th-line #LINK5_START_#LINK5_END
49+
#LINK6_START
50+
#LINK6_END
51+
8th-line #LINK7 8th-line""";
52+
53+
private static final List<DocumentLink> CONTENT_LINKS = List.of(
54+
new DocumentLink(new Range(new Position(0, 9), new Position(0, 15)), "file://link1"),
55+
new DocumentLink(new Range(new Position(1, 9), new Position(2, 10)), "file://link2"),
56+
new DocumentLink(new Range(new Position(2, 20), new Position(2, 26)), "file://link3"),
57+
new DocumentLink(new Range(new Position(3, 0), new Position(3, 6)), "file://link4"),
58+
new DocumentLink(new Range(new Position(4, 9), new Position(4, 32)), "file://link5"),
59+
new DocumentLink(new Range(new Position(5, 0), new Position(6, 10)), "file://link6"),
60+
new DocumentLink(new Range(new Position(7, 9), new Position(7, 15)), "file://link7")
61+
);
62+
63+
public static final Color COLOR_1ST_LINE = new Color(255, 0, 0);
64+
public static final Color COLOR_2ND_LINE = new Color(225, 255, 0);
65+
public static final Color COLOR_3RD_LINE = new Color(255, 0, 255);
66+
public static final Color COLOR_4TH_LINE = new Color(0, 255, 0);
67+
public static final Color COLOR_5TH_LINE = new Color(0, 255, 255);
68+
public static final Color COLOR_6TH_LINE = new Color(128, 128, 255);
69+
public static final Color COLOR_7TH_LINE = new Color(128, 0, 0);
70+
public static final Color COLOR_8TH_LINE = new Color(0, 128, 0);
71+
72+
private List<TextPresentation> textPresentations = new ArrayList<>(4);
73+
74+
@Test
75+
public void testFullDocumentLinkReconciling() throws Exception {
76+
MockLanguageServer.INSTANCE.setDocumentLinks(CONTENT_LINKS);
77+
78+
TextViewer viewer = (TextViewer) TestUtils.openTextViewer(TestUtils.createUniqueTestFile(project, CONTENT));
79+
IDocument doc = viewer.getDocument();
80+
var pos = 0;
81+
var len = 0;
82+
viewer.getTextWidget().setStyleRanges(new StyleRange[] {
83+
textStyle(pos, len = doc.getLineLength(0), COLOR_1ST_LINE), // whole 1st line
84+
textStyle(pos += len, len = doc.getLineLength(1), COLOR_2ND_LINE), // whole 2nd line
85+
textStyle(pos += len, len = doc.getLineLength(2), COLOR_3RD_LINE), // whole 3rd line
86+
textStyle(pos += len, len = doc.getLineLength(3), COLOR_4TH_LINE), // whole 4th line
87+
textStyle(pos += len, len = doc.getLineLength(4), COLOR_5TH_LINE), // whole 5th line
88+
textStyle(pos += len, len = doc.getLineLength(5), COLOR_6TH_LINE), // whole 6th line
89+
textStyle(pos += len, len = doc.getLineLength(6), COLOR_7TH_LINE), // whole 7th line
90+
textStyle(pos += len, doc.getLineLength(7), COLOR_8TH_LINE) // whole 8th line
91+
});
92+
viewer.addTextPresentationListener(this::textPresentationListener);
93+
94+
waitForAndAssertTextPresentations(doc);
95+
96+
assertEquals(1, getStylesCount(textPresentations.get(0)));
97+
assertEquals(2, getStylesCount(textPresentations.get(1))); // link2 spans 2 visible lines, 2nd and 3rd
98+
assertEquals(1, getStylesCount(textPresentations.get(2)));
99+
assertEquals(1, getStylesCount(textPresentations.get(3)));
100+
assertEquals(1, getStylesCount(textPresentations.get(4)));
101+
assertEquals(2, getStylesCount(textPresentations.get(5))); // link6 spans 2 visible lines, 6th and 7th
102+
assertEquals(1, getStylesCount(textPresentations.get(6)));
103+
104+
var styles = viewer.getTextWidget().getStyleRanges();
105+
106+
assertEquals(20, styles.length);
107+
108+
pos = 0;
109+
len = 0;
110+
assertEquals(textStyle(pos, len = 9, COLOR_1ST_LINE), styles[0]);
111+
assertEquals(linkStyle(pos += len, len = 6, COLOR_1ST_LINE), styles[1]);
112+
assertEquals(textStyle(pos += len, len = 9 + 1, COLOR_1ST_LINE), styles[2]); // also new line char
113+
114+
assertEquals(textStyle(pos += len, len = 9, COLOR_2ND_LINE), styles[3]);
115+
assertEquals(linkStyle(pos += len, len = 12 + 1, COLOR_2ND_LINE), styles[4]); // also new line char
116+
117+
assertEquals(linkStyle(pos += len, len = 10, COLOR_3RD_LINE), styles[5]);
118+
assertEquals(textStyle(pos += len, len = 10, COLOR_3RD_LINE), styles[6]);
119+
assertEquals(linkStyle(pos += len, len = 6, COLOR_3RD_LINE), styles[7]);
120+
assertEquals(textStyle(pos += len, len = 9 + 1, COLOR_3RD_LINE), styles[8]); // also new line char
121+
122+
assertEquals(linkStyle(pos += len, len = 6, COLOR_4TH_LINE), styles[9]);
123+
assertEquals(textStyle(pos += len, len = 1, COLOR_4TH_LINE), styles[10]); // new line char
124+
125+
assertEquals(textStyle(pos += len, len = 9, COLOR_5TH_LINE), styles[11]);
126+
assertEquals(linkStyle(pos += len, len = 23, COLOR_5TH_LINE), styles[12]);
127+
assertEquals(textStyle(pos += len, len = 1, COLOR_5TH_LINE), styles[13]); // new line char
128+
129+
assertEquals(linkStyle(pos += len, len = 12 + 1, COLOR_6TH_LINE), styles[14]); // also new line char
130+
131+
assertEquals(linkStyle(pos += len, len = 10, COLOR_7TH_LINE), styles[15]);
132+
assertEquals(textStyle(pos += len, len = 1, COLOR_7TH_LINE), styles[16]); // new line char
133+
134+
assertEquals(textStyle(pos += len, len = 9, COLOR_8TH_LINE), styles[17]);
135+
assertEquals(linkStyle(pos += len, len = 6, COLOR_8TH_LINE), styles[18]);
136+
assertEquals(textStyle(pos += len, 9, COLOR_8TH_LINE), styles[19]);
137+
}
138+
139+
@Test
140+
public void testClippedDocumentLinkReconciling() throws Exception {
141+
MockLanguageServer.INSTANCE.setDocumentLinks(CONTENT_LINKS);
142+
143+
TextViewer viewer = (TextViewer) TestUtils.openTextViewer(TestUtils.createUniqueTestFile(project, CONTENT));
144+
IDocument doc = viewer.getDocument();
145+
int line5visibleLength = 21;
146+
int line3Start = doc.getLineOffset(2);
147+
int middleOfLink5 = doc.getLineOffset(4) + line5visibleLength;
148+
// set visible region to 3rd + 4th + half of 5th line, link1 + link6 + link7 are completely outside
149+
viewer.setVisibleRegion(
150+
line3Start, // TextViewer would align start of visible region to start of the line anyway
151+
middleOfLink5 - line3Start);
152+
var pos = 0;
153+
var len = 0;
154+
viewer.getTextWidget().setStyleRanges(new StyleRange[] {
155+
textStyle(pos += len, len = doc.getLineLength(2), COLOR_3RD_LINE), // whole 3rd line
156+
textStyle(pos += len, len = doc.getLineLength(3), COLOR_4TH_LINE), // whole 4th line
157+
textStyle(pos += len, line5visibleLength, COLOR_5TH_LINE) // visible part of 5th line
158+
});
159+
viewer.addTextPresentationListener(this::textPresentationListener);
160+
161+
waitForAndAssertTextPresentations(doc);
162+
163+
assertEquals(1, getStylesCount(textPresentations.get(0)));
164+
assertEquals(2, getStylesCount(textPresentations.get(1))); // link2 spans 2 lines, invisible 2nd and visible 3rd
165+
assertEquals(1, getStylesCount(textPresentations.get(2)));
166+
assertEquals(1, getStylesCount(textPresentations.get(3)));
167+
assertEquals(2, getStylesCount(textPresentations.get(4))); // end of visible region cuts link into 2 styles
168+
assertEquals(1, getStylesCount(textPresentations.get(5)));
169+
assertEquals(1, getStylesCount(textPresentations.get(6)));
170+
171+
var styles = viewer.getTextWidget().getStyleRanges();
172+
173+
assertEquals(8, styles.length);
174+
175+
pos = 0;
176+
len = 0;
177+
assertEquals(linkStyle(pos, len = 10, COLOR_3RD_LINE), styles[0]);
178+
assertEquals(textStyle(pos += len, len = 10, COLOR_3RD_LINE), styles[1]);
179+
assertEquals(linkStyle(pos += len, len = 6, COLOR_3RD_LINE), styles[2]);
180+
assertEquals(textStyle(pos += len, len = 9 + 1, COLOR_3RD_LINE), styles[3]); // also new line char
181+
182+
assertEquals(linkStyle(pos += len, len = 6, COLOR_4TH_LINE), styles[4]);
183+
assertEquals(textStyle(pos += len, len = 1, COLOR_4TH_LINE), styles[5]); // new line char
184+
185+
assertEquals(textStyle(pos += len, len = 9, COLOR_5TH_LINE), styles[6]);
186+
assertEquals(linkStyle(pos += len, 12, COLOR_5TH_LINE), styles[7]);
187+
}
188+
189+
@Test
190+
public void testDocumentWithFoldingLinkReconciling() throws Exception {
191+
MockLanguageServer.INSTANCE.setDocumentLinks(CONTENT_LINKS);
192+
193+
ProjectionViewer viewer = (ProjectionViewer) TestUtils.openTextViewer(TestUtils.createUniqueTestFile(project, CONTENT));
194+
IDocument doc = viewer.getDocument();
195+
// line 1 visible
196+
foldLines(0, 1, viewer); // fold line 2 into line 1
197+
// line 3 visible
198+
// line 4 visible
199+
foldLines(3, 4, viewer); // fold line 5 into line 4
200+
// line 6 visible
201+
foldLines(5, 6, viewer); // fold line 7 into line 6
202+
// line 8 visible
203+
204+
var pos = 0;
205+
var len = 0;
206+
viewer.getTextWidget().setStyleRanges(new StyleRange[] {
207+
textStyle(pos, len = doc.getLineLength(0), COLOR_1ST_LINE), // whole 1st line
208+
textStyle(pos += len, len = doc.getLineLength(2), COLOR_3RD_LINE), // whole 3rd line
209+
textStyle(pos += len, len = doc.getLineLength(3), COLOR_4TH_LINE), // whole 4th line
210+
textStyle(pos += len, len = doc.getLineLength(5), COLOR_6TH_LINE), // whole 6th line
211+
textStyle(pos += len, doc.getLineLength(7), COLOR_8TH_LINE) // whole 8th line
212+
});
213+
viewer.addTextPresentationListener(this::textPresentationListener);
214+
215+
waitForAndAssertTextPresentations(doc);
216+
217+
assertEquals(1, getStylesCount(textPresentations.get(0)));
218+
assertEquals(2, getStylesCount(textPresentations.get(1))); // link2 spans 2 lines, folded 2nd and visible 3rd
219+
assertEquals(1, getStylesCount(textPresentations.get(2)));
220+
assertEquals(1, getStylesCount(textPresentations.get(3)));
221+
assertEquals(1, getStylesCount(textPresentations.get(4)));
222+
assertEquals(2, getStylesCount(textPresentations.get(5))); // link6 spans 2 lines, visible 6th and folded 7th
223+
assertEquals(1, getStylesCount(textPresentations.get(6)));
224+
225+
var styles = viewer.getTextWidget().getStyleRanges();
226+
227+
assertEquals(13, styles.length);
228+
229+
pos = 0;
230+
len = 0;
231+
assertEquals(textStyle(pos, len = 9, COLOR_1ST_LINE), styles[0]);
232+
assertEquals(linkStyle(pos += len, len = 6, COLOR_1ST_LINE), styles[1]);
233+
assertEquals(textStyle(pos += len, len = 9 + 1, COLOR_1ST_LINE), styles[2]); // also new line char
234+
235+
assertEquals(linkStyle(pos += len, len = 10, COLOR_3RD_LINE), styles[3]);
236+
assertEquals(textStyle(pos += len, len = 10, COLOR_3RD_LINE), styles[4]);
237+
assertEquals(linkStyle(pos += len, len = 6, COLOR_3RD_LINE), styles[5]);
238+
assertEquals(textStyle(pos += len, len = 9 + 1, COLOR_3RD_LINE), styles[6]); // also new line char
239+
240+
assertEquals(linkStyle(pos += len, len = 6, COLOR_4TH_LINE), styles[7]);
241+
assertEquals(textStyle(pos += len, len = 1, COLOR_4TH_LINE), styles[8]); // new line char
242+
243+
assertEquals(linkStyle(pos += len, len = 12 + 1, COLOR_6TH_LINE), styles[9]); // also new line char
244+
245+
assertEquals(textStyle(pos += len, len = 9, COLOR_8TH_LINE), styles[10]);
246+
assertEquals(linkStyle(pos += len, len = 6, COLOR_8TH_LINE), styles[11]);
247+
assertEquals(textStyle(pos += len, 9, COLOR_8TH_LINE), styles[12]);
248+
}
249+
250+
private void waitForAndAssertTextPresentations(IDocument document) throws BadLocationException {
251+
TestUtils.waitForAndAssertCondition(1_000, () -> textPresentations.size() == 7);
252+
253+
Region linkRegion = linkRegion(0, document);
254+
assertEquals(linkRegion, textPresentations.get(0).getExtent());
255+
assertEquals(linkRegion, textPresentations.get(0).getCoverage());
256+
257+
linkRegion = linkRegion(1, document);
258+
assertEquals(linkRegion, textPresentations.get(1).getExtent());
259+
assertEquals(linkRegion, textPresentations.get(1).getCoverage());
260+
261+
linkRegion = linkRegion(2, document);
262+
assertEquals(linkRegion, textPresentations.get(2).getExtent());
263+
assertEquals(linkRegion, textPresentations.get(2).getCoverage());
264+
265+
linkRegion = linkRegion(3, document);
266+
assertEquals(linkRegion, textPresentations.get(3).getExtent());
267+
assertEquals(linkRegion, textPresentations.get(3).getCoverage());
268+
269+
linkRegion = linkRegion(4, document);
270+
assertEquals(linkRegion, textPresentations.get(4).getExtent());
271+
assertEquals(linkRegion, textPresentations.get(4).getCoverage());
272+
273+
linkRegion = linkRegion(5, document);
274+
assertEquals(linkRegion, textPresentations.get(5).getExtent());
275+
assertEquals(linkRegion, textPresentations.get(5).getCoverage());
276+
277+
linkRegion = linkRegion(6, document);
278+
assertEquals(linkRegion, textPresentations.get(6).getExtent());
279+
assertEquals(linkRegion, textPresentations.get(6).getCoverage());
280+
}
281+
282+
private int getStylesCount(TextPresentation presentation) {
283+
var list = new ArrayList<>(2);
284+
presentation.getAllStyleRangeIterator().forEachRemaining(list::add);
285+
return list.size();
286+
}
287+
288+
private StyleRange textStyle(int start, int length, Color backgroundColor ) {
289+
return new StyleRange(start, length, null, backgroundColor);
290+
}
291+
292+
private StyleRange linkStyle(int start, int length, Color backgroundColor ) {
293+
var retVal = new StyleRange(start, length, null, backgroundColor);
294+
retVal.underline = true;
295+
return retVal;
296+
}
297+
298+
private Region linkRegion(int contentlinkPos, IDocument document) throws BadLocationException {
299+
DocumentLink link = CONTENT_LINKS.get(contentlinkPos);
300+
var start = link.getRange().getStart();
301+
var end = link.getRange().getEnd();
302+
int startOffset = document.getLineOffset(start.getLine()) + start.getCharacter();
303+
return new Region(startOffset, (document.getLineOffset(end.getLine()) + end.getCharacter()) - startOffset);
304+
}
305+
306+
private void foldLines(int startLine, int endLine, ProjectionViewer viewer) throws BadLocationException {
307+
var doc = viewer.getDocument();
308+
int firstLineOffset = doc.getLineOffset(startLine);
309+
int lastLineEndOffset = doc.getLineOffset(endLine) + doc.getLineLength(endLine);
310+
viewer.getProjectionAnnotationModel().addAnnotation(
311+
new ProjectionAnnotation(true),
312+
new org.eclipse.jface.text.Position(firstLineOffset, lastLineEndOffset - firstLineOffset));
313+
}
314+
315+
private void textPresentationListener(TextPresentation textPresentation) {
316+
textPresentations.add(textPresentation);
317+
}
318+
}

0 commit comments

Comments
 (0)