Skip to content

Commit ec21110

Browse files
committed
Improvements to NotebookUtils for conversions between line-based
positions and string offsets - Avoiding unnecessary string split and joins - Added unit tests
1 parent d331353 commit ec21110

File tree

5 files changed

+260
-104
lines changed

5 files changed

+260
-104
lines changed

nbcode/notebooks/nbproject/project.xml

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@
2929
<specification-version>2.11.0</specification-version>
3030
</run-dependency>
3131
</dependency>
32+
<dependency>
33+
<code-name-base>org.netbeans.api.annotations.common</code-name-base>
34+
<build-prerequisite/>
35+
<compile-dependency/>
36+
<run-dependency>
37+
<release-version>1</release-version>
38+
<specification-version>1.57</specification-version>
39+
</run-dependency>
40+
</dependency>
3241
<dependency>
3342
<code-name-base>org.netbeans.api.java</code-name-base>
3443
<build-prerequisite/>
@@ -153,10 +162,10 @@
153162
<recursive/>
154163
<compile-dependency/>
155164
</test-dependency>
156-
<test-dependency>
157-
<code-name-base>org.netbeans.modules.java.lsp.server</code-name-base>
158-
<compile-dependency/>
159-
</test-dependency>
165+
<test-dependency>
166+
<code-name-base>org.netbeans.modules.java.lsp.server</code-name-base>
167+
<compile-dependency/>
168+
</test-dependency>
160169
</test-type>
161170
</test-dependencies>
162171
<public-packages/>

nbcode/notebooks/src/org/netbeans/modules/nbcode/java/notebook/NotebookDocumentStateManager.java

