diff --git a/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/utils/Strings.java b/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/utils/Strings.java index 4efff36a2..da81162e1 100644 --- a/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/utils/Strings.java +++ b/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/utils/Strings.java @@ -13,7 +13,7 @@ * * Contributors: * - Microsoft Corporation: Initial code, written in TypeScript, licensed under MIT license - * - Sebastian Thomschke - translation and adaptation to Java + * - Sebastian Thomschke (Vegard IT) - translation and adaptation to Java */ package org.eclipse.tm4e.languageconfiguration.internal.utils; @@ -32,41 +32,45 @@ public static String escapeRegExpCharacters(final String value) { } /** - * @returns first index of the string that is not whitespace or -1 if string is empty or contains only whitespaces + * @returns first index of the string that is not whitespace; or -1 if string is empty or contains only whitespaces. */ public static int firstNonWhitespaceIndex(final String text) { for (int i = 0, len = text.length(); i < len; i++) { final char ch = text.charAt(i); - if (!Character.isWhitespace(ch)) { + if (!Character.isWhitespace(ch)) return i; - } } return -1; } /** - * @return the leading whitespace of the string. If the string contains only whitespaces, returns entire string. + * @return the leading whitespace of the string; or the entire string if the string contains only whitespaces. */ public static String getLeadingWhitespace(final String searchIn) { return getLeadingWhitespace(searchIn, 0, searchIn.length()); } /** - * @return the leading whitespace of the string. If the string contains only whitespaces, returns entire string. + * @return the leading whitespace of the string; or the entire string if the string contains only whitespaces. + */ + public static String getLeadingWhitespace(final String searchIn, final int startAt) { + return getLeadingWhitespace(searchIn, startAt, searchIn.length()); + } + + /** + * @return the leading whitespace of the string; or the entire string if the string contains only whitespaces. */ public static String getLeadingWhitespace(final String searchIn, final int startAt, final int endAt) { for (int i = startAt; i < endAt; i++) { - final char ch = searchIn.charAt(endAt); - if (ch != ' ' && ch != '\t') { + final char ch = searchIn.charAt(i); + if (!Character.isWhitespace(ch)) return searchIn.substring(startAt, i); - } } return searchIn.substring(startAt, endAt); } /** - * @return the last index of the string that is not whitespace. - * If the string is empty or contains only whitespaces, returns -1. + * @return the last index of the string that is not whitespace; or -1 if string is empty or contains only whitespaces. * Defaults to starting from the end of the string. */ public static int lastNonWhitespaceIndex(final String str) { @@ -74,8 +78,7 @@ public static int lastNonWhitespaceIndex(final String str) { } /** - * @return the last index of the string that is not whitespace. - * If the string is empty or contains only whitespaces, returns -1. + * @return the last index of the string that is not whitespace; or -1 if string is empty or contains only whitespaces. */ public static int lastNonWhitespaceIndex(final String str, final int startIndex) { for (int i = startIndex; i >= 0; i--) { diff --git a/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/utils/TextEditorPrefs.java b/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/utils/TextEditorPrefs.java index fcc55ee26..9653509f0 100644 --- a/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/utils/TextEditorPrefs.java +++ b/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/utils/TextEditorPrefs.java @@ -23,14 +23,20 @@ public final class TextEditorPrefs { public static CursorConfiguration getCursorConfiguration(final @Nullable ITextEditor editor) { - final var editorPrefStore = editor == null ? null : editor.getAdapter(IPreferenceStore.class); + final var editorPrefStore = editor == null + ? null + : editor.getAdapter(IPreferenceStore.class); + final var useSpacesPrefStore = editorPrefStore != null && editorPrefStore.contains(EDITOR_SPACES_FOR_TABS) ? editorPrefStore : EditorsUI.getPreferenceStore(); + final var tabWidthPrefStore = editorPrefStore != null && editorPrefStore.contains(EDITOR_TAB_WIDTH) ? editorPrefStore : EditorsUI.getPreferenceStore(); - return new CursorConfiguration(useSpacesPrefStore.getBoolean(EDITOR_SPACES_FOR_TABS), + + return new CursorConfiguration( + useSpacesPrefStore.getBoolean(EDITOR_SPACES_FOR_TABS), tabWidthPrefStore.getInt(EDITOR_TAB_WIDTH)); } diff --git a/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/utils/TextUtils.java b/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/utils/TextUtils.java index fd7e9d585..d88fb0f04 100644 --- a/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/utils/TextUtils.java +++ b/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/utils/TextUtils.java @@ -35,28 +35,6 @@ private static boolean isLegalLineDelimiter(final IDocument doc, final String de return false; } - /** - * Returns the start of searchFor at the offset in the searchIn. - * If the searchFor is not in the searchIn at the offset, returns -1. - *

- * Example: - * - *

-	 * text="apple banana", offset=8, string="banana" -> returns 6
-	 * 
- */ - public static int startIndexOfOffsetTouchingString(final String searchIn, final int offset, final String searchFor) { - final int start = Math.max(0, offset - searchFor.length()); - int end = offset + searchFor.length(); - end = end >= searchIn.length() ? searchIn.length() : end; - try { - final int indexInSubtext = searchIn.substring(start, end).indexOf(searchFor); - return indexInSubtext == -1 ? -1 : start + indexInSubtext; - } catch (final IndexOutOfBoundsException e) { - return -1; - } - } - public static String getIndentationFromWhitespace(final String whitespace, final CursorConfiguration cursorCfg) { final var tab = "\t"; int indentOffset = 0; @@ -80,7 +58,7 @@ public static String getIndentationFromWhitespace(final String whitespace, final /** * @see + * "https://github.com/microsoft/vscode/blob/ba2cf46e20df3edf77bdd905acde3e175d985f70/src/vs/editor/common/languages/languageConfigurationRegistry.ts"> * github.com/microsoft/vscode/blob/main/src/vs/editor/common/languages/languageConfigurationRegistry.ts */ public static String getIndentationAtPosition(final IDocument doc, final int offset) { @@ -120,6 +98,12 @@ private static int findEndOfWhiteSpace(final IDocument doc, final int startAt, f return endAt; } + public static String getLeadingWhitespace(final IDocument doc, int lineIndex) throws BadLocationException { + final int lineStartOffset = doc.getLineOffset(lineIndex); + final int lineLength = doc.getLineLength(lineIndex); + return doc.get(lineStartOffset, findEndOfWhiteSpace(doc, lineStartOffset, lineStartOffset + lineLength) - lineStartOffset); + } + /** * Determines if all the characters at any offset of the specified document line are the whitespace characters. * @@ -145,6 +129,22 @@ public static boolean isBlankLine(final IDocument doc, final int lineIndex) { return true; } + public static boolean isEmptyLine(final IDocument doc, final int lineIndex) { + try { + final int lineLength = doc.getLineLength(lineIndex); + if (lineLength > 2) + return false; + if (lineLength == 0) + return true; + + final int lineOffset = doc.getLineOffset(lineIndex); + return isLegalLineDelimiter(doc, doc.get(lineOffset, lineLength)); + } catch (final BadLocationException e) { + // Ignore, forcing a positive result + return true; + } + } + private TextUtils() { } } diff --git a/org.eclipse.tm4e.languageconfiguration/src/test/java/org/eclipse/tm4e/languageconfiguration/internal/utils/MockDocument.java b/org.eclipse.tm4e.languageconfiguration/src/test/java/org/eclipse/tm4e/languageconfiguration/internal/utils/MockDocument.java new file mode 100644 index 000000000..3838ba4b5 --- /dev/null +++ b/org.eclipse.tm4e.languageconfiguration/src/test/java/org/eclipse/tm4e/languageconfiguration/internal/utils/MockDocument.java @@ -0,0 +1,301 @@ +/** + * Copyright (c) 2024 Vegard IT GmbH and others. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Sebastian Thomschke (Vegard IT) - initial implementation + */ +package org.eclipse.tm4e.languageconfiguration.internal.utils; + +import java.io.IOException; +import java.io.LineNumberReader; +import java.io.StringReader; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.DefaultLineTracker; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IDocumentListener; +import org.eclipse.jface.text.IDocumentPartitioner; +import org.eclipse.jface.text.IDocumentPartitioningListener; +import org.eclipse.jface.text.IPositionUpdater; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.ITypedRegion; +import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.Region; + +/** + * Partially implemented {@link IDocument}. Especially all methods related to {@link IDocumentListener}, {@link IDocumentPartitioner}, + * {@link Position} are not implemented. + */ +@NonNullByDefault({}) +public class MockDocument implements IDocument { + + private String contentType; + private String text; + + public MockDocument(final String contentType, final String text) { + this.contentType = contentType; + this.text = text; + } + + @Override + public void addDocumentListener(IDocumentListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public void addDocumentPartitioningListener(IDocumentPartitioningListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public void addPosition(Position position) { + throw new UnsupportedOperationException(); + } + + @Override + public void addPosition(String category, Position position) { + throw new UnsupportedOperationException(); + } + + @Override + public void addPositionCategory(String category) { + throw new UnsupportedOperationException(); + } + + @Override + public void addPositionUpdater(IPositionUpdater updater) { + throw new UnsupportedOperationException(); + } + + @Override + public void addPrenotifiedDocumentListener(IDocumentListener documentAdapter) { + throw new UnsupportedOperationException(); + } + + @Override + public int computeIndexInCategory(String category, int offset) { + throw new UnsupportedOperationException(); + } + + @Override + public int computeNumberOfLines(String text) { + throw new UnsupportedOperationException(); + } + + @Override + public ITypedRegion[] computePartitioning(int offset, int length) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsPosition(String category, int offset, int length) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsPositionCategory(String category) { + throw new UnsupportedOperationException(); + } + + @Override + public String get() { + return this.text; + } + + @Override + public String get(final int offset, final int length) throws BadLocationException { + try { + return this.text.substring(offset, offset + length); + } catch (IndexOutOfBoundsException ex) { + throw new BadLocationException(ex.getMessage()); + } + } + + @Override + public char getChar(final int offset) throws BadLocationException { + try { + return this.text.charAt(offset); + } catch (IndexOutOfBoundsException ex) { + throw new BadLocationException(ex.getMessage()); + } + } + + @Override + public String getContentType(int offset) { + return contentType; + } + + @Override + public IDocumentPartitioner getDocumentPartitioner() { + throw new UnsupportedOperationException(); + } + + @Override + public String[] getLegalContentTypes() { + throw new UnsupportedOperationException(); + } + + @Override + public String[] getLegalLineDelimiters() { + return DefaultLineTracker.DELIMITERS; + } + + @Override + public int getLength() { + return this.text.length(); + } + + @Override + public String getLineDelimiter(final int lineIndex) throws BadLocationException { + final String[] lines = text.split("\n"); + if (lines.length <= lineIndex) + throw new BadLocationException("Line " + lineIndex + " not present."); + return lines[lineIndex].endsWith("\r") ? "\r\n" : "\n"; + } + + @Override + public IRegion getLineInformation(final int lineIndex) throws BadLocationException { + return new Region(getLineOffset(lineIndex), getLineLength(lineIndex)); + } + + @Override + public IRegion getLineInformationOfOffset(final int offset) throws BadLocationException { + final var lineIndex = getLineOfOffset(offset); + return new Region(getLineOffset(lineIndex), getLineLength(lineIndex)); + } + + @Override + public int getLineLength(final int lineIndex) throws BadLocationException { + final String[] lines = text.split("\n"); + if (lines.length <= lineIndex) + throw new BadLocationException("Line " + lineIndex + " not present."); + return lineIndex == lines.length - 1 + ? lines[lineIndex].length() + : lines[lineIndex].length() + 1; + } + + @Override + public int getLineOffset(final int lineIndex) throws BadLocationException { + final String[] lines = text.split("\n"); + if (lines.length <= lineIndex) + throw new BadLocationException("Line " + lineIndex + " not present."); + + int offset = 0; + for (int i = 0; i < lineIndex; i++) { + offset += lines[i].length() + 1; + } + return offset; + } + + @Override + public int getLineOfOffset(final int offset) throws BadLocationException { + try (var lineIndexReader = new LineNumberReader(new StringReader(text))) { + lineIndexReader.skip(offset); + return lineIndexReader.getLineNumber(); + } catch (IOException ex) { + throw new BadLocationException(ex.getMessage()); + } + } + + @Override + public int getNumberOfLines() { + return (int) text.lines().count(); + } + + @Override + public int getNumberOfLines(final int offset, final int length) throws BadLocationException { + return (int) get(offset, length).lines().count(); + } + + @Override + public ITypedRegion getPartition(int offset) { + throw new UnsupportedOperationException(); + } + + @Override + public String[] getPositionCategories() { + throw new UnsupportedOperationException(); + } + + @Override + public Position[] getPositions(String category) { + throw new UnsupportedOperationException(); + } + + @Override + public IPositionUpdater[] getPositionUpdaters() { + throw new UnsupportedOperationException(); + } + + @Override + public void insertPositionUpdater(IPositionUpdater updater, int index) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeDocumentListener(IDocumentListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeDocumentPartitioningListener(IDocumentPartitioningListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public void removePosition(Position position) { + throw new UnsupportedOperationException(); + } + + @Override + public void removePosition(String category, Position position) { + throw new UnsupportedOperationException(); + } + + @Override + public void removePositionCategory(String category) { + throw new UnsupportedOperationException(); + } + + @Override + public void removePositionUpdater(IPositionUpdater updater) { + throw new UnsupportedOperationException(); + } + + @Override + public void removePrenotifiedDocumentListener(IDocumentListener documentAdapter) { + throw new UnsupportedOperationException(); + } + + @Override + public void replace(final int offset, final int length, final String text) throws BadLocationException { + try { + this.text = new StringBuilder(this.text).replace(offset, offset + length, text).toString(); + } catch (IndexOutOfBoundsException ex) { + throw new BadLocationException(ex.getMessage()); + } + } + + @Override + public int search(int startOffset, String findString, boolean forwardSearch, boolean caseSensitive, boolean wholeWord) { + throw new UnsupportedOperationException(); + } + + @Override + public void set(final String text) { + this.text = text; + } + + @Override + public void setDocumentPartitioner(IDocumentPartitioner partitioner) { + throw new UnsupportedOperationException(); + } + +} diff --git a/org.eclipse.tm4e.languageconfiguration/src/test/java/org/eclipse/tm4e/languageconfiguration/internal/utils/StringsTest.java b/org.eclipse.tm4e.languageconfiguration/src/test/java/org/eclipse/tm4e/languageconfiguration/internal/utils/StringsTest.java new file mode 100644 index 000000000..e776c7df6 --- /dev/null +++ b/org.eclipse.tm4e.languageconfiguration/src/test/java/org/eclipse/tm4e/languageconfiguration/internal/utils/StringsTest.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2024 Vegard IT GmbH and others. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Initial code from https://github.com/microsoft/vscode/ + * Initial copyright Copyright (C) Microsoft Corporation. All rights reserved. + * Initial license: MIT + * + * Contributors: + * - Microsoft Corporation: Initial code, written in TypeScript, licensed under MIT license + * - Sebastian Thomschke (Vegard IT) - translation and adaptation to Java + */ +package org.eclipse.tm4e.languageconfiguration.internal.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +/** + * @see + * github.com/microsoft/vscode/blob/main/src/vs/base/test/common/strings.test.ts + */ +class StringsTest { + + @Test + void testLastNonWhitespaceIndex() { + assertEquals(2, Strings.lastNonWhitespaceIndex("abc \t \t ")); + assertEquals(2, Strings.lastNonWhitespaceIndex("abc")); + assertEquals(2, Strings.lastNonWhitespaceIndex("abc\t")); + assertEquals(2, Strings.lastNonWhitespaceIndex("abc ")); + assertEquals(2, Strings.lastNonWhitespaceIndex("abc \t \t ")); + assertEquals(11, Strings.lastNonWhitespaceIndex("abc \t \t abc \t \t ")); + assertEquals(2, Strings.lastNonWhitespaceIndex("abc \t \t abc \t \t ", 8)); + assertEquals(-1, Strings.lastNonWhitespaceIndex(" \t \t ")); + } + + @Test + void testGetLeadingWhitespace() { + assertEquals(" ", Strings.getLeadingWhitespace(" foo")); + assertEquals("", Strings.getLeadingWhitespace(" foo", 2)); + assertEquals("", Strings.getLeadingWhitespace(" foo", 1, 1)); + assertEquals(" ", Strings.getLeadingWhitespace(" foo", 0, 1)); + assertEquals(" ", Strings.getLeadingWhitespace(" ")); + assertEquals(" ", Strings.getLeadingWhitespace(" ", 1)); + assertEquals(" ", Strings.getLeadingWhitespace(" ", 0, 1)); + assertEquals("\t", Strings.getLeadingWhitespace("\t\tfunction foo(){", 0, 1)); + assertEquals("\t\t", Strings.getLeadingWhitespace("\t\tfunction foo(){", 0, 2)); + } +} diff --git a/org.eclipse.tm4e.languageconfiguration/src/test/java/org/eclipse/tm4e/languageconfiguration/internal/utils/TextUtilsTest.java b/org.eclipse.tm4e.languageconfiguration/src/test/java/org/eclipse/tm4e/languageconfiguration/internal/utils/TextUtilsTest.java new file mode 100644 index 000000000..ca0acbcc4 --- /dev/null +++ b/org.eclipse.tm4e.languageconfiguration/src/test/java/org/eclipse/tm4e/languageconfiguration/internal/utils/TextUtilsTest.java @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2024 Vegard IT GmbH and others. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Sebastian Thomschke (Vegard IT) - initial implementation + */ +package org.eclipse.tm4e.languageconfiguration.internal.utils; + +import static org.eclipse.tm4e.languageconfiguration.internal.utils.TextUtils.*; +import static org.junit.jupiter.api.Assertions.*; + +import org.eclipse.jface.text.DocumentCommand; +import org.eclipse.tm4e.languageconfiguration.internal.model.CursorConfiguration; +import org.junit.jupiter.api.Test; + +class TextUtilsTest { + + private static MockDocument newDoc(final String txt) { + return new MockDocument("", txt); + } + + @Test + void testIsEnter() { + assertTrue(isEnter(newDoc(""), new DocumentCommand() { + { + text = "\n"; + } + })); + assertTrue(isEnter(newDoc(""), new DocumentCommand() { + { + text = "\r\n"; + } + })); + assertFalse(isEnter(newDoc(""), new DocumentCommand() { + { + text = "word"; + } + })); + } + + @Test + void testGetIndentationFromWhitespace() { + assertEquals("\t\t", getIndentationFromWhitespace("\t\t", new CursorConfiguration(false, 0))); + assertEquals("\t\t", getIndentationFromWhitespace("\t\t", new CursorConfiguration(true, 2))); + assertEquals("\t\t", getIndentationFromWhitespace("\t\t ", new CursorConfiguration(true, 2))); + assertEquals("\t\t ", getIndentationFromWhitespace("\t\t ", new CursorConfiguration(true, 2))); + assertEquals("\t\t ", getIndentationFromWhitespace("\t\t ", new CursorConfiguration(true, 2))); + assertEquals("", getIndentationFromWhitespace(" \t ", new CursorConfiguration(true, 2))); + } + + @Test + void testGetLeadingWhitespace() throws Exception { + final var indent = "\t \t "; + assertEquals(indent, getLeadingWhitespace(newDoc(indent + "hello"), 0)); + assertEquals(indent, getLeadingWhitespace(newDoc("hello\n" + indent + "world"), 1)); + assertEquals(indent, getIndentationAtPosition(newDoc(indent + "hello"), indent.length() + 2)); + } + + @Test + void testIndentationAtPosition() throws Exception { + final var indent = "\t \t "; + assertEquals("", getIndentationAtPosition(newDoc(indent + "hello"), 0)); + assertEquals("\t", getIndentationAtPosition(newDoc(indent + "hello"), 1)); + assertEquals("\t ", getIndentationAtPosition(newDoc(indent + "hello"), 2)); + } + + @Test + void testIsBlankLine() { + assertTrue(isBlankLine(newDoc(""), 0)); + assertTrue(isBlankLine(newDoc(" "), 0)); + assertTrue(isBlankLine(newDoc(" \t"), 0)); + assertTrue(isBlankLine(newDoc("\n\n"), 1)); + assertTrue(isBlankLine(newDoc("\n \n"), 1)); + assertTrue(isBlankLine(newDoc("\n \t\n"), 1)); + + assertFalse(isBlankLine(newDoc("word"), 0)); + assertFalse(isBlankLine(newDoc("\nword\n"), 1)); + } + + @Test + void testIsEmptyLine() { + assertTrue(isEmptyLine(newDoc(""), 0)); + assertFalse(isEmptyLine(newDoc(" "), 0)); + assertFalse(isEmptyLine(newDoc(" \t"), 0)); + assertTrue(isEmptyLine(newDoc("\n\n"), 1)); + assertFalse(isEmptyLine(newDoc("\n \n"), 1)); + assertFalse(isEmptyLine(newDoc("\n \t\n"), 1)); + + assertFalse(isEmptyLine(newDoc("word"), 0)); + assertFalse(isEmptyLine(newDoc("\nword\n"), 1)); + } +}