diff --git a/Fixtures/LineIterator/bar.swift b/Fixtures/LineIterator/bar.swift new file mode 100644 index 0000000..7bb5e3c --- /dev/null +++ b/Fixtures/LineIterator/bar.swift @@ -0,0 +1,14 @@ +struct Bar { + let x: Int + let y: String +} + +extension Bar { + func foo() -> Int { + return 1 + } +} + +private struct Blah { + let x: Bar +} diff --git a/Fixtures/LineIterator/complete_inner.swift b/Fixtures/LineIterator/complete_inner.swift new file mode 100644 index 0000000..8faaaca --- /dev/null +++ b/Fixtures/LineIterator/complete_inner.swift @@ -0,0 +1,198 @@ +// XFAIL: broken_std_regex + +class Foo {} +class Base { + init() {} + func base() {} +} +class FooBar { + init() {} + init(x: Foo) {} + static func fooBar() {} + static func fooBaz() {} + func method() {} + let prop: FooBar { return FooBar() } + subscript(x: Foo) -> Foo {} +} + +enum E1 { + case one + case two +} + +// Note: this test uses line:column inputs as a workaround for a complete-test limitation. +func test010(x: E1, y: FooBar) { + switch x { + case .: + break + case two: + y. + } +} + +// RUN: %sourcekitd-test -req=complete.open -pos=26:11 -req-opts=filtertext=one %s -- %s | %FileCheck %s -check-prefix=INNER_POSTFIX_0 +// INNER_POSTFIX_0-NOT: key.description: "one{{.+}}" +// INNER_POSTFIX_0: key.description: "one",{{$}} +// INNER_POSTFIX_0-NOT: key.description: "one{{.+}}" + +// RUN: %sourcekitd-test -req=complete.open -pos=29:9 -req-opts=filtertext=prop %s -- %s | %FileCheck %s -check-prefix=INNER_POSTFIX_1 +// INNER_POSTFIX_1-NOT: key.description: "prop{{.+}}" +// INNER_POSTFIX_1: key.description: "prop",{{$}} +// INNER_POSTFIX_1-NOT: key.description: "prop{{.+}}" + + +func test001() { + #^TOP_LEVEL_0,fo,foo,foob,foobar^# +} +// RUN: %complete-test %s -no-fuzz -group=none -add-inner-results -tok=TOP_LEVEL_0 | %FileCheck %s -check-prefix=TOP_LEVEL_0 +// TOP_LEVEL_0-LABEL: Results for filterText: fo [ +// TOP_LEVEL_0-NEXT: for +// TOP_LEVEL_0-NEXT: Foo +// TOP_LEVEL_0-NEXT: FooBar +// TOP_LEVEL_0: ] + +// TOP_LEVEL_0-LABEL: Results for filterText: foo [ +// TOP_LEVEL_0-NEXT: Foo +// TOP_LEVEL_0-NEXT: Foo( +// TOP_LEVEL_0-NEXT: FooBar +// TOP_LEVEL_0-NEXT: Foo() +// TOP_LEVEL_0-NEXT: ] + +// TOP_LEVEL_0-LABEL: Results for filterText: foob [ +// TOP_LEVEL_0-NEXT: FooBar +// TOP_LEVEL_0-NEXT: ] + +// TOP_LEVEL_0-LABEL: Results for filterText: foobar [ +// TOP_LEVEL_0-NEXT: FooBar +// TOP_LEVEL_0-NEXT: FooBar. +// TOP_LEVEL_0-NEXT: FooBar( +// TOP_LEVEL_0-NEXT: FooBar() +// TOP_LEVEL_0-NEXT: FooBar(x: Foo) +// TOP_LEVEL_0-NEXT: FooBar.fooBar() +// TOP_LEVEL_0-NEXT: FooBar.fooBaz() +// TOP_LEVEL_0: ] + +func test002(abc: FooBar, abd: Base) { + #^TOP_LEVEL_1,ab,abc,abd^# +} +// RUN: %complete-test %s -no-fuzz -group=none -add-inner-results -tok=TOP_LEVEL_1 | %FileCheck %s -check-prefix=TOP_LEVEL_1 +// TOP_LEVEL_1-LABEL: Results for filterText: ab [ +// TOP_LEVEL_1-NEXT: abc +// TOP_LEVEL_1-NEXT: abd +// TOP_LEVEL_1: ] + +// TOP_LEVEL_1-LABEL: Results for filterText: abc [ +// TOP_LEVEL_1-NEXT: abc +// TOP_LEVEL_1-NEXT: abc. +// TOP_LEVEL_1-NEXT: abc=== +// TOP_LEVEL_1-NEXT: abc!== +// TOP_LEVEL_1-NEXT: abc.method() +// TOP_LEVEL_1-NEXT: abc.prop +// TOP_LEVEL_1-NEXT: ] + +// TOP_LEVEL_1-LABEL: Results for filterText: abd [ +// TOP_LEVEL_1-NEXT: abd +// TOP_LEVEL_1-NEXT: abd. +// TOP_LEVEL_1-NEXT: abd=== +// TOP_LEVEL_1-NEXT: abd!== +// TOP_LEVEL_1-NEXT: abd.base() +// TOP_LEVEL_1-NEXT: ] + +func test003(x: FooBar) { + x.#^FOOBAR_QUALIFIED,pro,prop,prop.^# +} +// RUN: %complete-test %s -group=none -add-inner-results -tok=FOOBAR_QUALIFIED | %FileCheck %s -check-prefix=FOOBAR_QUALIFIED +// FOOBAR_QUALIFIED-LABEL: Results for filterText: pro [ +// FOOBAR_QUALIFIED-NEXT: prop +// FOOBAR_QUALIFIED-NEXT: ] + +// FOOBAR_QUALIFIED-LABEL: Results for filterText: prop [ +// FOOBAR_QUALIFIED-NEXT: prop +// FOOBAR_QUALIFIED-NEXT: prop. +// FOOBAR_QUALIFIED: prop.method() +// FOOBAR_QUALIFIED-NEXT: prop.prop +// FOOBAR_QUALIFIED-NEXT: ] + +// Just don't explode. We generally expect to get a new session here. +// FOOBAR_QUALIFIED-LABEL: Results for filterText: prop. [ +// FOOBAR_QUALIFIED-NEXT: ] + +// RUN: %complete-test %s -group=none -no-inner-results -inner-operators -tok=FOOBAR_QUALIFIED | %FileCheck %s -check-prefix=FOOBAR_QUALIFIED_OP +// FOOBAR_QUALIFIED_OP-LABEL: Results for filterText: pro [ +// FOOBAR_QUALIFIED_OP-NEXT: prop +// FOOBAR_QUALIFIED_OP-NEXT: ] +// FOOBAR_QUALIFIED_OP-LABEL: Results for filterText: prop [ +// FOOBAR_QUALIFIED_OP-NEXT: prop +// FOOBAR_QUALIFIED_OP-NEXT: prop. +// FOOBAR_QUALIFIED_OP: ] + +// RUN: %complete-test %s -group=none -add-inner-results -no-inner-operators -tok=FOOBAR_QUALIFIED | %FileCheck %s -check-prefix=FOOBAR_QUALIFIED_NOOP +// FOOBAR_QUALIFIED_NOOP-LABEL: Results for filterText: pro [ +// FOOBAR_QUALIFIED_NOOP-NEXT: prop +// FOOBAR_QUALIFIED_NOOP-NEXT: ] +// FOOBAR_QUALIFIED_NOOP-LABEL: Results for filterText: prop [ +// FOOBAR_QUALIFIED_NOOP-NEXT: prop +// FOOBAR_QUALIFIED_NOOP-NEXT: prop.method() +// FOOBAR_QUALIFIED_NOOP-NEXT: prop.prop +// FOOBAR_QUALIFIED_NOOP-NEXT: ] + +// RUN: %complete-test %s -group=none -no-include-exact-match -add-inner-results -no-inner-operators -tok=FOOBAR_QUALIFIED | %FileCheck %s -check-prefix=FOOBAR_QUALIFIED_NOEXACT +// FOOBAR_QUALIFIED_NOEXACT-LABEL: Results for filterText: prop [ +// FOOBAR_QUALIFIED_NOEXACT-NEXT: prop.method() +// FOOBAR_QUALIFIED_NOEXACT-NEXT: prop.prop +// FOOBAR_QUALIFIED_NOEXACT-NEXT: ] + +func test004() { + FooBar#^FOOBAR_POSTFIX^# +} +// RUN: %complete-test %s -group=none -no-inner-results -inner-operators -tok=FOOBAR_POSTFIX | %FileCheck %s -check-prefix=FOOBAR_POSTFIX_OP +// FOOBAR_POSTFIX_OP: {{^}}.{{$}} +// FOOBAR_POSTFIX_OP: {{^}}({{$}} + +func test005(x: FooBar) { + x#^FOOBAR_INSTANCE_POSTFIX^# +} +// RUN: %complete-test %s -group=none -no-inner-results -inner-operators -tok=FOOBAR_INSTANCE_POSTFIX | %FileCheck %s -check-prefix=FOOBAR_INSTANCE_POSTFIX_OP +// FOOBAR_INSTANCE_POSTFIX_OP: . +// FIXME: We should probably just have '[' here - rdar://22702955 +// FOOBAR_INSTANCE_POSTFIX_OP: [Foo] + +func test005(x: Base?) { + x#^OPTIONAL_POSTFIX^# +} +// RUN: %complete-test %s -group=none -no-inner-results -inner-operators -tok=OPTIONAL_POSTFIX | %FileCheck %s -check-prefix=OPTIONAL_POSTFIX_OP +// OPTIONAL_POSTFIX_OP: . +// OPTIONAL_POSTFIX_OP: ?. + +// RUN: %complete-test %s -group=none -no-inner-results -inner-operators -tok=KEYWORD_0 | %FileCheck %s -check-prefix=KEYWORD_0 +func test006() { + #^KEYWORD_0,for^# +} +// KEYWORD_0-NOT: for_ +// KEYWORD_0-NOT: fortest +// KEYWORD_0-NOT: for. + +enum E0 { + case case0 +} + +// RUN: %complete-test %s -group=none -no-inner-results -no-inner-operators -tok=LEADING_DOT_0 | %FileCheck %s -check-prefix=LEADING_NODOT_E0 +// RUN: %complete-test %s -group=none -no-inner-results -inner-operators -tok=LEADING_DOT_0 | %FileCheck %s -check-prefix=LEADING_DOT_E0 +func test007() { + var e: E0 + e = #^LEADING_DOT_0^# +} +// LEADING_NODOT_E0-NOT: . +// LEADING_DOT_E0: . + +struct WithLeading { + static var foo: WithLeading = WithLeading() +} + +// RUN: %complete-test %s -group=none -no-inner-results -inner-operators -tok=LEADING_DOT_1 | %FileCheck %s -check-prefix=LEADING_DOT_S +func test009() { + var e: WithLeading + e = #^LEADING_DOT_1^# +} +// FIXME: should have leading dot. +// LEADING_DOT_S-NOT: . diff --git a/Fixtures/LineIterator/cursor_getter.swift b/Fixtures/LineIterator/cursor_getter.swift new file mode 100644 index 0000000..20bfe43 --- /dev/null +++ b/Fixtures/LineIterator/cursor_getter.swift @@ -0,0 +1,24 @@ +var t1 : Bool { return true } +var t2 : Bool { get { return true } } +var t3 : Bool { get { return true } set {} } + +struct S1 { + subscript(i: Int) -> Bool { return true } +} + +// Checks that we don't crash. +// RUN: %sourcekitd-test -req=cursor -pos=1:15 %s -- %s | %FileCheck %s +// RUN: %sourcekitd-test -req=cursor -pos=1:17 %s -- %s | %FileCheck %s +// RUN: %sourcekitd-test -req=cursor -pos=2:15 %s -- %s | %FileCheck %s +// RUN: %sourcekitd-test -req=cursor -pos=2:17 %s -- %s | %FileCheck %s +// RUN: %sourcekitd-test -req=cursor -pos=2:21 %s -- %s | %FileCheck %s +// RUN: %sourcekitd-test -req=cursor -pos=2:23 %s -- %s | %FileCheck %s +// RUN: %sourcekitd-test -req=cursor -pos=3:15 %s -- %s | %FileCheck %s +// RUN: %sourcekitd-test -req=cursor -pos=3:17 %s -- %s | %FileCheck %s +// RUN: %sourcekitd-test -req=cursor -pos=3:21 %s -- %s | %FileCheck %s +// RUN: %sourcekitd-test -req=cursor -pos=3:23 %s -- %s | %FileCheck %s +// RUN: %sourcekitd-test -req=cursor -pos=3:37 %s -- %s | %FileCheck %s +// RUN: %sourcekitd-test -req=cursor -pos=3:41 %s -- %s | %FileCheck %s +// RUN: %sourcekitd-test -req=cursor -pos=6:29 %s -- %s | %FileCheck %s +// RUN: %sourcekitd-test -req=cursor -pos=6:31 %s -- %s | %FileCheck %s +// CHECK: diff --git a/Fixtures/LineIterator/main.swift b/Fixtures/LineIterator/main.swift new file mode 100644 index 0000000..565dc15 --- /dev/null +++ b/Fixtures/LineIterator/main.swift @@ -0,0 +1,2 @@ +let x = Bar(x: 1, y: "Ryan") +print(x.y) \ No newline at end of file diff --git a/Package.resolved b/Package.resolved index 87a7ba7..e0beff9 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,9 +5,9 @@ "package": "AEXML", "repositoryURL": "https://github.com/tadija/AEXML.git", "state": { - "branch": "master", + "branch": null, "revision": "6eea665515d079c338690147082a8084a36484b0", - "version": null + "version": "4.3.0" } }, { diff --git a/Package.swift b/Package.swift index c463bda..cdf1641 100644 --- a/Package.swift +++ b/Package.swift @@ -42,10 +42,10 @@ let package = Package( .target( name: "LanguageServerProtocol", dependencies: [ + "AEXML", "BaseProtocol", "SourceKitter", - "SwiftPM", - "AEXML", + "SwiftPM" ] ), .target( diff --git a/Sources/LanguageServerProtocol/Types/LineCollection.swift b/Sources/LanguageServerProtocol/Types/LineCollection.swift deleted file mode 100644 index b30d739..0000000 --- a/Sources/LanguageServerProtocol/Types/LineCollection.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// LineCollection.swift -// langserver-swift -// -// Created by Ryan Lovelett on 11/22/16. -// -// - -import Foundation - -fileprivate let lf: UInt8 = 0x0A - -fileprivate func buildLineRangeCollection(from data: Data) -> [Range] { - var array: [Range] = [] - var lower: Data.Index = data.startIndex - var last: Range = Range(data.startIndex..(lower...upper) - array.append(last) - // The next lowest is greater than the current index - lower = (upper + 1) - } - // File does not have a trailing line-feed - if last.upperBound != data.endIndex { - let end = Range(last.upperBound..] - - init(for file: URL) throws { - data = try Data(contentsOf: file, options: .mappedIfSafe) - lines = buildLineRangeCollection(from: data) - } - - init?(for string: String) { - guard let d = string.data(using: .utf8) else { - return nil - } - data = d - lines = buildLineRangeCollection(from: data) - } - - func byteOffset(at: Position) throws -> Int { - guard at.line < lines.count else { throw WorkspaceError.positionNotFound } - let lineRange = lines[at.line] - let offset = lineRange.lowerBound.advanced(by: at.character) - guard offset < lineRange.upperBound else { throw WorkspaceError.positionNotFound } - return offset - } - - func position(for offset: Int) throws -> Position { - guard let lineIndex = lines.index(where: { $0.contains(offset) }) else { throw WorkspaceError.positionNotFound } - let lineRange = lines[lineIndex] - let x = offset - lineRange.lowerBound - let position = Position(line: lineIndex, character: x) - return position - } - - func selection(startAt offset: Int, length: Int) throws -> TextDocumentRange { - let endOffset = Int(offset + length) - let start = try position(for: offset) - let end = try position(for: endOffset) - return TextDocumentRange(start: start, end: end) - } - -} diff --git a/Sources/LanguageServerProtocol/Types/LineIterator.swift b/Sources/LanguageServerProtocol/Types/LineIterator.swift new file mode 100644 index 0000000..8c64522 --- /dev/null +++ b/Sources/LanguageServerProtocol/Types/LineIterator.swift @@ -0,0 +1,78 @@ +// +// LineIterator.swift +// LanguageServerProtocol +// +// Created by Ryan Lovelett on 5/30/18. +// + +private extension Character { + + var isLineEnding: Bool { + return self == "\r\n" || self == "\r" || self == "\n" + } + +} + +struct Line { + let number: Int + let start: String.Index + let end: String.Index + let last: Bool + + func contains(_ index: String.Index) -> Bool { + if last { + return (start...end).contains(index) + } else { + return (start.. Line? { + guard cursor < text.endIndex else { + if handleTrailingNewLine { + handleTrailingNewLine = false + return Line(number: lineNumber, start: text.endIndex, end: text.endIndex, last: true) + } + // If here we are at EOF + return nil + } + // Make sure to increment the line number + defer { lineNumber += 1 } + let start = cursor + guard !text[cursor].isLineEnding else { + // If the first character on a line is a new line character + cursor = text.index(after: cursor) + return Line(number: lineNumber, start: start, end: cursor, last: false) + } + var isLineEding: Bool = false + var isEOF: Bool = false + while !isLineEding && !isEOF { + cursor = text.index(after: cursor) + isEOF = cursor == text.endIndex + isLineEding = (isEOF) ? true : text[cursor].isLineEnding + } + if isLineEding && !isEOF { + cursor = text.index(after: cursor) + } + return Line(number: lineNumber, start: start, end: cursor, last: isEOF) + } + +} diff --git a/Sources/LanguageServerProtocol/Types/Server.swift b/Sources/LanguageServerProtocol/Types/Server.swift index db5ef47..1113fe5 100644 --- a/Sources/LanguageServerProtocol/Types/Server.swift +++ b/Sources/LanguageServerProtocol/Types/Server.swift @@ -172,7 +172,7 @@ public class Server { private func getCursor(forText at: TextDocumentPositionParams) throws -> Cursor? { let url = at.textDocument.uri let (module, source) = try getSource(url) - let offset = try Int64(source.lines.byteOffset(at: at.position)) + let offset = try Int64(source.byteOffset(at: at.position)) // SourceKit may send back JSON that is an empty object. This is _not_ an error condition. // So we have to seperate SourceKit throwing an error from SourceKit sending back a @@ -197,7 +197,7 @@ public class Server { switch c.defined { case let .local(filepath, symbolOffset, symbolLength): let (_, source) = try getSource(filepath) - let range = try source.lines.selection(startAt: Int(symbolOffset), length: Int(symbolLength)) + let range = try source.selection(startAt: Int(symbolOffset), length: Int(symbolLength)) let location = Location(uri: filepath.absoluteString, range: range) return [location] case .system(_): @@ -240,7 +240,7 @@ public class Server { switch c.defined { case let .local(filepath, symbolOffset, symbolLength): let (_, source) = try getSource(filepath) - let range = try source.lines.selection(startAt: Int(symbolOffset), length: Int(symbolLength)) + let range = try source.selection(startAt: Int(symbolOffset), length: Int(symbolLength)) return Hover(contents: contents, range: range) case .system(_): return Hover(contents: contents, range: .none) @@ -255,7 +255,7 @@ public class Server { public func complete(forText at: TextDocumentPositionParams) throws -> [CompletionItem] { let url = at.textDocument.uri let (module, source) = try getSource(url) - let offset = try Int64(source.lines.byteOffset(at: at.position)) + let offset = try Int64(source.byteOffset(at: at.position)) #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) os_log("%{public}@", log: log, type: .default, url as NSURL) os_log("Line %d, character %d, byte %d", log: log, type: .default, at.position.line, at.position.character, offset) diff --git a/Sources/LanguageServerProtocol/Types/TextDocument.swift b/Sources/LanguageServerProtocol/Types/TextDocument.swift index b170e00..9a3bfb2 100644 --- a/Sources/LanguageServerProtocol/Types/TextDocument.swift +++ b/Sources/LanguageServerProtocol/Types/TextDocument.swift @@ -29,9 +29,6 @@ struct TextDocument : TextDocumentItem { /// The content of the opened text document. let text: String - /// A collection of Ranges that denote the different byte position of lines in a document. - let lines: LineCollection - } extension TextDocument { @@ -40,28 +37,13 @@ extension TextDocument { /// /// - Parameter file: The file, on the local file system, that should be read. init?(_ file: URL) { + guard let contents = try? String(contentsOf: file, encoding: .utf8) else { + return nil + } uri = file languageId = "swift" version = Int.min - text = "" - guard let lines = try? LineCollection(for: file) else { return nil } - self.lines = lines - } - - /// Create a text document instance from information sent by the client. - /// - /// - Parameters: - /// - uri: The source file's fully qualified path on the filesystem. - /// - languageId: The text document's language identifier, typically `"swift"`. - /// - version: The version number of this document. - /// - text: The content of the opened text document. - fileprivate init(uri: URL, languageId: String, version: Int, text: String) { - self.uri = uri - self.languageId = languageId - self.version = version - self.text = text - // TODO: Force cast 🤢 - self.lines = LineCollection(for: text)! + text = contents } /// Create a new instance from the current instance, whild changing the version and text. @@ -74,6 +56,57 @@ extension TextDocument { return TextDocument(uri: uri, languageId: languageId, version: version, text: andText) } + /// Converts the position to a zero-based byte offset in the TextDocument. + /// + /// - Parameter position: The position to convert. + /// - Returns: The byte offset from the TextDocument start index. + /// - Throws: WorkspaceError.positionNotFound if the Position is not within the bounds of the TextDocument. + func byteOffset(at position: Position) throws -> Int { + let seq = AnySequence { LineIterator(self.text) } + guard let line = seq.first(where: { $0.number == position.line }) else { + throw WorkspaceError.positionNotFound + } + let limit = (line.last) ? text.endIndex : text.index(before: line.end) + guard let final = text.index(line.start, offsetBy: position.character, limitedBy: limit) else { + throw WorkspaceError.positionNotFound + } + return text.distance(from: text.startIndex, to: final) + } + + /// Create a Position in a TextDocument from a byte offset. + /// + /// - Parameter offset: The offset from the start of the TextDocument. + /// - Returns: A new and valid Position. + /// - Throws: WorkspaceError.positionNotFound if the offset is not within the bounds of the TextDocument. + func position(for offset: Int) throws -> Position { + guard offset >= 0 else { + throw WorkspaceError.positionNotFound + } + guard let index = text.index(text.startIndex, offsetBy: offset, limitedBy: text.endIndex) else { + throw WorkspaceError.positionNotFound + } + let seq = AnySequence { LineIterator(self.text) } + guard let line = seq.first(where: { $0.contains(index) }) else { + throw WorkspaceError.positionNotFound + } + let character = text.distance(from: line.start, to: index) + return Position(line: line.number, character: character) + } + + /// Create a TextDocumentRange in a TextDocument from a byte offset and number of bytes. + /// + /// - Parameters: + /// - offset: The offset from the start of the TextDocument. + /// - length: The number of bytes from the offset to include in the range. + /// - Returns: A new and valid TextDocumentRange. + /// - Throws: WorkspaceError.positionNotFound if the range is not within the bounds of the TextDocument. + func selection(startAt offset: Int, length: Int) throws -> TextDocumentRange { + let endOffset = Int(offset + length) + let start = try position(for: offset) + let end = try position(for: endOffset) + return TextDocumentRange(start: start, end: end) + } + } extension TextDocument : Argo.Decodable { diff --git a/Tests/LanguageServerProtocolTests/Types/LineCollectionTests.swift b/Tests/LanguageServerProtocolTests/Types/LineCollectionTests.swift deleted file mode 100644 index 1e48168..0000000 --- a/Tests/LanguageServerProtocolTests/Types/LineCollectionTests.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// LineCollectionTests.swift -// langserver-swift -// -// Created by Ryan Lovelett on 11/22/16. -// -// - -import XCTest -@testable import LanguageServerProtocol - -class LineCollectionTests: XCTestCase { - - override func setUp() { - super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() - } - - func testOffsetCalculation() { - let url = getFixture("bar.swift", in: "ValidLayouts/Simple/Sources")! - - do { - let lc = try LineCollection(for: url) - - XCTAssertEqual(lc.lines.count, 14) - - // All should be valid - XCTAssertEqual(try lc.byteOffset(at: Position(line: 0, character: 0)), 0) // First byte - XCTAssertEqual(try lc.byteOffset(at: Position(line: 5, character: 0)), 49) - XCTAssertEqual(try lc.byteOffset(at: Position(line: 5, character: 14)), 63) - XCTAssertEqual(try lc.byteOffset(at: Position(line: 11, character: 8)), 123) - XCTAssertEqual(try lc.byteOffset(at: Position(line: 13, character: 1)), 153) // Last byte - - // Off the "right" edge of the first line - XCTAssertNil(try? lc.byteOffset(at: Position(line: 0, character: 13))) - - // Off the "bottom" edge of the document - XCTAssertNil(try? lc.byteOffset(at: Position(line: 15, character: 0))) - } catch { - XCTFail(error.localizedDescription) - } - } - - func testPositionCalculation() { - let url = getFixture("bar.swift", in: "ValidLayouts/Simple/Sources")! - - do { - let lc = try LineCollection(for: url) - - // All should be valid - XCTAssertEqual(try lc.position(for: 0), Position(line: 0, character: 0)) - XCTAssertEqual(try lc.position(for: 49), Position(line: 5, character: 0)) - XCTAssertEqual(try lc.position(for: 63), Position(line: 5, character: 14)) - XCTAssertEqual(try lc.position(for: 123), Position(line: 11, character: 8)) - XCTAssertEqual(try lc.position(for: 153), Position(line: 13, character: 1)) - - // All should be out of range - XCTAssertNil(try? lc.position(for: -100)) - XCTAssertNil(try? lc.position(for: 4700)) - } catch { - XCTFail(error.localizedDescription) - } - } - - func testSourceWithoutTrailingNewLine() { - let url = getFixture("main.swift", in: "ValidLayouts/Simple/Sources")! - - do { - let lc = try LineCollection(for: url) - XCTAssertEqual(lc.lines.count, 2) - } catch { - XCTFail(error.localizedDescription) - } - } - -} diff --git a/Tests/LanguageServerProtocolTests/Types/LineIteratorTests.swift b/Tests/LanguageServerProtocolTests/Types/LineIteratorTests.swift new file mode 100644 index 0000000..9251e6b --- /dev/null +++ b/Tests/LanguageServerProtocolTests/Types/LineIteratorTests.swift @@ -0,0 +1,117 @@ +// +// LineIteratorTests.swift +// LanguageServerProtocolTests +// +// Created by Ryan Lovelett on 5/30/18. +// + +import XCTest +@testable import LanguageServerProtocol + +class LineIteratorTests: XCTestCase { + + let completeInner: String = { + let url = getFixture("complete_inner.swift", in: "LineIterator")! + return try! String(contentsOf: url) + }() + + let cursorGetter: String = { + let url = getFixture("cursor_getter.swift", in: "LineIterator")! + return try! String(contentsOf: url) + }() + + let simpleMain: String = { + let url = getFixture("main.swift", in: "LineIterator")! + return try! String(contentsOf: url) + }() + + let simpleBar: String = { + let url = getFixture("bar.swift", in: "LineIterator")! + return try! String(contentsOf: url) + }() + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testCountLines() { + XCTAssertEqual(Array(AnySequence { LineIterator(self.completeInner) }).count, 199) + XCTAssertEqual(Array(AnySequence { LineIterator(self.cursorGetter) }).count, 25) + XCTAssertEqual(Array(AnySequence { LineIterator(self.simpleMain) }).count, 2) + XCTAssertEqual(Array(AnySequence { LineIterator(self.simpleBar) }).count, 15) + } + + func testLineLayoutSimpleMain() { + let seq = AnySequence { LineIterator(self.simpleMain) } + let lines = Dictionary(seq.map { ($0.number, String(self.simpleMain[$0.start..<$0.end])) }) + let expectedLines = [ + 0: "let x = Bar(x: 1, y: \"Ryan\")\n", + 1: "print(x.y)" + ] + XCTAssertEqual(lines, expectedLines) + } + + func testLineLayoutSimpleBar() { + let seq = AnySequence { LineIterator(self.simpleBar) } + let lines = Dictionary(seq.map { ($0.number, String(self.simpleBar[$0.start..<$0.end])) }) + // Why are there 14 lines? + // Why, is the last line an empty string? + // If the last character in the document is a new line VS Code displays a zero length line + let expectedLines = [ + 0: "struct Bar {\n", + 1: " let x: Int\n", + 2: " let y: String\n", + 3: "}\n", + 4: "\n", + 5: "extension Bar {\n", + 6: " func foo() -> Int {\n", + 7: " return 1\n", + 8: " }\n", + 9: "}\n", + 10: "\n", + 11: "private struct Blah {\n", + 12: " let x: Bar\n", + 13: "}\n", + 14: "" + ] + XCTAssertEqual(lines, expectedLines) + } + + func testLineContainsIndex() { + var it = LineIterator(self.simpleBar) + let firstLine = it.next()! + let secondLine = it.next()! + XCTAssertTrue(firstLine.contains(self.simpleBar.startIndex)) + XCTAssertTrue(firstLine.contains(firstLine.start)) + XCTAssertTrue(firstLine.contains(self.simpleBar.index(before: firstLine.end))) + XCTAssertFalse(firstLine.contains(firstLine.end)) + XCTAssertFalse(firstLine.contains(secondLine.start)) + XCTAssertFalse(firstLine.contains(secondLine.end)) + XCTAssertFalse(firstLine.contains(self.simpleBar.endIndex)) + } + + func testLastLine() { + for line in AnySequence({ LineIterator(self.simpleBar) }) { + if line.number == 14 { + XCTAssertTrue(line.last) + } else { + XCTAssertFalse(line.last) + } + } + + for line in AnySequence({ LineIterator(self.simpleMain) }) { + if line.number == 1 { + XCTAssertTrue(line.last) + } else { + XCTAssertFalse(line.last) + } + } + } + +} diff --git a/Tests/LanguageServerProtocolTests/Types/SwiftSourceTests.swift b/Tests/LanguageServerProtocolTests/Types/SwiftSourceTests.swift deleted file mode 100644 index 13f8719..0000000 --- a/Tests/LanguageServerProtocolTests/Types/SwiftSourceTests.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// SwiftSourceTests.swift -// langserver-swift -// -// Created by Ryan Lovelett on 11/21/16. -// -// - -import Foundation -@testable import LanguageServerProtocol -import XCTest - -class SwiftSourceTests: XCTestCase { - - func testFindDefintion() { - let m = URL(fileURLWithPath: "/Users/ryan/Desktop/Test/Sources/main.swift") - let b = URL(fileURLWithPath: "/Users/ryan/Desktop/Test/Sources/bar.swift") - let ms = TextDocument(m) - let bs = TextDocument(b) -// let e = ms!.defines("s:vV4main3Bar1ySS") -// XCTAssertNotNil(e) -// print(e!) - } - - func testTwo() { - let m = URL(fileURLWithPath: "/Users/ryan/Desktop/Test/Sources/main.swift") - let b = URL(fileURLWithPath: "/Users/ryan/Desktop/Test/Sources/bar.swift") - let ms = TextDocument(m) - let bs = TextDocument(b) -// XCTAssertNil(ms!.defines("s:V4main3Bar")) -// let e = bs!.defines("s:V4main3Bar") -// XCTAssertNotNil(e) -// print(e!) -// print(e!) - } - -} diff --git a/Tests/LanguageServerProtocolTests/Types/TextDocumentTests.swift b/Tests/LanguageServerProtocolTests/Types/TextDocumentTests.swift new file mode 100644 index 0000000..ee7d1ba --- /dev/null +++ b/Tests/LanguageServerProtocolTests/Types/TextDocumentTests.swift @@ -0,0 +1,145 @@ +// +// TextDocumentTests.swift +// langserver-swift +// +// Created by Ryan Lovelett on 11/21/16. +// +// + +import Foundation +@testable import LanguageServerProtocol +import XCTest + +class TextDocumentTests: XCTestCase { + + func testCompleteInner() { + let m = getFixture("complete_inner.swift", in: "LineIterator")! + let ms = TextDocument(m)! + let conversions = [ + // resolveFromLineCol(26, 11) => 455 + 455: Position(line: 25, character: 10), + // resolveFromLineCol(29, 9) => 491 + 491: Position(line: 28, character: 8), + ] + for conversion in conversions { + XCTAssertEqual(try ms.byteOffset(at: conversion.value), conversion.key) + XCTAssertEqual(try ms.position(for: conversion.key), conversion.value) + } + } + + func testCusorGetter() { + let m = getFixture("cursor_getter.swift", in: "LineIterator")! + let ms = TextDocument(m)! + + let conversions = [ + // resolveFromLineCol(1, 1) => 0 + 0: Position(line: 0, character: 0), + // RUN: %sourcekitd-test -req=cursor -pos=1:15 %s -- %s | %FileCheck %s + // resolveFromLineCol(1, 15) => 14 + 14: Position(line: 0, character: 14), + // RUN: %sourcekitd-test -req=cursor -pos=1:17 %s -- %s | %FileCheck %s + // resolveFromLineCol(1, 17) => 16 + 16: Position(line: 0, character: 16), + // resolveFromLineCol(1, 30) => 29 + 29: Position(line: 0, character: 29), + // RUN: %sourcekitd-test -req=cursor -pos=2:15 %s -- %s | %FileCheck %s + // resolveFromLineCol(2, 15) => 44 + 44: Position(line: 1, character: 14), + // RUN: %sourcekitd-test -req=cursor -pos=2:17 %s -- %s | %FileCheck %s + // resolveFromLineCol(2, 17) => 46 + 46: Position(line: 1, character: 16), + // RUN: %sourcekitd-test -req=cursor -pos=2:21 %s -- %s | %FileCheck %s + // resolveFromLineCol(2, 21) => 50 + 50: Position(line: 1, character: 20), + // RUN: %sourcekitd-test -req=cursor -pos=2:23 %s -- %s | %FileCheck %s + // resolveFromLineCol(2, 23) => 52 + 52: Position(line: 1, character: 22), + // RUN: %sourcekitd-test -req=cursor -pos=3:15 %s -- %s | %FileCheck %s + // resolveFromLineCol(3, 15) => 82 + 82: Position(line: 2, character: 14), + // RUN: %sourcekitd-test -req=cursor -pos=3:17 %s -- %s | %FileCheck %s + // resolveFromLineCol(3, 17) => 84 + 84: Position(line: 2, character: 16), + // RUN: %sourcekitd-test -req=cursor -pos=3:21 %s -- %s | %FileCheck %s + // resolveFromLineCol(3, 21) => 88 + 88: Position(line: 2, character: 20), + // RUN: %sourcekitd-test -req=cursor -pos=3:23 %s -- %s | %FileCheck %s + // resolveFromLineCol(3, 23) => 90 + 90: Position(line: 2, character: 22), + // RUN: %sourcekitd-test -req=cursor -pos=3:37 %s -- %s | %FileCheck %s + // resolveFromLineCol(3, 37) => 104 + 104: Position(line: 2, character: 36), + // RUN: %sourcekitd-test -req=cursor -pos=3:41 %s -- %s | %FileCheck %s + // resolveFromLineCol(3, 41) => 108 + 108: Position(line: 2, character: 40), + // RUN: %sourcekitd-test -req=cursor -pos=6:29 %s -- %s | %FileCheck %s + // resolveFromLineCol(6, 29) => 154 + 154: Position(line: 5, character: 28), + // RUN: %sourcekitd-test -req=cursor -pos=6:31 %s -- %s | %FileCheck %s + // resolveFromLineCol(6, 31) => 156 + 156: Position(line: 5, character: 30) + ] + for conversion in conversions { + XCTAssertEqual(try ms.byteOffset(at: conversion.value), conversion.key) + XCTAssertEqual(try ms.position(for: conversion.key), conversion.value) + } + + XCTAssertThrowsError(try ms.byteOffset(at: Position(line: 0, character: 30))) + XCTAssertThrowsError(try ms.byteOffset(at: Position(line: 25, character: 1))) + } + + func testSimpleBar() { + let url = getFixture("bar.swift", in: "ValidLayouts/Simple/Sources")! + let ms = TextDocument(url)! + + let conversions = [ + 0: Position(line: 0, character: 0), + 12: Position(line: 0, character: 12), + 13: Position(line: 1, character: 0), + 49: Position(line: 5, character: 0), + 63: Position(line: 5, character: 14), + 123: Position(line: 11, character: 8), + 153: Position(line: 13, character: 1), + 154: Position(line: 14, character: 0) + ] + for conversion in conversions { + XCTAssertEqual(try ms.byteOffset(at: conversion.value), conversion.key) + XCTAssertEqual(try ms.position(for: conversion.key), conversion.value) + } + + // Off the right "edge" of the line + XCTAssertThrowsError(try ms.byteOffset(at: Position(line: 0, character: 13))) + XCTAssertThrowsError(try ms.byteOffset(at: Position(line: 1, character: 15))) + XCTAssertThrowsError(try ms.byteOffset(at: Position(line: 2, character: 18))) + XCTAssertThrowsError(try ms.byteOffset(at: Position(line: 3, character: 2))) + XCTAssertThrowsError(try ms.byteOffset(at: Position(line: 4, character: 1))) + XCTAssertThrowsError(try ms.byteOffset(at: Position(line: 5, character: 16))) + XCTAssertThrowsError(try ms.byteOffset(at: Position(line: 6, character: 24))) + XCTAssertThrowsError(try ms.byteOffset(at: Position(line: 7, character: 17))) + XCTAssertThrowsError(try ms.byteOffset(at: Position(line: 8, character: 6))) + XCTAssertThrowsError(try ms.byteOffset(at: Position(line: 9, character: 2))) + XCTAssertThrowsError(try ms.byteOffset(at: Position(line: 10, character: 1))) + XCTAssertThrowsError(try ms.byteOffset(at: Position(line: 11, character: 22))) + XCTAssertThrowsError(try ms.byteOffset(at: Position(line: 12, character: 15))) + XCTAssertThrowsError(try ms.byteOffset(at: Position(line: 13, character: 2))) + XCTAssertThrowsError(try ms.byteOffset(at: Position(line: 14, character: 1))) + + // Off the "bottom" edge of the document + XCTAssertThrowsError(try ms.byteOffset(at: Position(line: 15, character: 0))) + } + + func testSimpleMain() { + let url = getFixture("main.swift", in: "ValidLayouts/Simple/Sources")! + let lc = TextDocument(url)! + + XCTAssertEqual(try lc.byteOffset(at: Position(line: 1, character: 3)), 32) + XCTAssertEqual(try lc.byteOffset(at: Position(line: 1, character: 9)), 38) + XCTAssertEqual(try lc.byteOffset(at: Position(line: 1, character: 10)), 39) + + // Of the right "edge" of the line + XCTAssertThrowsError(try lc.byteOffset(at: Position(line: 1, character: 11))) + + XCTAssertThrowsError(try lc.position(for: -1)) + } + +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 5d400ea..1caa12b 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -5,7 +5,7 @@ // LinuxMain.swift // Tests // -// Created by Sourcery on 2018-05-27T21:24:16-0400. +// Created by Sourcery on 2018-05-30T17:17:03-0400. // sourcery --sources Tests --templates sourcery --output Tests --args testimports='@testable import BaseProtocolTests // @testable import LanguageServerProtocolTests' // @@ -45,11 +45,13 @@ extension HeaderTests { ("testHeaderWithMultipleFields", testHeaderWithMultipleFields) ] } -extension LineCollectionTests { - static var allTests: [(String, (LineCollectionTests) -> () throws -> Void)] = [ - ("testOffsetCalculation", testOffsetCalculation), - ("testPositionCalculation", testPositionCalculation), - ("testSourceWithoutTrailingNewLine", testSourceWithoutTrailingNewLine) +extension LineIteratorTests { + static var allTests: [(String, (LineIteratorTests) -> () throws -> Void)] = [ + ("testCountLines", testCountLines), + ("testLineLayoutSimpleMain", testLineLayoutSimpleMain), + ("testLineLayoutSimpleBar", testLineLayoutSimpleBar), + ("testLineContainsIndex", testLineContainsIndex), + ("testLastLine", testLastLine) ] } extension RequestIteratorTests { @@ -81,10 +83,12 @@ extension SwiftModuleTests { static var allTests: [(String, (SwiftModuleTests) -> () throws -> Void)] = [ ] } -extension SwiftSourceTests { - static var allTests: [(String, (SwiftSourceTests) -> () throws -> Void)] = [ - ("testFindDefintion", testFindDefintion), - ("testTwo", testTwo) +extension TextDocumentTests { + static var allTests: [(String, (TextDocumentTests) -> () throws -> Void)] = [ + ("testCompleteInner", testCompleteInner), + ("testCusorGetter", testCusorGetter), + ("testSimpleBar", testSimpleBar), + ("testSimpleMain", testSimpleMain) ] } extension URLTests { @@ -106,12 +110,12 @@ XCTMain([ testCase(CursorTests.allTests), testCase(DidChangeWatchedFilesParamsTests.allTests), testCase(HeaderTests.allTests), - testCase(LineCollectionTests.allTests), + testCase(LineIteratorTests.allTests), testCase(RequestIteratorTests.allTests), testCase(RequestTests.allTests), testCase(ResponseTests.allTests), testCase(SwiftModuleTests.allTests), - testCase(SwiftSourceTests.allTests), + testCase(TextDocumentTests.allTests), testCase(URLTests.allTests), testCase(WorkspaceTests.allTests), ])