From 6dcd4331f92e304dd397a491752e59152e46f94f Mon Sep 17 00:00:00 2001 From: snelusha Date: Tue, 5 Aug 2025 14:04:22 +0530 Subject: [PATCH 01/11] Add tools API and errors package --- common/errors/errors.go | 47 +++++++ tools/diagnostics/default-diagnostic.go | 90 +++++++++++++ tools/diagnostics/diagnostic-code.go | 27 ++++ tools/diagnostics/diagnostic-factory.go | 44 ++++++ tools/diagnostics/diagnostic-info.go | 79 +++++++++++ tools/diagnostics/diagnostic-property-kind.go | 48 +++++++ tools/diagnostics/diagnostic-property.go | 25 ++++ .../diagnostic-related-information.go | 46 +++++++ tools/diagnostics/diagnostic-severity.go | 47 +++++++ tools/diagnostics/diagnostic.go | 50 +++++++ tools/diagnostics/location.go | 28 ++++ tools/text/char-reader.go | 103 +++++++++++++++ tools/text/line-map.go | 124 +++++++++++++++++ tools/text/line-position.go | 67 ++++++++++ tools/text/line-range.go | 75 +++++++++++ tools/text/string-text-document.go | 125 ++++++++++++++++++ tools/text/text-document-change.go | 63 +++++++++ tools/text/text-document.go | 47 +++++++ tools/text/text-documents.go | 24 ++++ tools/text/text-edit.go | 52 ++++++++ tools/text/text-line.go | 76 +++++++++++ tools/text/text-range.go | 87 ++++++++++++ 22 files changed, 1374 insertions(+) create mode 100644 common/errors/errors.go create mode 100644 tools/diagnostics/default-diagnostic.go create mode 100644 tools/diagnostics/diagnostic-code.go create mode 100644 tools/diagnostics/diagnostic-factory.go create mode 100644 tools/diagnostics/diagnostic-info.go create mode 100644 tools/diagnostics/diagnostic-property-kind.go create mode 100644 tools/diagnostics/diagnostic-property.go create mode 100644 tools/diagnostics/diagnostic-related-information.go create mode 100644 tools/diagnostics/diagnostic-severity.go create mode 100644 tools/diagnostics/diagnostic.go create mode 100644 tools/diagnostics/location.go create mode 100644 tools/text/char-reader.go create mode 100644 tools/text/line-map.go create mode 100644 tools/text/line-position.go create mode 100644 tools/text/line-range.go create mode 100644 tools/text/string-text-document.go create mode 100644 tools/text/text-document-change.go create mode 100644 tools/text/text-document.go create mode 100644 tools/text/text-documents.go create mode 100644 tools/text/text-edit.go create mode 100644 tools/text/text-line.go create mode 100644 tools/text/text-range.go diff --git a/common/errors/errors.go b/common/errors/errors.go new file mode 100644 index 000000000..59ab80aa3 --- /dev/null +++ b/common/errors/errors.go @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package errors + +type IndexOutOfBoundsError struct { + message string +} + +func (e IndexOutOfBoundsError) Error() string { + return e.message +} + +func NewIndexOutOfBoundsError(message string) *IndexOutOfBoundsError { + return &IndexOutOfBoundsError{ + message: message, + } +} + +type IllegalArgumentError struct { + message string +} + +func (e IllegalArgumentError) Error() string { + return e.message +} + +func NewIllegalArgumentError(message string) *IllegalArgumentError { + return &IllegalArgumentError{ + message: message, + } +} diff --git a/tools/diagnostics/default-diagnostic.go b/tools/diagnostics/default-diagnostic.go new file mode 100644 index 000000000..011c1d660 --- /dev/null +++ b/tools/diagnostics/default-diagnostic.go @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package diagnostics + +import ( + "ballerina-lang-go/tools/text" + "fmt" +) + +// DefaultDiagnostic is an internal implementation of the Diagnostic interface that is used by the DiagnosticFactory +// to create diagnostics. +type DefaultDiagnostic interface { + Diagnostic +} + +type defaultDiagnosticImpl struct { + diagnosticBase + diagnosticInfo DiagnosticInfo + location Location + properties []DiagnosticProperty[interface{}] + message string +} + +func NewDefaultDiagnostic(diagnosticInfo DiagnosticInfo, location Location, properties []DiagnosticProperty[any], args ...interface{}) DefaultDiagnostic { + message := formatMessage(diagnosticInfo.MessageFormat(), args...) + return &defaultDiagnosticImpl{ + diagnosticBase: diagnosticBase{}, + diagnosticInfo: diagnosticInfo, + location: location, + properties: properties, + message: message, + } +} + +func (dd *defaultDiagnosticImpl) Location() Location { + return dd.location +} + +func (dd *defaultDiagnosticImpl) DiagnosticInfo() DiagnosticInfo { + return dd.diagnosticInfo +} + +func (dd *defaultDiagnosticImpl) Message() string { + return dd.message +} + +func (dd *defaultDiagnosticImpl) Properties() []DiagnosticProperty[any] { + return dd.properties +} + +func (dd *defaultDiagnosticImpl) String() string { + lineRange := dd.location.LineRange() + filePath := lineRange.FileName() + + startLine := lineRange.StartLine() + endLine := lineRange.EndLine() + + oneBasedStartLine := text.LinePositionFromLineAndOffset(startLine.Line()+1, startLine.Offset()+1) + oneBasedEndLine := text.LinePositionFromLineAndOffset(endLine.Line()+1, endLine.Offset()+1) + oneBasedLineRange := text.LineRangeFromLinePositions(filePath, oneBasedStartLine, oneBasedEndLine) + + return fmt.Sprintf("%s [%s:%s] %s", + dd.diagnosticInfo.Severity().String(), + filePath, + oneBasedLineRange.String(), + dd.Message()) +} + +func formatMessage(format string, args ...interface{}) string { + if len(args) == 0 { + return format + } + return fmt.Sprintf(format, args...) +} diff --git a/tools/diagnostics/diagnostic-code.go b/tools/diagnostics/diagnostic-code.go new file mode 100644 index 000000000..df621d3de --- /dev/null +++ b/tools/diagnostics/diagnostic-code.go @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package diagnostics + +// DiagnosticCode represents a diagnostic code. +// Diagnostic code uniquely identifies a diagnostic. +type DiagnosticCode interface { + Severity() DiagnosticSeverity + DiagnosticId() string + MessageKey() string +} diff --git a/tools/diagnostics/diagnostic-factory.go b/tools/diagnostics/diagnostic-factory.go new file mode 100644 index 000000000..612aa8f65 --- /dev/null +++ b/tools/diagnostics/diagnostic-factory.go @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package diagnostics + +// CreateDiagnostic creates a Diagnostic instance from the given details. +// +// Parameters: +// - diagnosticInfo: static diagnostic information +// - location: the location of the diagnostic +// - args: arguments to diagnostic message format +// +// Returns a Diagnostic instance. +func CreateDiagnostic(diagnosticInfo DiagnosticInfo, location Location, args ...interface{}) Diagnostic { + return NewDefaultDiagnostic(diagnosticInfo, location, []DiagnosticProperty[any]{}, args...) +} + +// CreateDiagnosticWithProperties creates a Diagnostic instance from the given details. +// +// Parameters: +// - diagnosticInfo: static diagnostic information +// - location: the location of the diagnostic +// - properties: properties associated with the diagnostic +// - args: arguments to diagnostic message format +// +// Returns a Diagnostic instance. +func CreateDiagnosticWithProperties(diagnosticInfo DiagnosticInfo, location Location, properties []DiagnosticProperty[any], args ...interface{}) Diagnostic { + return NewDefaultDiagnostic(diagnosticInfo, location, properties, args...) +} diff --git a/tools/diagnostics/diagnostic-info.go b/tools/diagnostics/diagnostic-info.go new file mode 100644 index 000000000..4afc89452 --- /dev/null +++ b/tools/diagnostics/diagnostic-info.go @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package diagnostics + +// DiagnosticInfo represents an abstract shape of a Diagnostic that is independent of +// the location and message arguments. +type DiagnosticInfo interface { + Code() string + MessageFormat() string + Severity() DiagnosticSeverity + DiagnosticInfoLookupKey() DiagnosticInfoLookupKey +} + +// DiagnosticInfoLookupKey represents the comparable fields of DiagnosticInfo for equality/hashing. +type DiagnosticInfoLookupKey struct { + Code *string // pointer to handle nil values + MessageFormat string + Severity DiagnosticSeverity +} + +type diagnosticInfoImpl struct { + code *string // pointer to handle nil values + messageFormat string + severity DiagnosticSeverity +} + +// NewDiagnosticInfo constructs an abstract shape of a Diagnostic. +// +// Parameters: +// - code: a code that can be used to uniquely identify a diagnostic category +// - messageFormat: a pattern that can be formatted with message formatting utilities +// - severity: the severity of the diagnostic +func NewDiagnosticInfo(code *string, messageFormat string, severity DiagnosticSeverity) DiagnosticInfo { + return &diagnosticInfoImpl{ + code: code, + messageFormat: messageFormat, + severity: severity, + } +} + +func (di diagnosticInfoImpl) Code() string { + if di.code == nil { + return "" + } + return *di.code +} + +func (di diagnosticInfoImpl) MessageFormat() string { + return di.messageFormat +} + +func (di diagnosticInfoImpl) Severity() DiagnosticSeverity { + return di.severity +} + +// DiagnosticInfoLookupKey returns the lookup key for equality comparisons. +func (di diagnosticInfoImpl) DiagnosticInfoLookupKey() DiagnosticInfoLookupKey { + return DiagnosticInfoLookupKey{ + Code: di.code, + MessageFormat: di.messageFormat, + Severity: di.severity, + } +} diff --git a/tools/diagnostics/diagnostic-property-kind.go b/tools/diagnostics/diagnostic-property-kind.go new file mode 100644 index 000000000..e66a7a6e8 --- /dev/null +++ b/tools/diagnostics/diagnostic-property-kind.go @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package diagnostics + +// DiagnosticPropertyKind represents the kind of the diagnostic property. +type DiagnosticPropertyKind uint8 + +const ( + Symbolic DiagnosticPropertyKind = iota + String + Numeric + Collection + Other +) + +// String returns the string representation of the diagnostic property kind. +func (dpk DiagnosticPropertyKind) String() string { + switch dpk { + case Symbolic: + return "SYMBOLIC" + case String: + return "STRING" + case Numeric: + return "NUMERIC" + case Collection: + return "COLLECTION" + case Other: + return "OTHER" + default: + return "UNKNOWN" + } +} diff --git a/tools/diagnostics/diagnostic-property.go b/tools/diagnostics/diagnostic-property.go new file mode 100644 index 000000000..c3e573d47 --- /dev/null +++ b/tools/diagnostics/diagnostic-property.go @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package diagnostics + +// DiagnosticProperty represents properties passed when diagnostic logging. +type DiagnosticProperty[T interface{}] interface { + Kind() DiagnosticPropertyKind + Value() T +} diff --git a/tools/diagnostics/diagnostic-related-information.go b/tools/diagnostics/diagnostic-related-information.go new file mode 100644 index 000000000..89c6bc046 --- /dev/null +++ b/tools/diagnostics/diagnostic-related-information.go @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package diagnostics + +// DiagnosticRelatedInformation represents a message and location related to a particular Diagnostic. +// A sample usage would be to record all symbol information related to duplicate symbol error. +type DiagnosticRelatedInformation interface { + Location() Location + Message() string +} + +type diagnosticRelatedInformationImpl struct { + location Location + message string +} + +func NewDiagnosticRelatedInformation(location Location, message string) DiagnosticRelatedInformation { + return &diagnosticRelatedInformationImpl{ + location: location, + message: message, + } +} + +func (dri diagnosticRelatedInformationImpl) Location() Location { + return dri.location +} + +func (dri diagnosticRelatedInformationImpl) Message() string { + return dri.message +} diff --git a/tools/diagnostics/diagnostic-severity.go b/tools/diagnostics/diagnostic-severity.go new file mode 100644 index 000000000..047047a8f --- /dev/null +++ b/tools/diagnostics/diagnostic-severity.go @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package diagnostics + +// DiagnosticSeverity represents a severity of a Diagnostic. +type DiagnosticSeverity uint8 + +const ( + Internal DiagnosticSeverity = iota + Hint + Info + Warning + Error +) + +func (ds DiagnosticSeverity) String() string { + switch ds { + case Internal: + return "INTERNAL" + case Hint: + return "HINT" + case Info: + return "INFO" + case Warning: + return "WARNING" + case Error: + return "ERROR" + default: + return "UNKNOWN" + } +} diff --git a/tools/diagnostics/diagnostic.go b/tools/diagnostics/diagnostic.go new file mode 100644 index 000000000..f9cfb18cc --- /dev/null +++ b/tools/diagnostics/diagnostic.go @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package diagnostics + +import "fmt" + +// Diagnostic represents a diagnostic message (error, warning, etc.) with location information. +// A diagnostic represents a compiler error, a warning or a message at a specific location in the source file. +type Diagnostic interface { + Location() Location + DiagnosticInfo() DiagnosticInfo + Message() string + Properties() []DiagnosticProperty[any] + String() string +} + +type diagnosticBase struct{} + +// String returns a string representation of the diagnostic. +// This is the default implementation from the abstract Diagnostic class. +func (db diagnosticBase) String(d Diagnostic) string { + var location string + if d.Location().LineRange().FileName() == "" { + location = "" + } else { + location = fmt.Sprintf(" [%s:%s]", + d.Location().LineRange().FileName(), + d.Location().LineRange().String()) + } + return fmt.Sprintf("%s%s %s", + d.DiagnosticInfo().Severity().String(), + location, + d.Message()) +} diff --git a/tools/diagnostics/location.go b/tools/diagnostics/location.go new file mode 100644 index 000000000..075c3a0ea --- /dev/null +++ b/tools/diagnostics/location.go @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package diagnostics + +import "ballerina-lang-go/tools/text" + +// Location represents the location in TextDocument. +// It is a combination of source file path, start and end line numbers, and start and end column numbers. +type Location interface { + LineRange() text.LineRange + TextRange() text.TextRange +} diff --git a/tools/text/char-reader.go b/tools/text/char-reader.go new file mode 100644 index 000000000..ef02580fe --- /dev/null +++ b/tools/text/char-reader.go @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package text + +import "unicode" + +// CharReader is a character reader utility used by the Ballerina lexer. +type CharReader interface { + Reset(offset int) + Peek() rune + PeekN(k int) rune + Advance() + AdvanceN(k int) + Mark() + GetMarkedChars() string + IsEOF() bool +} + +// charReaderImpl is the concrete implementation of CharReader. +type charReaderImpl struct { + charBuffer []rune + offset int + charBufferLength int + lexemeStartPos int +} + +// newCharReader constructs a CharReader with the given character buffer. +func newCharReader(charBuffer []rune) CharReader { + return &charReaderImpl{ + charBuffer: charBuffer, + offset: 0, + charBufferLength: len(charBuffer), + lexemeStartPos: 0, + } +} + +func CharReaderFromTextDocument(textDocument TextDocument) CharReader { + return newCharReader(textDocument.ToCharArray()) +} + +func CharReaderFromText(text string) CharReader { + charBuffer := []rune(text) + return newCharReader(charBuffer) +} + +func (cr *charReaderImpl) Reset(offset int) { + cr.offset = offset +} + +func (cr charReaderImpl) Peek() rune { + if cr.offset < cr.charBufferLength { + return cr.charBuffer[cr.offset] + } else { + // TODO Revisit this branch + return unicode.MaxRune + } +} + +func (cr charReaderImpl) PeekN(k int) rune { + n := cr.offset + k + if n < cr.charBufferLength { + return cr.charBuffer[n] + } else { + // TODO Revisit this branch + return unicode.MaxRune + } +} + +func (cr *charReaderImpl) Advance() { + cr.offset++ +} + +func (cr *charReaderImpl) AdvanceN(k int) { + cr.offset += k +} + +func (cr *charReaderImpl) Mark() { + cr.lexemeStartPos = cr.offset +} + +func (cr charReaderImpl) GetMarkedChars() string { + return string(cr.charBuffer[cr.lexemeStartPos:cr.offset]) +} + +func (cr charReaderImpl) IsEOF() bool { + return cr.offset >= cr.charBufferLength +} diff --git a/tools/text/line-map.go b/tools/text/line-map.go new file mode 100644 index 000000000..528c29ab1 --- /dev/null +++ b/tools/text/line-map.go @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package text + +import ( + "ballerina-lang-go/common/errors" + "fmt" +) + +// LineMap represents a collection of text lines in the TextDocument. +type LineMap interface { + TextLine(line int) (TextLine, error) + LinePositionFromPosition(position int) (LinePosition, error) + TextPositionFromLinePosition(linePosition LinePosition) (int, error) + TextLines() []string +} + +type lineMapImpl struct { + textLines []TextLine + length int +} + +func NewLineMap(textLines []TextLine) LineMap { + return &lineMapImpl{ + textLines: textLines, + length: len(textLines), + } +} + +func (lm lineMapImpl) TextLine(line int) (TextLine, error) { + if err := lm.lineRangeCheck(line); err != nil { + return nil, err + } + return lm.textLines[line], nil +} + +func (lm lineMapImpl) LinePositionFromPosition(position int) (LinePosition, error) { + if err := lm.positionRangeCheck(position); err != nil { + return nil, err + } + textLine := lm.findLineFrom(position) + return LinePositionFromLineAndOffset(textLine.LineNo(), position-textLine.StartOffset()), nil +} + +func (lm lineMapImpl) TextPositionFromLinePosition(linePosition LinePosition) (int, error) { + if err := lm.lineRangeCheck(linePosition.Line()); err != nil { + return 0, err + } + textLine := lm.textLines[linePosition.Line()] + if textLine.Length() < linePosition.Offset() { + return 0, errors.NewIllegalArgumentError(fmt.Sprintf("Cannot find a line with the character offset '%d'", linePosition.Offset())) + } + return textLine.StartOffset() + linePosition.Offset(), nil +} + +func (lm lineMapImpl) TextLines() []string { + lines := make([]string, len(lm.textLines)) + for i, textLine := range lm.textLines { + lines[i] = textLine.Text() + } + return lines +} + +func (lm lineMapImpl) positionRangeCheck(position int) error { + if position < 0 || position > lm.textLines[lm.length-1].EndOffset() { + return errors.NewIndexOutOfBoundsError(fmt.Sprintf("Index: '%d', Size: '%d'", position, lm.textLines[lm.length-1].EndOffset())) + } + return nil +} + +func (lm lineMapImpl) lineRangeCheck(lineNo int) error { + if lineNo < 0 || lineNo > lm.length { + return errors.NewIndexOutOfBoundsError(fmt.Sprintf("Line number: '%d', Size: '%d'", lineNo, lm.length)) + } + return nil +} + +// findLineFrom returns the TextLine to which the given position belongs. +// Performs a binary search to find the matching text line. +func (lm lineMapImpl) findLineFrom(position int) TextLine { + // Check boundary conditions + if position == 0 { + return lm.textLines[0] + } else if position == lm.textLines[lm.length-1].EndOffset() { + return lm.textLines[lm.length-1] + } + + var foundTextLine TextLine + left := 0 + right := lm.length - 1 + + for left <= right { + // Using bit shift to handle overflow when sum of left and right is greater than max int + middle := (left + right) >> 1 + startOffset := lm.textLines[middle].StartOffset() + endOffset := lm.textLines[middle].EndOffsetWithNewLines() + + if startOffset <= position && position < endOffset { + foundTextLine = lm.textLines[middle] + break + } else if endOffset <= position { + left = middle + 1 + } else { + right = middle - 1 + } + } + return foundTextLine +} diff --git a/tools/text/line-position.go b/tools/text/line-position.go new file mode 100644 index 000000000..aeafaaf06 --- /dev/null +++ b/tools/text/line-position.go @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package text + +import "fmt" + +// LinePosition represents a line number and a character offset from the start of the line. +type LinePosition interface { + Line() int + Offset() int + String() string + LinePositionLookupKey() LinePositionLookupKey +} + +// LinePositionLookupKey represents the comparable fields of LinePosition for equality/hashing. +type LinePositionLookupKey struct { + Line int + Offset int +} + +type linePositionImpl struct { + line int + offset int +} + +func LinePositionFromLineAndOffset(line, offset int) LinePosition { + return &linePositionImpl{ + line: line, + offset: offset, + } +} + +func (lp linePositionImpl) Line() int { + return lp.line +} + +func (lp linePositionImpl) Offset() int { + return lp.offset +} + +func (lp linePositionImpl) String() string { + return fmt.Sprintf("%d:%d", lp.line, lp.offset) +} + +// LinePositionLookupKey returns the lookup key for equality comparisons. +func (lp linePositionImpl) LinePositionLookupKey() LinePositionLookupKey { + return LinePositionLookupKey{ + Line: lp.line, + Offset: lp.offset, + } +} diff --git a/tools/text/line-range.go b/tools/text/line-range.go new file mode 100644 index 000000000..6aac6c41e --- /dev/null +++ b/tools/text/line-range.go @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package text + +import "fmt" + +// LineRange represents a pair of LinePosition. +type LineRange interface { + FileName() string + StartLine() LinePosition + EndLine() LinePosition + String() string + LineRangeLookupKey() LineRangeLookupKey +} + +// LineRangeLookupKey represents the comparable fields of LineRange for equality/hashing. +type LineRangeLookupKey struct { + StartLine LinePositionLookupKey + EndLine LinePositionLookupKey +} + +type lineRangeImpl struct { + fileName string + startLine LinePosition + endLine LinePosition +} + +func LineRangeFromLinePositions(fileName string, startLine, endLine LinePosition) LineRange { + return &lineRangeImpl{ + fileName: fileName, + startLine: startLine, + endLine: endLine, + } +} + +// FileName returns the file name. +func (lr lineRangeImpl) FileName() string { + return lr.fileName +} + +func (lr lineRangeImpl) StartLine() LinePosition { + return lr.startLine +} + +func (lr lineRangeImpl) EndLine() LinePosition { + return lr.endLine +} + +func (lr lineRangeImpl) String() string { + return fmt.Sprintf("(%s,%s)", lr.startLine.String(), lr.endLine.String()) +} + +// LineRangeLookupKey returns the lookup key for equality comparisons. +func (lr lineRangeImpl) LineRangeLookupKey() LineRangeLookupKey { + return LineRangeLookupKey{ + StartLine: lr.startLine.LinePositionLookupKey(), + EndLine: lr.endLine.LinePositionLookupKey(), + } +} diff --git a/tools/text/string-text-document.go b/tools/text/string-text-document.go new file mode 100644 index 000000000..574c08a82 --- /dev/null +++ b/tools/text/string-text-document.go @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package text + +import "strings" + +// StringTextDocument represents a TextDocument created with a string. +type StringTextDocument interface { + TextDocument + String() string +} + +type stringTextDocumentImpl struct { + textDocumentBase + text string + textLineMap LineMap +} + +func NewStringTextDocument(text string) StringTextDocument { + return &stringTextDocumentImpl{ + textDocumentBase: textDocumentBase{}, + text: text, + } +} + +func (std *stringTextDocumentImpl) Apply(textDocumentChange TextDocumentChange) TextDocument { + startOffset := 0 + var sb strings.Builder + textEditCount := textDocumentChange.GetTextEditCount() + for i := range textEditCount { + textEdit := textDocumentChange.GetTextEdit(i) + textRange := textEdit.Range() + sb.WriteString(std.text[startOffset:textRange.StartOffset()]) + sb.WriteString(textEdit.Text()) + startOffset = textRange.EndOffset() + } + sb.WriteString(std.text[startOffset:]) + return NewStringTextDocument(sb.String()) +} + +func (std *stringTextDocumentImpl) PopulateTextLineMap() LineMap { + if std.textLineMap != nil { + return std.textLineMap + } + std.textLineMap = NewLineMap(std.calculateTextLines()) + return std.textLineMap +} + +func (std *stringTextDocumentImpl) ToCharArray() []rune { + return []rune(std.text) +} + +func (std *stringTextDocumentImpl) String() string { + return std.text +} + +func (std *stringTextDocumentImpl) TextLines() []string { + if std.textDocumentBase.lineMap != nil { + return std.textDocumentBase.lineMap.TextLines() + } + std.textDocumentBase.lineMap = std.PopulateTextLineMap() + return std.textDocumentBase.lineMap.TextLines() +} + +func (std *stringTextDocumentImpl) Lines() LineMap { + if std.textDocumentBase.lineMap != nil { + return std.textDocumentBase.lineMap + } + std.textDocumentBase.lineMap = std.PopulateTextLineMap() + return std.textDocumentBase.lineMap +} + +func (std *stringTextDocumentImpl) calculateTextLines() []TextLine { + startOffset := 0 + var textLines []TextLine + var lineBuilder strings.Builder + index := 0 + line := 0 + textLength := len(std.text) + var lengthOfNewLineChars int + + for index < textLength { + c := rune(std.text[index]) + if c == '\r' || c == '\n' { + nextCharIndex := index + 1 + if c == '\r' && textLength != nextCharIndex && rune(std.text[nextCharIndex]) == '\n' { + lengthOfNewLineChars = 2 + } else { + lengthOfNewLineChars = 1 + } + + strLine := lineBuilder.String() + endOffset := startOffset + len(strLine) + textLines = append(textLines, NewTextLine(line, strLine, startOffset, endOffset, lengthOfNewLineChars)) + line++ + startOffset = endOffset + lengthOfNewLineChars + lineBuilder.Reset() + index += lengthOfNewLineChars + } else { + lineBuilder.WriteRune(c) + index++ + } + } + + strLine := lineBuilder.String() + textLines = append(textLines, NewTextLine(line, strLine, startOffset, startOffset+len(strLine), 0)) + + return textLines +} diff --git a/tools/text/text-document-change.go b/tools/text/text-document-change.go new file mode 100644 index 000000000..60e002bff --- /dev/null +++ b/tools/text/text-document-change.go @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package text + +import "strings" + +// TextDocumentChange represents textual changes on a single TextDocument. +type TextDocumentChange interface { + GetTextEditCount() int + GetTextEdit(index int) TextEdit + String() string +} + +type textDocumentChangeImpl struct { + textEdits []TextEdit +} + +func TextDocumentChangeFromTextEdits(textEdits []TextEdit) TextDocumentChange { + // Create a copy of the slice to ensure immutability + editsCopy := make([]TextEdit, len(textEdits)) + copy(editsCopy, textEdits) + + return &textDocumentChangeImpl{ + textEdits: editsCopy, + } +} + +func (tdc textDocumentChangeImpl) GetTextEditCount() int { + return len(tdc.textEdits) +} + +func (tdc textDocumentChangeImpl) GetTextEdit(index int) TextEdit { + return tdc.textEdits[index] +} + +func (tdc textDocumentChangeImpl) String() string { + if len(tdc.textEdits) == 0 { + return "" + } + + var editStrings []string + for _, textEdit := range tdc.textEdits { + editStrings = append(editStrings, textEdit.String()) + } + + return strings.Join(editStrings, ",") +} diff --git a/tools/text/text-document.go b/tools/text/text-document.go new file mode 100644 index 000000000..240fb8915 --- /dev/null +++ b/tools/text/text-document.go @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package text + +// TextDocument is an abstract representation of a Ballerina source file (.bal). +type TextDocument interface { + Apply(textDocumentChange TextDocumentChange) TextDocument + ToCharArray() []rune + Line(line int) (TextLine, error) + LinePositionFromTextPosition(textPosition int) (LinePosition, error) + TextPositionFromLinePosition(linePosition LinePosition) (int, error) + TextLines() []string + Lines() LineMap + PopulateTextLineMap() LineMap +} + +type textDocumentBase struct { + lineMap LineMap +} + +func (td textDocumentBase) Line(line int) (TextLine, error) { + return td.lineMap.TextLine(line) +} + +func (td textDocumentBase) LinePositionFromTextPosition(textPosition int) (LinePosition, error) { + return td.lineMap.LinePositionFromPosition(textPosition) +} + +func (td textDocumentBase) TextPositionFromLinePosition(linePosition LinePosition) (int, error) { + return td.lineMap.TextPositionFromLinePosition(linePosition) +} diff --git a/tools/text/text-documents.go b/tools/text/text-documents.go new file mode 100644 index 000000000..7069f6120 --- /dev/null +++ b/tools/text/text-documents.go @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package text + +// TextDocumentFromText creates a TextDocument from the given text string. +func TextDocumentFromText(text string) TextDocument { + return NewStringTextDocument(text) +} diff --git a/tools/text/text-edit.go b/tools/text/text-edit.go new file mode 100644 index 000000000..966218944 --- /dev/null +++ b/tools/text/text-edit.go @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package text + +import "fmt" + +// TextEdit represents a text edit on a TextDocument. +type TextEdit interface { + Range() TextRange + Text() string + String() string +} + +type textEditImpl struct { + textRange TextRange + text string +} + +func TextEditFromTextRangeAndText(textRange TextRange, text string) TextEdit { + return &textEditImpl{ + textRange: textRange, + text: text, + } +} + +func (te textEditImpl) Range() TextRange { + return te.textRange +} + +func (te textEditImpl) Text() string { + return te.text +} + +func (te textEditImpl) String() string { + return fmt.Sprintf("%s%s", te.textRange.String(), te.text) +} diff --git a/tools/text/text-line.go b/tools/text/text-line.go new file mode 100644 index 000000000..733838b77 --- /dev/null +++ b/tools/text/text-line.go @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package text + +// TextLine represents a single line in the TextDocument. +type TextLine interface { + LineNo() int + Text() string + StartOffset() int + EndOffset() int + EndOffsetWithNewLines() int + Length() int + LengthWithNewLineChars() int +} + +type textLineImpl struct { + lineNo int + text string + startOffset int + endOffset int + lengthOfNewLineChars int +} + +func NewTextLine(lineNo int, text string, startOffset, endOffset, lengthOfNewLineChars int) TextLine { + return &textLineImpl{ + lineNo: lineNo, + text: text, + startOffset: startOffset, + endOffset: endOffset, + lengthOfNewLineChars: lengthOfNewLineChars, + } +} + +func (tl textLineImpl) LineNo() int { + return tl.lineNo +} + +func (tl textLineImpl) Text() string { + return tl.text +} + +func (tl textLineImpl) StartOffset() int { + return tl.startOffset +} + +func (tl textLineImpl) EndOffset() int { + return tl.endOffset +} + +func (tl textLineImpl) EndOffsetWithNewLines() int { + return tl.endOffset + tl.lengthOfNewLineChars +} + +func (tl textLineImpl) Length() int { + return tl.endOffset - tl.startOffset +} + +func (tl textLineImpl) LengthWithNewLineChars() int { + return tl.endOffset - tl.startOffset + tl.lengthOfNewLineChars +} diff --git a/tools/text/text-range.go b/tools/text/text-range.go new file mode 100644 index 000000000..a751cda6a --- /dev/null +++ b/tools/text/text-range.go @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package text + +import "fmt" + +// TextRange describes a contiguous sequence of unicode code points in the TextDocument. +type TextRange interface { + StartOffset() int + EndOffset() int + Length() int + Contains(position int) bool + IntersectionExists(textRange TextRange) bool + String() string + TextRangeLookupKey() TextRangeLookupKey +} + +// TextRangeLookupKey represents the comparable fields of TextRange for equality/hashing. +type TextRangeLookupKey struct { + StartOffset int + EndOffset int +} + +type textRangeImpl struct { + startOffset int + endOffset int + length int +} + +func TextRangeFromStartOffsetAndLength(startOffset, length int) TextRange { + return &textRangeImpl{ + startOffset: startOffset, + length: length, + endOffset: startOffset + length, + } +} + +func (tr textRangeImpl) StartOffset() int { + return tr.startOffset +} + +func (tr textRangeImpl) EndOffset() int { + return tr.endOffset +} + +func (tr textRangeImpl) Length() int { + return tr.length +} + +func (tr textRangeImpl) Contains(position int) bool { + return tr.startOffset <= position && position < tr.endOffset +} + +// IntersectionExists tests whether there exists an intersection of this range and the given range. +// The ranges R1(S1, E1) and R2(S2, E2) intersects if S1 is greater than or equal to E2 and +// S2 is less than or equal to E1. +func (tr textRangeImpl) IntersectionExists(textRange TextRange) bool { + return tr.startOffset <= textRange.EndOffset() && textRange.StartOffset() <= tr.endOffset +} + +func (tr textRangeImpl) String() string { + return fmt.Sprintf("(%d,%d)", tr.startOffset, tr.endOffset) +} + +// TextRangeLookupKey returns the lookup key for equality comparisons. +func (tr textRangeImpl) TextRangeLookupKey() TextRangeLookupKey { + return TextRangeLookupKey{ + StartOffset: tr.startOffset, + EndOffset: tr.endOffset, + } +} From 95ab461a5182eaca2fee13d9c5c18af4ccf69a43 Mon Sep 17 00:00:00 2001 From: snelusha Date: Tue, 5 Aug 2025 14:31:47 +0530 Subject: [PATCH 02/11] Delegate `TextLines` to the `Lines()` method --- tools/text/string-text-document.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tools/text/string-text-document.go b/tools/text/string-text-document.go index 574c08a82..508d57d28 100644 --- a/tools/text/string-text-document.go +++ b/tools/text/string-text-document.go @@ -70,12 +70,8 @@ func (std *stringTextDocumentImpl) String() string { return std.text } -func (std *stringTextDocumentImpl) TextLines() []string { - if std.textDocumentBase.lineMap != nil { - return std.textDocumentBase.lineMap.TextLines() - } - std.textDocumentBase.lineMap = std.PopulateTextLineMap() - return std.textDocumentBase.lineMap.TextLines() +func (std stringTextDocumentImpl) TextLines() []string { + return std.Lines().TextLines() } func (std *stringTextDocumentImpl) Lines() LineMap { From 7d3fe1e61808a1bac1cb99abd27993f687055303 Mon Sep 17 00:00:00 2001 From: snelusha Date: Tue, 5 Aug 2025 21:46:03 +0530 Subject: [PATCH 03/11] Refactor `IndexOutOfBoundsError` and `IllegalArgumentError` --- common/errors/errors.go | 32 ++++++++++++++++++++++++-------- tools/text/line-map.go | 4 ++-- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/common/errors/errors.go b/common/errors/errors.go index 59ab80aa3..36a5f2d62 100644 --- a/common/errors/errors.go +++ b/common/errors/errors.go @@ -18,30 +18,46 @@ package errors +import "fmt" + type IndexOutOfBoundsError struct { - message string + index int + length int } func (e IndexOutOfBoundsError) Error() string { - return e.message + return fmt.Sprintf("Index %d out of bounds for length %d", e.index, e.length) +} + +func (e IndexOutOfBoundsError) GetIndex() int { + return e.index } -func NewIndexOutOfBoundsError(message string) *IndexOutOfBoundsError { +func (e IndexOutOfBoundsError) GetLength() int { + return e.length +} + +func NewIndexOutOfBoundsError(index, length int) *IndexOutOfBoundsError { return &IndexOutOfBoundsError{ - message: message, + index: index, + length: length, } } type IllegalArgumentError struct { - message string + argument interface{} } func (e IllegalArgumentError) Error() string { - return e.message + return fmt.Sprintf("Illegal argument: %v", e.argument) +} + +func (e IllegalArgumentError) GetArgument() interface{} { + return e.argument } -func NewIllegalArgumentError(message string) *IllegalArgumentError { +func NewIllegalArgumentError(argument interface{}) *IllegalArgumentError { return &IllegalArgumentError{ - message: message, + argument: argument, } } diff --git a/tools/text/line-map.go b/tools/text/line-map.go index 528c29ab1..0872aea45 100644 --- a/tools/text/line-map.go +++ b/tools/text/line-map.go @@ -79,14 +79,14 @@ func (lm lineMapImpl) TextLines() []string { func (lm lineMapImpl) positionRangeCheck(position int) error { if position < 0 || position > lm.textLines[lm.length-1].EndOffset() { - return errors.NewIndexOutOfBoundsError(fmt.Sprintf("Index: '%d', Size: '%d'", position, lm.textLines[lm.length-1].EndOffset())) + return errors.NewIndexOutOfBoundsError(position, lm.textLines[lm.length-1].EndOffset()) } return nil } func (lm lineMapImpl) lineRangeCheck(lineNo int) error { if lineNo < 0 || lineNo > lm.length { - return errors.NewIndexOutOfBoundsError(fmt.Sprintf("Line number: '%d', Size: '%d'", lineNo, lm.length)) + return errors.NewIndexOutOfBoundsError(lineNo, lm.length) } return nil } From d1d97b2c29027df0440f86b5c125c021d62c72be Mon Sep 17 00:00:00 2001 From: snelusha Date: Wed, 6 Aug 2025 11:50:46 +0530 Subject: [PATCH 04/11] Refactor `calculateTextLines` to ignore rune (int32) --- tools/text/string-text-document.go | 45 ++++++++++++++++-------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/tools/text/string-text-document.go b/tools/text/string-text-document.go index 508d57d28..d35aa2a6c 100644 --- a/tools/text/string-text-document.go +++ b/tools/text/string-text-document.go @@ -20,6 +20,11 @@ package text import "strings" +const ( + CR = 13 // Carriage Return + LF = 10 // Line Feed +) + // StringTextDocument represents a TextDocument created with a string. type StringTextDocument interface { TextDocument @@ -75,47 +80,45 @@ func (std stringTextDocumentImpl) TextLines() []string { } func (std *stringTextDocumentImpl) Lines() LineMap { - if std.textDocumentBase.lineMap != nil { - return std.textDocumentBase.lineMap + if std.lineMap != nil { + return std.lineMap } - std.textDocumentBase.lineMap = std.PopulateTextLineMap() - return std.textDocumentBase.lineMap + std.lineMap = std.PopulateTextLineMap() + return std.lineMap } func (std *stringTextDocumentImpl) calculateTextLines() []TextLine { - startOffset := 0 var textLines []TextLine - var lineBuilder strings.Builder - index := 0 + line := 0 + startOffset := 0 + + index := 0 textLength := len(std.text) + var lengthOfNewLineChars int for index < textLength { - c := rune(std.text[index]) - if c == '\r' || c == '\n' { - nextCharIndex := index + 1 - if c == '\r' && textLength != nextCharIndex && rune(std.text[nextCharIndex]) == '\n' { + if std.text[index] == CR || std.text[index] == LF { + nextIndex := index + 1 + if std.text[index] == CR && nextIndex < textLength && std.text[nextIndex] == LF { lengthOfNewLineChars = 2 } else { lengthOfNewLineChars = 1 } - strLine := lineBuilder.String() - endOffset := startOffset + len(strLine) - textLines = append(textLines, NewTextLine(line, strLine, startOffset, endOffset, lengthOfNewLineChars)) - line++ + endOffset := startOffset + (index - startOffset) + textLines = append(textLines, NewTextLine(line, std.text[startOffset:index], startOffset, endOffset, lengthOfNewLineChars)) + + line = line + 1 startOffset = endOffset + lengthOfNewLineChars - lineBuilder.Reset() - index += lengthOfNewLineChars + index = index + lengthOfNewLineChars } else { - lineBuilder.WriteRune(c) - index++ + index = index + 1 } } - strLine := lineBuilder.String() - textLines = append(textLines, NewTextLine(line, strLine, startOffset, startOffset+len(strLine), 0)) + textLines = append(textLines, NewTextLine(line, std.text[startOffset:], startOffset, textLength, 0)) return textLines } From 8581550752ecfcccc8bdac6a44e5654a98011591 Mon Sep 17 00:00:00 2001 From: snelusha Date: Wed, 6 Aug 2025 12:00:34 +0530 Subject: [PATCH 05/11] =?UTF-8?q?Refactor=20`String()`=20to=20drop=20the?= =?UTF-8?q?=20empty=E2=80=90slice=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/text/text-document-change.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tools/text/text-document-change.go b/tools/text/text-document-change.go index 60e002bff..5ec38240d 100644 --- a/tools/text/text-document-change.go +++ b/tools/text/text-document-change.go @@ -50,10 +50,6 @@ func (tdc textDocumentChangeImpl) GetTextEdit(index int) TextEdit { } func (tdc textDocumentChangeImpl) String() string { - if len(tdc.textEdits) == 0 { - return "" - } - var editStrings []string for _, textEdit := range tdc.textEdits { editStrings = append(editStrings, textEdit.String()) From a5b694022aec315be62dae70875ef8049298cec6 Mon Sep 17 00:00:00 2001 From: snelusha Date: Wed, 6 Aug 2025 21:40:20 +0530 Subject: [PATCH 06/11] Return -1 instead of 0 on error --- tools/text/line-map.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/text/line-map.go b/tools/text/line-map.go index 0872aea45..41c445e7e 100644 --- a/tools/text/line-map.go +++ b/tools/text/line-map.go @@ -60,11 +60,11 @@ func (lm lineMapImpl) LinePositionFromPosition(position int) (LinePosition, erro func (lm lineMapImpl) TextPositionFromLinePosition(linePosition LinePosition) (int, error) { if err := lm.lineRangeCheck(linePosition.Line()); err != nil { - return 0, err + return -1, err } textLine := lm.textLines[linePosition.Line()] if textLine.Length() < linePosition.Offset() { - return 0, errors.NewIllegalArgumentError(fmt.Sprintf("Cannot find a line with the character offset '%d'", linePosition.Offset())) + return -1, errors.NewIllegalArgumentError(fmt.Sprintf("Cannot find a line with the character offset '%d'", linePosition.Offset())) } return textLine.StartOffset() + linePosition.Offset(), nil } From 8df6ffcc92ee461d83babbfad2abad1a42f7f582 Mon Sep 17 00:00:00 2001 From: Sithija Nelusha Silva Date: Thu, 7 Aug 2025 11:13:58 +0530 Subject: [PATCH 07/11] Use `any` type instead of `interface{}` --- common/errors/errors.go | 6 +++--- tools/diagnostics/default-diagnostic.go | 6 +++--- tools/diagnostics/diagnostic-factory.go | 4 ++-- tools/diagnostics/diagnostic-property.go | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/common/errors/errors.go b/common/errors/errors.go index 36a5f2d62..2884ccfd7 100644 --- a/common/errors/errors.go +++ b/common/errors/errors.go @@ -45,18 +45,18 @@ func NewIndexOutOfBoundsError(index, length int) *IndexOutOfBoundsError { } type IllegalArgumentError struct { - argument interface{} + argument any } func (e IllegalArgumentError) Error() string { return fmt.Sprintf("Illegal argument: %v", e.argument) } -func (e IllegalArgumentError) GetArgument() interface{} { +func (e IllegalArgumentError) GetArgument() any { return e.argument } -func NewIllegalArgumentError(argument interface{}) *IllegalArgumentError { +func NewIllegalArgumentError(argument any) *IllegalArgumentError { return &IllegalArgumentError{ argument: argument, } diff --git a/tools/diagnostics/default-diagnostic.go b/tools/diagnostics/default-diagnostic.go index 011c1d660..74220cca2 100644 --- a/tools/diagnostics/default-diagnostic.go +++ b/tools/diagnostics/default-diagnostic.go @@ -33,11 +33,11 @@ type defaultDiagnosticImpl struct { diagnosticBase diagnosticInfo DiagnosticInfo location Location - properties []DiagnosticProperty[interface{}] + properties []DiagnosticProperty[any] message string } -func NewDefaultDiagnostic(diagnosticInfo DiagnosticInfo, location Location, properties []DiagnosticProperty[any], args ...interface{}) DefaultDiagnostic { +func NewDefaultDiagnostic(diagnosticInfo DiagnosticInfo, location Location, properties []DiagnosticProperty[any], args ...any) DefaultDiagnostic { message := formatMessage(diagnosticInfo.MessageFormat(), args...) return &defaultDiagnosticImpl{ diagnosticBase: diagnosticBase{}, @@ -82,7 +82,7 @@ func (dd *defaultDiagnosticImpl) String() string { dd.Message()) } -func formatMessage(format string, args ...interface{}) string { +func formatMessage(format string, args ...any) string { if len(args) == 0 { return format } diff --git a/tools/diagnostics/diagnostic-factory.go b/tools/diagnostics/diagnostic-factory.go index 612aa8f65..d749fb38a 100644 --- a/tools/diagnostics/diagnostic-factory.go +++ b/tools/diagnostics/diagnostic-factory.go @@ -26,7 +26,7 @@ package diagnostics // - args: arguments to diagnostic message format // // Returns a Diagnostic instance. -func CreateDiagnostic(diagnosticInfo DiagnosticInfo, location Location, args ...interface{}) Diagnostic { +func CreateDiagnostic(diagnosticInfo DiagnosticInfo, location Location, args ...any) Diagnostic { return NewDefaultDiagnostic(diagnosticInfo, location, []DiagnosticProperty[any]{}, args...) } @@ -39,6 +39,6 @@ func CreateDiagnostic(diagnosticInfo DiagnosticInfo, location Location, args ... // - args: arguments to diagnostic message format // // Returns a Diagnostic instance. -func CreateDiagnosticWithProperties(diagnosticInfo DiagnosticInfo, location Location, properties []DiagnosticProperty[any], args ...interface{}) Diagnostic { +func CreateDiagnosticWithProperties(diagnosticInfo DiagnosticInfo, location Location, properties []DiagnosticProperty[any], args ...any) Diagnostic { return NewDefaultDiagnostic(diagnosticInfo, location, properties, args...) } diff --git a/tools/diagnostics/diagnostic-property.go b/tools/diagnostics/diagnostic-property.go index c3e573d47..909bf32e7 100644 --- a/tools/diagnostics/diagnostic-property.go +++ b/tools/diagnostics/diagnostic-property.go @@ -19,7 +19,7 @@ package diagnostics // DiagnosticProperty represents properties passed when diagnostic logging. -type DiagnosticProperty[T interface{}] interface { +type DiagnosticProperty[T any] interface { Kind() DiagnosticPropertyKind Value() T } From e3414b1e5178d00c25d1df8eeb60167f7541f260 Mon Sep 17 00:00:00 2001 From: snelusha Date: Sun, 10 Aug 2025 22:48:32 +0530 Subject: [PATCH 08/11] Add Copilot prompt file --- .github/prompts/migration.prompt.md | 151 ++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 .github/prompts/migration.prompt.md diff --git a/.github/prompts/migration.prompt.md b/.github/prompts/migration.prompt.md new file mode 100644 index 000000000..206164252 --- /dev/null +++ b/.github/prompts/migration.prompt.md @@ -0,0 +1,151 @@ +# Task + +Convert the selected Java source into idiomatic Go **with minimal semantic drift**. + +- Map packages to directories and package names. +- Map classes (including abstract) and their fields/methods to Go **interface** and **unexported implementation structs**. +- Preserve logic while adapting to Go naming, encapsulation and error handling. +- Keep diffs focused: one Java file -> one Go file (small support code if strictly necessary). + +## Inputs + +- Java file(s) to convert. +- Any related types the file depends on. +- Package path target within the Go module. + +## Outputs + +- A new Go file with idiomatic code. + +## Package & File Mapping + +1. **One-to-one element mapping** + - **Files**: Map each Java source file `.java` to to one go file, named `lowercase-with-dashes.go` (e.g., `FooBar.java` -> `foo-bar.go`). Be consistent within the package. + - **Classes/Interfaces**: For each Java class or interface define a **Go interface** named after the Java type (exported) and an **unexported implementation struct**. + - **Methods**: Map each java method to a Go method or function. For overloads, pick distinct names (e.g., `Advance`, `AdvanceN`). + +2. **Package structure** + - Java: `package com.example.foo.something;` + - Go: directory `foo/something` with `package something` in the file. + +## Types & Encapsulation + +### Classes -> Interface + Impl Struct + +- Define an **interface** named exactly after the Java class, e.g. `Foo`. +- Define a constructor `NewFoo(...) Foo` returning the interface. +- Define an **unexported** struct `fooImpl` implements the interface. Prefer composition/embedding for reuse. + +### Fields -> Struct Fields + +- Private/encapsulated state lives in unexported struct fields (e.g., `bar int`). +- Provide getters/setters as interface methods only if the Java API requires them to be public. Avoid exporting fields directly unless they are immutable configuration. + +### Constructors + +For each Java public constructor: + +```go +func NewFoo(params) Foo { + return &fooImpl{/* initialize fields */} +} +``` + +- If multiple Java constructors exist, use either distinct names (`NewFooWithParams`) **or** the functional options pattern for optional params (avoid overloading). + +### Methods & Receivers + +- **Non-mutating** -> value receiver if the struct is small and the method is read-only. +- **Mutating** -> pointer receiver. +- Prefer returning `(T, error)` over panicking; translate Java exceptions to `error` values when they cross API boundaries. + +### Abstract classes + +```go +type Foo interface { + // abstract methods + requires accessors +} + +type fooBase struct {/* shared fields */} + +func (b fooBase) GetX() T { return b.x } + +type fooImpl struct { + fooBase +} +func NewFoo(params) Foo { + return &fooImpl{ + fooBase: fooBase{/* initialize shared fields */}, + } +} +``` + +### Method overloading + +Java: + +```java +void advance(); +void advance(int n); +``` + +Go: + +```go +func (r *readerImpl) Advance() {} +func (r *readerImpl) AdvanceN(n int) {} +``` + +### Equality / Hashing + +- If Java only overrides `equals()`/`hashCode()`, in Go prefer: + - Use direct `==` for comparable structs; or + - Provide an explicit **lookup key**: + + ```go + type FooLookupKey struct { Bar int; Baz string } + func (f *fooImpl) FooLookupKey() FooLookupKey { return FooLookupKey{f.bar, f.baz} } + ``` + +## Naming & Comments + +- **Packages:** short, lowercase, single word (`something`). +- **Exports:** capitalize to export. Keep names concise and avoid stutter (prefer `something.Reader` with type name `Reader`). + +## Error Handling + +- Always return `(T, error)` for fallible operations. Don't use `panic` for normal control flow. +- Wrap lower-level errors with context using `fmt.Errorf("op: %w", err)` so callers can use `errors.Is/As`. +- Match errors with `errors.Is` (sentinels) or `errors.As` (typed errors with additional context). + +### Java exceptions -> Go typed errors + +- Define Java-specific exceptions as typed errros in `common/errors/errors.go` (package `errors`). + +```go +package errors + +import "fmt" + +type IndexOutOfBoundsError struct { + index int + length int +} + +func (e IndexOutOfBoundsError) Error() string { + return fmt.Sprintf("Index %d out of bounds for length %d", e.index, e.length) +} + +func (e IndexOutOfBoundsError) GetIndex() int { return e.index } +func (e IndexOutOfBoundsError) GetLength() int { return e.length } +``` + +## Generics + +- Map Java generics to Go generics when needed. + +## Guardrails & Non goals + +- **Do no** add file headers or license comments. +- **Do not** introduce new public APIs unless requires by the Java surface. +- **Do not** add comments unless the Java source has them. From 687b886f56684805802c8a918ce4be87d1e4f864 Mon Sep 17 00:00:00 2001 From: Sithija Nelusha Silva Date: Mon, 11 Aug 2025 14:33:50 +0530 Subject: [PATCH 09/11] Use overflow-safe midpoint calc --- tools/text/line-map.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/text/line-map.go b/tools/text/line-map.go index 41c445e7e..fb1f20b67 100644 --- a/tools/text/line-map.go +++ b/tools/text/line-map.go @@ -106,8 +106,10 @@ func (lm lineMapImpl) findLineFrom(position int) TextLine { right := lm.length - 1 for left <= right { - // Using bit shift to handle overflow when sum of left and right is greater than max int - middle := (left + right) >> 1 + lhs := left >> 1 + rhs := right >> 1 + + middle := (lhs + rhs) + (left & right & 1) startOffset := lm.textLines[middle].StartOffset() endOffset := lm.textLines[middle].EndOffsetWithNewLines() From cfb60deb4f3cd8cc5540beda45eafbe8c765980c Mon Sep 17 00:00:00 2001 From: Sithija Nelusha Silva Date: Thu, 14 Aug 2025 12:00:30 +0530 Subject: [PATCH 10/11] Use `string` instead of `[]rune` --- tools/text/char-reader.go | 50 +++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/tools/text/char-reader.go b/tools/text/char-reader.go index ef02580fe..8e59ff49d 100644 --- a/tools/text/char-reader.go +++ b/tools/text/char-reader.go @@ -18,7 +18,10 @@ package text -import "unicode" +import ( + "unicode" + "unicode/utf8" +) // CharReader is a character reader utility used by the Ballerina lexer. type CharReader interface { @@ -34,29 +37,23 @@ type CharReader interface { // charReaderImpl is the concrete implementation of CharReader. type charReaderImpl struct { - charBuffer []rune + charBuffer string offset int charBufferLength int lexemeStartPos int } -// newCharReader constructs a CharReader with the given character buffer. -func newCharReader(charBuffer []rune) CharReader { - return &charReaderImpl{ - charBuffer: charBuffer, - offset: 0, - charBufferLength: len(charBuffer), - lexemeStartPos: 0, - } -} - func CharReaderFromTextDocument(textDocument TextDocument) CharReader { - return newCharReader(textDocument.ToCharArray()) + return CharReaderFromText(string(textDocument.ToCharArray())) } func CharReaderFromText(text string) CharReader { - charBuffer := []rune(text) - return newCharReader(charBuffer) + return &charReaderImpl{ + charBuffer: text, + offset: 0, + charBufferLength: len(text), + lexemeStartPos: 0, + } } func (cr *charReaderImpl) Reset(offset int) { @@ -65,7 +62,8 @@ func (cr *charReaderImpl) Reset(offset int) { func (cr charReaderImpl) Peek() rune { if cr.offset < cr.charBufferLength { - return cr.charBuffer[cr.offset] + r, _ := utf8.DecodeRuneInString(cr.charBuffer[cr.offset:]) + return r } else { // TODO Revisit this branch return unicode.MaxRune @@ -73,9 +71,15 @@ func (cr charReaderImpl) Peek() rune { } func (cr charReaderImpl) PeekN(k int) rune { - n := cr.offset + k + n := cr.offset + for range k { + _, size := utf8.DecodeRuneInString(cr.charBuffer[n:]) + n = n + size + } + if n < cr.charBufferLength { - return cr.charBuffer[n] + r, _ := utf8.DecodeRuneInString(cr.charBuffer[n:]) + return r } else { // TODO Revisit this branch return unicode.MaxRune @@ -83,11 +87,17 @@ func (cr charReaderImpl) PeekN(k int) rune { } func (cr *charReaderImpl) Advance() { - cr.offset++ + _, size := utf8.DecodeRuneInString(cr.charBuffer[cr.offset:]) + cr.offset = cr.offset + size } func (cr *charReaderImpl) AdvanceN(k int) { - cr.offset += k + for range k { + if cr.offset < cr.charBufferLength { + _, size := utf8.DecodeRuneInString(cr.charBuffer[cr.offset:]) + cr.offset = cr.offset + size + } + } } func (cr *charReaderImpl) Mark() { From f997904f896539f4675ec2ef5a1b37f8e0646273 Mon Sep 17 00:00:00 2001 From: Sithija Nelusha Silva Date: Mon, 27 Oct 2025 11:03:08 +0530 Subject: [PATCH 11/11] Address code review suggestions --- tools/diagnostics/default-diagnostic.go | 4 ++-- tools/diagnostics/diagnostic-factory.go | 2 +- tools/text/char-reader.go | 4 ++-- tools/text/line-map.go | 15 ++++++--------- tools/text/string-text-document.go | 3 +-- tools/text/text-document.go | 1 + 6 files changed, 13 insertions(+), 16 deletions(-) diff --git a/tools/diagnostics/default-diagnostic.go b/tools/diagnostics/default-diagnostic.go index 74220cca2..2c133e98f 100644 --- a/tools/diagnostics/default-diagnostic.go +++ b/tools/diagnostics/default-diagnostic.go @@ -19,8 +19,9 @@ package diagnostics import ( - "ballerina-lang-go/tools/text" "fmt" + + "ballerina-lang-go/tools/text" ) // DefaultDiagnostic is an internal implementation of the Diagnostic interface that is used by the DiagnosticFactory @@ -40,7 +41,6 @@ type defaultDiagnosticImpl struct { func NewDefaultDiagnostic(diagnosticInfo DiagnosticInfo, location Location, properties []DiagnosticProperty[any], args ...any) DefaultDiagnostic { message := formatMessage(diagnosticInfo.MessageFormat(), args...) return &defaultDiagnosticImpl{ - diagnosticBase: diagnosticBase{}, diagnosticInfo: diagnosticInfo, location: location, properties: properties, diff --git a/tools/diagnostics/diagnostic-factory.go b/tools/diagnostics/diagnostic-factory.go index d749fb38a..2f5cad000 100644 --- a/tools/diagnostics/diagnostic-factory.go +++ b/tools/diagnostics/diagnostic-factory.go @@ -27,7 +27,7 @@ package diagnostics // // Returns a Diagnostic instance. func CreateDiagnostic(diagnosticInfo DiagnosticInfo, location Location, args ...any) Diagnostic { - return NewDefaultDiagnostic(diagnosticInfo, location, []DiagnosticProperty[any]{}, args...) + return NewDefaultDiagnostic(diagnosticInfo, location, nil, args...) } // CreateDiagnosticWithProperties creates a Diagnostic instance from the given details. diff --git a/tools/text/char-reader.go b/tools/text/char-reader.go index 8e59ff49d..52b8a18eb 100644 --- a/tools/text/char-reader.go +++ b/tools/text/char-reader.go @@ -44,7 +44,7 @@ type charReaderImpl struct { } func CharReaderFromTextDocument(textDocument TextDocument) CharReader { - return CharReaderFromText(string(textDocument.ToCharArray())) + return CharReaderFromText(textDocument.String()) } func CharReaderFromText(text string) CharReader { @@ -105,7 +105,7 @@ func (cr *charReaderImpl) Mark() { } func (cr charReaderImpl) GetMarkedChars() string { - return string(cr.charBuffer[cr.lexemeStartPos:cr.offset]) + return cr.charBuffer[cr.lexemeStartPos:cr.offset] } func (cr charReaderImpl) IsEOF() bool { diff --git a/tools/text/line-map.go b/tools/text/line-map.go index fb1f20b67..bd1044e59 100644 --- a/tools/text/line-map.go +++ b/tools/text/line-map.go @@ -19,8 +19,9 @@ package text import ( - "ballerina-lang-go/common/errors" "fmt" + + "ballerina-lang-go/common/errors" ) // LineMap represents a collection of text lines in the TextDocument. @@ -66,6 +67,7 @@ func (lm lineMapImpl) TextPositionFromLinePosition(linePosition LinePosition) (i if textLine.Length() < linePosition.Offset() { return -1, errors.NewIllegalArgumentError(fmt.Sprintf("Cannot find a line with the character offset '%d'", linePosition.Offset())) } + // TODO: Lazy initialize and cache return textLine.StartOffset() + linePosition.Offset(), nil } @@ -100,27 +102,22 @@ func (lm lineMapImpl) findLineFrom(position int) TextLine { } else if position == lm.textLines[lm.length-1].EndOffset() { return lm.textLines[lm.length-1] } - - var foundTextLine TextLine left := 0 right := lm.length - 1 - for left <= right { lhs := left >> 1 rhs := right >> 1 - middle := (lhs + rhs) + (left & right & 1) startOffset := lm.textLines[middle].StartOffset() endOffset := lm.textLines[middle].EndOffsetWithNewLines() - if startOffset <= position && position < endOffset { - foundTextLine = lm.textLines[middle] - break + return lm.textLines[middle] } else if endOffset <= position { left = middle + 1 } else { right = middle - 1 } } - return foundTextLine + // This should never happen given the boundary checks above + panic("binary search failed to find matching text line") } diff --git a/tools/text/string-text-document.go b/tools/text/string-text-document.go index d35aa2a6c..b8458920d 100644 --- a/tools/text/string-text-document.go +++ b/tools/text/string-text-document.go @@ -39,8 +39,7 @@ type stringTextDocumentImpl struct { func NewStringTextDocument(text string) StringTextDocument { return &stringTextDocumentImpl{ - textDocumentBase: textDocumentBase{}, - text: text, + text: text, } } diff --git a/tools/text/text-document.go b/tools/text/text-document.go index 240fb8915..863edb01a 100644 --- a/tools/text/text-document.go +++ b/tools/text/text-document.go @@ -28,6 +28,7 @@ type TextDocument interface { TextLines() []string Lines() LineMap PopulateTextLineMap() LineMap + String() string } type textDocumentBase struct {