Skip to content

Commit 655b637

Browse files
authored
Merge pull request #3704 from 1c-syntax/copilot/optimize-incremental-sync
Optimize incremental text document synchronization
2 parents a92fe5c + cd59396 commit 655b637

File tree

2 files changed

+150
-36
lines changed

2 files changed

+150
-36
lines changed

src/main/java/com/github/_1c_syntax/bsl/languageserver/BSLTextDocumentService.java

Lines changed: 31 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -671,61 +671,56 @@ protected static String applyIncrementalChange(String content, TextDocumentConte
671671
int startOffset = getOffset(content, startLine, startChar);
672672
int endOffset = getOffset(content, endLine, endChar);
673673

674-
// Perform direct string replacement to preserve original line endings
675-
return content.substring(0, startOffset) + newText + content.substring(endOffset);
674+
// Use StringBuilder with pre-calculated capacity to avoid intermediate allocations
675+
int newLength = startOffset + newText.length() + (content.length() - endOffset);
676+
var sb = new StringBuilder(newLength);
677+
sb.append(content, 0, startOffset);
678+
sb.append(newText);
679+
sb.append(content, endOffset, content.length());
680+
return sb.toString();
676681
}
677682

678683
/**
679684
* Вычисляет абсолютную позицию символа в тексте по номеру строки и позиции в строке.
680-
* Использует indexOf для быстрого поиска переносов строк.
685+
* Использует однопроходное сканирование символов для быстрого поиска позиции.
681686
*
682687
* @param content содержимое документа
683688
* @param line номер строки (0-based)
684689
* @param character позиция символа в строке (0-based)
685690
* @return абсолютная позиция символа в тексте
686691
*/
687692
protected static int getOffset(String content, int line, int character) {
693+
var contentLength = content.length();
694+
688695
if (line == 0) {
689-
return character;
696+
return Math.min(character, contentLength);
690697
}
691698

692-
var offset = 0;
693699
var currentLine = 0;
694-
var searchFrom = 0;
695-
696-
while (currentLine < line) {
697-
int nlPos = content.indexOf('\n', searchFrom);
698-
int crPos = content.indexOf('\r', searchFrom);
699-
700-
if (nlPos == -1 && crPos == -1) {
701-
// No more line breaks found
702-
break;
703-
}
704-
705-
int nextLineBreak;
706-
if (nlPos == -1) {
707-
nextLineBreak = crPos;
708-
} else if (crPos == -1) {
709-
nextLineBreak = nlPos;
710-
} else {
711-
nextLineBreak = Math.min(nlPos, crPos);
712-
}
713-
714-
currentLine++;
715700

716-
// Handle \r\n as a single line ending
717-
if (content.charAt(nextLineBreak) == '\r'
718-
&& nextLineBreak + 1 < content.length()
719-
&& content.charAt(nextLineBreak + 1) == '\n') {
720-
offset = nextLineBreak + 2;
721-
searchFrom = nextLineBreak + 2;
722-
} else {
723-
offset = nextLineBreak + 1;
724-
searchFrom = nextLineBreak + 1;
701+
for (var i = 0; i < contentLength && currentLine < line; i++) {
702+
var c = content.charAt(i);
703+
if (c == '\n') {
704+
currentLine++;
705+
if (currentLine == line) {
706+
// Next line starts at i+1, add character offset
707+
return Math.min(i + 1 + character, contentLength);
708+
}
709+
} else if (c == '\r') {
710+
currentLine++;
711+
// Handle \r\n as a single line ending - skip the \n
712+
if (i + 1 < contentLength && content.charAt(i + 1) == '\n') {
713+
i++;
714+
}
715+
if (currentLine == line) {
716+
// Next line starts at i+1 (after \r or \r\n), add character offset
717+
return Math.min(i + 1 + character, contentLength);
718+
}
725719
}
726720
}
727721

728-
return offset + character;
722+
// Fallback: requested line beyond content, return end of content
723+
return contentLength;
729724
}
730725

731726
private void processDocumentChange(

src/test/java/com/github/_1c_syntax/bsl/languageserver/BSLTextDocumentServiceTest.java

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,4 +354,123 @@ private void doOpen() throws IOException {
354354
params.setTextDocument(getTextDocumentItem());
355355
textDocumentService.didOpen(params);
356356
}
357+
358+
// Tests for getOffset method
359+
360+
@Test
361+
void getOffset_emptyContent() {
362+
// Empty content should return 0 for any position
363+
assertThat(BSLTextDocumentService.getOffset("", 0, 0)).isZero();
364+
assertThat(BSLTextDocumentService.getOffset("", 0, 5)).isZero();
365+
assertThat(BSLTextDocumentService.getOffset("", 1, 0)).isZero();
366+
assertThat(BSLTextDocumentService.getOffset("", 5, 10)).isZero();
367+
}
368+
369+
@Test
370+
void getOffset_singleLineContent() {
371+
var content = "Hello, World!";
372+
// Line 0 with various character positions
373+
assertThat(BSLTextDocumentService.getOffset(content, 0, 0)).isZero();
374+
assertThat(BSLTextDocumentService.getOffset(content, 0, 5)).isEqualTo(5);
375+
assertThat(BSLTextDocumentService.getOffset(content, 0, 13)).isEqualTo(13);
376+
// Character position beyond line length should be capped
377+
assertThat(BSLTextDocumentService.getOffset(content, 0, 100)).isEqualTo(13);
378+
// Line beyond content should return end of content
379+
assertThat(BSLTextDocumentService.getOffset(content, 1, 0)).isEqualTo(13);
380+
}
381+
382+
@Test
383+
void getOffset_multiLineWithLF() {
384+
var content = "Line1\nLine2\nLine3";
385+
// Line 0
386+
assertThat(BSLTextDocumentService.getOffset(content, 0, 0)).isZero();
387+
assertThat(BSLTextDocumentService.getOffset(content, 0, 3)).isEqualTo(3);
388+
// Line 1 starts at position 6 (after "Line1\n")
389+
assertThat(BSLTextDocumentService.getOffset(content, 1, 0)).isEqualTo(6);
390+
assertThat(BSLTextDocumentService.getOffset(content, 1, 3)).isEqualTo(9);
391+
// Line 2 starts at position 12 (after "Line1\nLine2\n")
392+
assertThat(BSLTextDocumentService.getOffset(content, 2, 0)).isEqualTo(12);
393+
assertThat(BSLTextDocumentService.getOffset(content, 2, 5)).isEqualTo(17);
394+
// Line beyond content
395+
assertThat(BSLTextDocumentService.getOffset(content, 3, 0)).isEqualTo(17);
396+
}
397+
398+
@Test
399+
void getOffset_multiLineWithCRLF() {
400+
var content = "Line1\r\nLine2\r\nLine3";
401+
// Line 0
402+
assertThat(BSLTextDocumentService.getOffset(content, 0, 0)).isZero();
403+
assertThat(BSLTextDocumentService.getOffset(content, 0, 3)).isEqualTo(3);
404+
// Line 1 starts at position 7 (after "Line1\r\n")
405+
assertThat(BSLTextDocumentService.getOffset(content, 1, 0)).isEqualTo(7);
406+
assertThat(BSLTextDocumentService.getOffset(content, 1, 3)).isEqualTo(10);
407+
// Line 2 starts at position 14 (after "Line1\r\nLine2\r\n")
408+
assertThat(BSLTextDocumentService.getOffset(content, 2, 0)).isEqualTo(14);
409+
assertThat(BSLTextDocumentService.getOffset(content, 2, 5)).isEqualTo(19);
410+
}
411+
412+
@Test
413+
void getOffset_multiLineWithCR() {
414+
var content = "Line1\rLine2\rLine3";
415+
// Line 0
416+
assertThat(BSLTextDocumentService.getOffset(content, 0, 0)).isZero();
417+
// Line 1 starts at position 6 (after "Line1\r")
418+
assertThat(BSLTextDocumentService.getOffset(content, 1, 0)).isEqualTo(6);
419+
// Line 2 starts at position 12 (after "Line1\rLine2\r")
420+
assertThat(BSLTextDocumentService.getOffset(content, 2, 0)).isEqualTo(12);
421+
}
422+
423+
@Test
424+
void getOffset_onlyLineBreaks() {
425+
// Content with only LF line breaks
426+
var lfOnly = "\n\n\n";
427+
assertThat(BSLTextDocumentService.getOffset(lfOnly, 0, 0)).isZero();
428+
assertThat(BSLTextDocumentService.getOffset(lfOnly, 1, 0)).isEqualTo(1);
429+
assertThat(BSLTextDocumentService.getOffset(lfOnly, 2, 0)).isEqualTo(2);
430+
assertThat(BSLTextDocumentService.getOffset(lfOnly, 3, 0)).isEqualTo(3);
431+
assertThat(BSLTextDocumentService.getOffset(lfOnly, 4, 0)).isEqualTo(3);
432+
433+
// Content with only CRLF line breaks
434+
var crlfOnly = "\r\n\r\n";
435+
assertThat(BSLTextDocumentService.getOffset(crlfOnly, 0, 0)).isZero();
436+
assertThat(BSLTextDocumentService.getOffset(crlfOnly, 1, 0)).isEqualTo(2);
437+
assertThat(BSLTextDocumentService.getOffset(crlfOnly, 2, 0)).isEqualTo(4);
438+
assertThat(BSLTextDocumentService.getOffset(crlfOnly, 3, 0)).isEqualTo(4);
439+
}
440+
441+
@Test
442+
void getOffset_characterBeyondLineLength() {
443+
var content = "AB\nCD\nEF";
444+
// Line 0 has length 2, character 10 should be capped to content length
445+
assertThat(BSLTextDocumentService.getOffset(content, 0, 10)).isEqualTo(8);
446+
// Line 1 starts at 3, character 10 would be 13, capped to 8
447+
assertThat(BSLTextDocumentService.getOffset(content, 1, 10)).isEqualTo(8);
448+
// Line 2 starts at 6, character 10 would be 16, capped to 8
449+
assertThat(BSLTextDocumentService.getOffset(content, 2, 10)).isEqualTo(8);
450+
}
451+
452+
@Test
453+
void getOffset_lineBeyondDocumentLength() {
454+
var content = "Only one line";
455+
// Line 0 exists
456+
assertThat(BSLTextDocumentService.getOffset(content, 0, 0)).isZero();
457+
// Lines 1, 5, 100 don't exist, should return end of content
458+
assertThat(BSLTextDocumentService.getOffset(content, 1, 0)).isEqualTo(13);
459+
assertThat(BSLTextDocumentService.getOffset(content, 5, 0)).isEqualTo(13);
460+
assertThat(BSLTextDocumentService.getOffset(content, 100, 0)).isEqualTo(13);
461+
}
462+
463+
@Test
464+
void getOffset_mixedLineEndings() {
465+
// Mixed: LF, then CRLF, then CR
466+
var content = "A\nB\r\nC\rD";
467+
// Line 0: starts at 0
468+
assertThat(BSLTextDocumentService.getOffset(content, 0, 0)).isZero();
469+
// Line 1: starts at 2 (after "A\n")
470+
assertThat(BSLTextDocumentService.getOffset(content, 1, 0)).isEqualTo(2);
471+
// Line 2: starts at 5 (after "A\nB\r\n")
472+
assertThat(BSLTextDocumentService.getOffset(content, 2, 0)).isEqualTo(5);
473+
// Line 3: starts at 7 (after "A\nB\r\nC\r")
474+
assertThat(BSLTextDocumentService.getOffset(content, 3, 0)).isEqualTo(7);
475+
}
357476
}

0 commit comments

Comments
 (0)