Lines changed: 18 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
import org.eclipse.lsp4j.NotebookDocumentChangeEvent;
3131
import org.eclipse.lsp4j.NotebookDocumentChangeEventCellStructure;
3232
import org.eclipse.lsp4j.NotebookDocumentChangeEventCellTextContent;
33-
import org.eclipse.lsp4j.Position;
3433
import org.eclipse.lsp4j.Range;
3534
import org.eclipse.lsp4j.TextDocumentContentChangeEvent;
3635
import org.eclipse.lsp4j.TextDocumentIdentifier;
@@ -48,8 +47,14 @@ public class NotebookDocumentStateManager {
4847
private final NotebookDocument notebookDoc;
4948
private final Map<String, CellState> cellsMap = new ConcurrentHashMap<>();
5049
private final List<String> cellsOrder;
50+
private final CellStateCreator cellStateCreator;
5151

5252
public NotebookDocumentStateManager(NotebookDocument notebookDoc, List<TextDocumentItem> cells) {
53+
this(notebookDoc, cells, null);
54+
}
55+
56+
public NotebookDocumentStateManager(NotebookDocument notebookDoc, List<TextDocumentItem> cells, CellStateCreator cellStateCreator) {
57+
this.cellStateCreator = cellStateCreator != null ? cellStateCreator : CellState::new;
5358
this.notebookDoc = notebookDoc;
5459
this.cellsOrder = new ArrayList<>();
5560
Iterator<NotebookCell> notebookCellsIterator = notebookDoc.getCells().iterator();
@@ -193,7 +198,7 @@ private void updateNotebookCellContent(NotebookDocumentChangeEventCellTextConten
193198
cellState.setContent(updatedContent, newVersion);
194199
LOG.log(Level.FINE, "Updated content for cell: {0}, version: {1}", new Object[]{uri, newVersion});
195200
} catch (Exception e) {
196-
LOG.log(Level.WARNING, "applyContentChanges failed, requesting full content: " + uri, e);
201+
LOG.log(Level.WARNING, "applyContentChanges failed, requesting full content for cell: {0}. Error - {1}", new Object[]{uri, e});
197202
try {
198203
cellState.requestContentAndSet();
199204
} catch (Exception ex) {
@@ -222,60 +227,33 @@ private String applyContentChanges(String originalContent, List<TextDocumentCont
222227

223228
private String applyRangeChange(String content, TextDocumentContentChangeEvent change) {
224229
Range range = change.getRange();
225-
Position start = range.getStart();
226-
Position end = range.getEnd();
227-
228-
String[] lines = content.split("\n", -1);
229-
230-
if (start.getLine() < 0 || start.getLine() >= lines.length
231-
|| end.getLine() < 0 || end.getLine() >= lines.length) {
232-
throw new IllegalArgumentException("Invalid range positions");
233-
}
234-
235-
StringBuilder result = new StringBuilder();
236-
237-
for (int i = 0; i < start.getLine(); i++) {
238-
result.append(lines[i]);
239-
if (i < lines.length - 1) {
240-
result.append("\n");
241-
}
242-
}
243-
244-
String startLine = lines[start.getLine()];
245-
String beforeChange = startLine.substring(0, Math.min(start.getCharacter(), startLine.length()));
246-
result.append(beforeChange);
247-
248-
result.append(change.getText());
249-
250-
String endLine = lines[end.getLine()];
251-
String afterChange = endLine.substring(Math.min(end.getCharacter(), endLine.length()));
252-
result.append(afterChange);
253-
254-
for (int i = end.getLine() + 1; i < lines.length; i++) {
255-
result.append("\n").append(lines[i]);
256-
}
257-
258-
return result.toString();
230+
return NotebookUtils.applyChange(content, range.getStart(), range.getEnd(), change.getText());
259231
}
260232

261-
// protected methods for ease of unit testing
262-
protected void addNewCellState(NotebookCell cell, TextDocumentItem item) {
233+
234+
private void addNewCellState(NotebookCell cell, TextDocumentItem item) {
263235
if (cell == null || item == null) {
264236
LOG.log(Level.WARNING, "Attempted to add null cell or item");
265237
return;
266238
}
267239

240+
CellState cellState;
268241
try {
269-
CellState cellState = new CellState(cell, item, notebookDoc.getUri());
270-
cellsMap.put(item.getUri(), cellState);
242+
cellState = cellStateCreator.create(cell, item, notebookDoc.getUri());
271243
LOG.log(Level.FINE, "Added new cell state: {0}", item.getUri());
272244
} catch (Exception e) {
273245
LOG.log(Level.SEVERE, "Failed to create cell state for: " + item.getUri(), e);
274246
throw new RuntimeException("Failed to create cell state", e);
275247
}
248+
cellsMap.put(item.getUri(), cellState);
276249
}
277250

278251
protected Map<String, CellState> getCellsMap() {
279252
return cellsMap;
280253
}
254+
255+
// protected methods for ease of unit testing
256+
protected interface CellStateCreator {
257+
CellState create(NotebookCell cell, TextDocumentItem item, String notebookDocUri);
258+
}
281259
}

nbcode/notebooks/src/org/netbeans/modules/nbcode/java/notebook/NotebookUtils.java

Lines changed: 106 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -22,84 +22,69 @@
2222
import com.google.gson.JsonPrimitive;
2323
import java.util.ArrayList;
2424
import java.util.List;
25+
import java.util.regex.Pattern;
2526
import jdk.jshell.SourceCodeAnalysis;
2627
import org.eclipse.lsp4j.Position;
28+
import org.netbeans.api.annotations.common.NonNull;
2729

2830
/**
2931
*
3032
* @author atalati
3133
*/
3234
public class NotebookUtils {
35+
private static final Pattern LINE_ENDINGS = Pattern.compile("\\R");
3336

3437
public static String normalizeLineEndings(String text) {
35-
if (text == null) {
36-
return null;
37-
}
38-
39-
if (text.indexOf('\r') == -1) {
40-
return text;
41-
}
42-
43-
StringBuilder normalized = new StringBuilder(text.length());
44-
int len = text.length();
45-
46-
for (int i = 0; i < len; i++) {
47-
char c = text.charAt(i);
48-
if (c == '\r') {
49-
if (i + 1 < len && text.charAt(i + 1) == '\n') {
50-
i++;
51-
}
52-
normalized.append('\n');
53-
} else {
54-
normalized.append(c);
55-
}
56-
}
57-
58-
return normalized.toString();
38+
return text == null ? null : LINE_ENDINGS.matcher(text).replaceAll("\n");
5939
}
6040

6141
public static int getOffset(String content, Position position) {
6242
if (content == null || position == null) {
6343
return 0;
6444
}
6545

66-
String[] lines = content.split("\n", -1);
67-
int offset = 0;
6846
int targetLine = position.getLine();
69-
int targetChar = position.getCharacter();
70-
7147
if (targetLine < 0) {
7248
return 0;
7349
}
74-
if (targetLine >= lines.length) {
75-
return content.length();
76-
}
50+
int targetChar = Math.max(0, position.getCharacter());
7751

78-
for (int i = 0; i < targetLine; i++) {
79-
offset += lines[i].length() + 1;
80-
}
52+
int lineStartIndex = -1;
53+
int lineEndIndex = -1;
54+
int line = -1;
8155

82-
String currentLine = lines[targetLine];
83-
int charPosition = Math.min(Math.max(targetChar, 0), currentLine.length());
84-
offset += charPosition;
56+
// find the start line in content
57+
do {
58+
lineStartIndex = lineEndIndex + 1;
59+
lineEndIndex = content.indexOf('\n', lineStartIndex);
60+
line++;
61+
} while (line < targetLine && lineEndIndex >= 0);
8562

86-
return Math.min(offset, content.length());
63+
return line < targetLine ? content.length() : Math.min(lineStartIndex + targetChar, lineEndIndex < 0 ? content.length() : lineEndIndex);
8764
}
8865

89-
public static Position getPosition(String content, int offset) {
90-
if (content == null || offset <= 0) {
66+
public static Position getPosition(String text, int offset) {
67+
if (text == null || offset < 0) {
9168
return new Position(0, 0);
9269
}
9370

94-
int clampedOffset = Math.min(offset, content.length());
95-
96-
String textUpToOffset = content.substring(0, clampedOffset);
97-
String[] lines = textUpToOffset.split("\n", -1);
98-
99-
int line = lines.length - 1;
100-
int character = lines[line].length();
101-
102-
return new Position(line, character);
71+
offset = Math.min(offset, text.length());
72+
int lineStartIndex = -1;
73+
int lineEndIndex = -1;
74+
int line = -1;
75+
76+
// count line endings in content upto offset
77+
do {
78+
lineStartIndex = lineEndIndex + 1;
79+
lineEndIndex = text.indexOf('\n', lineStartIndex);
80+
line++;
81+
} while (lineEndIndex >= 0 && offset > lineEndIndex);
82+
83+
if (offset == lineEndIndex) {
84+
return new Position(line + 1, 0);
85+
} else {
86+
return new Position(line, offset - lineStartIndex);
87+
}
10388
}
10489

10590
public static boolean checkEmptyString(String input) {
@@ -158,4 +143,76 @@ public static List<String> getCodeSnippets(SourceCodeAnalysis analysis, String c
158143

159144
return codeSnippets;
160145
}
146+
147+
/**
148+
* Applies the supplied change, that is encoded as a diff
149+
* i.e. `{range-start, range-end, text-replacement}`, to the supplied text.
150+
*
151+
* This diff format can encode additions, deletions and modifications at a
152+
* single range in the text.
153+
*
154+
* The supplied text is expected to contain normalized line endings, and, the
155+
* new text adheres to the line ending normalization.
156+
*
157+
* @param text existing text
158+
* @param start start of the range of replaced text
159+
* @param end end of the range of replaced text
160+
* @param replacement text to be added at the supplied position in text
161+
* @throws IllegalArgumentException - when the supplied diff range is invalid
162+
*/
163+
public static String applyChange(@NonNull String text, @NonNull Position start, @NonNull Position end, @NonNull String replacement) throws IllegalArgumentException {
164+
int startLine = start.getLine();
165+
int startLineOffset = start.getCharacter();
166+
int endLine = end.getLine();
167+
int endLineOffset = end.getCharacter();
168+
169+
if (startLine < 0 || endLine < startLine || (endLine == startLine && endLineOffset < startLineOffset)) {
170+
throw new IllegalArgumentException("Invalid range positions");
171+
}
172+
173+
if (replacement.length() == 0 && startLine == endLine && startLineOffset == endLineOffset) {
174+
return text; // Nothing to be done; no addition nor deletion
175+
}
176+
177+
final int textLength = text.length();
178+
179+
int lineStartIndex = -1;
180+
int lineEndIndex = -1;
181+
int line = -1;
182+
183+
// find the start line in content
184+
do {
185+
lineStartIndex = lineEndIndex + 1;
186+
lineEndIndex = text.indexOf('\n', lineStartIndex);
187+
line++;
188+
} while (line < startLine && lineEndIndex >= 0);
189+
190+
if (line < startLine) {
191+
throw new IllegalArgumentException("Invalid range start out of bounds");
192+
}
193+
194+
StringBuilder result = new StringBuilder(textLength + replacement.length());
195+
196+
// append content before the change
197+
result.append(text, 0, Math.min(lineStartIndex + startLineOffset, lineEndIndex < 0 ? textLength : lineEndIndex));
198+
// append added text, with line ending normalization
199+
result.append(LINE_ENDINGS.matcher(replacement).replaceAll("\n"));
200+
201+
// find the end line in content
202+
while (line < endLine && lineEndIndex >= 0) {
203+
lineStartIndex = lineEndIndex + 1;
204+
lineEndIndex = text.indexOf('\n', lineStartIndex);
205+
line++;
206+
}
207+
208+
if (line < endLine) {
209+
throw new IllegalArgumentException("Invalid range end out of bounds");
210+
}
211+
212+
if (lineStartIndex >= 0) {
213+
// append content after the change
214+
result.append(text, Math.min(lineStartIndex + endLineOffset, lineEndIndex < 0 ? textLength : lineEndIndex), textLength);
215+
}
216+
return result.toString();
217+
}
161218
}

nbcode/notebooks/test/unit/src/org/netbeans/modules/nbcode/java/notebook/NotebookDocumentStateManagerTest.java

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -201,18 +201,8 @@ public void testSyncState_UpdateCellContent_FailureAndFallback() throws Interrup
201201
}
202202

203203
private static class TestableNotebookDocumentStateManager extends NotebookDocumentStateManager {
204-
205204
public TestableNotebookDocumentStateManager(NotebookDocument notebookDoc, List<TextDocumentItem> cells) {
206-
super(notebookDoc, cells);
207-
}
208-
209-
@Override
210-
protected void addNewCellState(NotebookCell cell, TextDocumentItem item) {
211-
if (cell == null || item == null) {
212-
return;
213-
}
214-
CellState cellState = new TestableCellState(cell, item, getNotebookDocument().getUri());
215-
getCellsMap().put(item.getUri(), cellState);
205+
super(notebookDoc, cells, TestableCellState::new);
216206
}
217207
}
218208

0 commit comments

Comments
 (0)