mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
247 lines
7.7 KiB
Swift
247 lines
7.7 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors
|
|
// Licensed under Apache License v2.0 with Runtime Library Exception
|
|
//
|
|
// See https://swift.org/LICENSE.txt for license information
|
|
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import Dispatch
|
|
import LanguageServerProtocol
|
|
import LSPLogging
|
|
import SKSupport
|
|
|
|
public struct DocumentSnapshot {
|
|
public var document: Document
|
|
public var version: Int
|
|
public var lineTable: LineTable
|
|
/// Syntax highlighting tokens for the document. Note that
|
|
/// `uri` + `latestVersion` only uniquely identifies a snapshot's content,
|
|
/// the tokens are updated independently and only used internally.
|
|
public var tokens: DocumentTokens
|
|
|
|
public var text: String { lineTable.content }
|
|
|
|
public init(
|
|
document: Document,
|
|
version: Int,
|
|
lineTable: LineTable,
|
|
tokens: DocumentTokens
|
|
) {
|
|
self.document = document
|
|
self.version = version
|
|
self.lineTable = lineTable
|
|
self.tokens = tokens
|
|
}
|
|
|
|
func index(of pos: Position) -> String.Index? {
|
|
return lineTable.stringIndexOf(line: pos.line, utf16Column: pos.utf16index)
|
|
}
|
|
}
|
|
|
|
public final class Document {
|
|
public let uri: DocumentURI
|
|
public let language: Language
|
|
var latestVersion: Int
|
|
var latestLineTable: LineTable
|
|
var latestTokens: DocumentTokens
|
|
|
|
init(uri: DocumentURI, language: Language, version: Int, text: String) {
|
|
self.uri = uri
|
|
self.language = language
|
|
self.latestVersion = version
|
|
self.latestLineTable = LineTable(text)
|
|
self.latestTokens = DocumentTokens()
|
|
}
|
|
|
|
/// **Not thread safe!** Use `DocumentManager.latestSnapshot` instead.
|
|
fileprivate var latestSnapshot: DocumentSnapshot {
|
|
DocumentSnapshot(
|
|
document: self,
|
|
version: latestVersion,
|
|
lineTable: latestLineTable,
|
|
tokens: latestTokens
|
|
)
|
|
}
|
|
}
|
|
|
|
public final class DocumentManager {
|
|
|
|
public enum Error: Swift.Error {
|
|
case alreadyOpen(DocumentURI)
|
|
case missingDocument(DocumentURI)
|
|
}
|
|
|
|
let queue: DispatchQueue = DispatchQueue(label: "document-manager-queue")
|
|
|
|
var documents: [DocumentURI: Document] = [:]
|
|
|
|
public init() {}
|
|
|
|
/// All currently opened documents.
|
|
public var openDocuments: Set<DocumentURI> {
|
|
return queue.sync {
|
|
return Set(documents.keys)
|
|
}
|
|
}
|
|
|
|
/// Opens a new document with the given content and metadata.
|
|
///
|
|
/// - returns: The initial contents of the file.
|
|
/// - throws: Error.alreadyOpen if the document is already open.
|
|
@discardableResult
|
|
public func open(_ uri: DocumentURI, language: Language, version: Int, text: String) throws -> DocumentSnapshot {
|
|
return try queue.sync {
|
|
let document = Document(uri: uri, language: language, version: version, text: text)
|
|
if nil != documents.updateValue(document, forKey: uri) {
|
|
throw Error.alreadyOpen(uri)
|
|
}
|
|
return document.latestSnapshot
|
|
}
|
|
}
|
|
|
|
/// Closes the given document.
|
|
///
|
|
/// - returns: The initial contents of the file.
|
|
/// - throws: Error.missingDocument if the document is not open.
|
|
public func close(_ uri: DocumentURI) throws {
|
|
try queue.sync {
|
|
if nil == documents.removeValue(forKey: uri) {
|
|
throw Error.missingDocument(uri)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Applies the given edits to the document.
|
|
///
|
|
/// - parameter willEditDocument: Optional closure to call before each edit.
|
|
/// - parameter updateDocumentTokens: Optional closure to call after each edit.
|
|
/// - parameter before: The document contents *before* the edit is applied.
|
|
/// - parameter after: The document contents *after* the edit is applied.
|
|
/// - returns: The contents of the file after all the edits are applied.
|
|
/// - throws: Error.missingDocument if the document is not open.
|
|
@discardableResult
|
|
public func edit(
|
|
_ uri: DocumentURI,
|
|
newVersion: Int,
|
|
edits: [TextDocumentContentChangeEvent],
|
|
willEditDocument: ((_ before: DocumentSnapshot, TextDocumentContentChangeEvent) -> Void)? = nil,
|
|
updateDocumentTokens: ((_ after: DocumentSnapshot) -> DocumentTokens)? = nil
|
|
) throws -> DocumentSnapshot {
|
|
return try queue.sync {
|
|
guard let document = documents[uri] else {
|
|
throw Error.missingDocument(uri)
|
|
}
|
|
|
|
for edit in edits {
|
|
if let f = willEditDocument {
|
|
f(document.latestSnapshot, edit)
|
|
}
|
|
|
|
if let range = edit.range {
|
|
document.latestLineTable.replace(
|
|
fromLine: range.lowerBound.line,
|
|
utf16Offset: range.lowerBound.utf16index,
|
|
toLine: range.upperBound.line,
|
|
utf16Offset: range.upperBound.utf16index,
|
|
with: edit.text)
|
|
|
|
// Remove all tokens in the updated range and shift later ones.
|
|
let rangeAdjuster = RangeAdjuster(edit: edit)!
|
|
|
|
document.latestTokens.semantic = document.latestTokens.semantic.compactMap {
|
|
var token = $0
|
|
if let adjustedRange = rangeAdjuster.adjust(token.range) {
|
|
token.range = adjustedRange
|
|
return token
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
} else {
|
|
// Full text replacement.
|
|
document.latestLineTable = LineTable(edit.text)
|
|
document.latestTokens = DocumentTokens()
|
|
}
|
|
|
|
if let f = updateDocumentTokens {
|
|
document.latestTokens = f(document.latestSnapshot)
|
|
}
|
|
}
|
|
|
|
document.latestVersion = newVersion
|
|
return document.latestSnapshot
|
|
}
|
|
}
|
|
|
|
/// Updates the tokens in a document.
|
|
///
|
|
/// - parameter uri: The URI of the document to be updated
|
|
/// - parameter tokens: The new tokens for the document
|
|
@discardableResult
|
|
public func updateTokens(_ uri: DocumentURI, tokens: DocumentTokens) throws -> DocumentSnapshot {
|
|
return try queue.sync {
|
|
guard let document = documents[uri] else {
|
|
throw Error.missingDocument(uri)
|
|
}
|
|
|
|
document.latestTokens = tokens
|
|
|
|
return document.latestSnapshot
|
|
}
|
|
}
|
|
|
|
public func latestSnapshot(_ uri: DocumentURI) -> DocumentSnapshot? {
|
|
return queue.sync {
|
|
guard let document = documents[uri] else {
|
|
return nil
|
|
}
|
|
return document.latestSnapshot
|
|
}
|
|
}
|
|
}
|
|
|
|
extension DocumentManager {
|
|
|
|
// MARK: - LSP notification handling
|
|
|
|
/// Convenience wrapper for `open(_:language:version:text:)` that logs on failure.
|
|
@discardableResult
|
|
func open(_ note: DidOpenTextDocumentNotification) -> DocumentSnapshot? {
|
|
let doc = note.textDocument
|
|
return orLog("failed to open document", level: .error) {
|
|
try open(doc.uri, language: doc.language, version: doc.version, text: doc.text)
|
|
}
|
|
}
|
|
|
|
/// Convenience wrapper for `close(_:)` that logs on failure.
|
|
func close(_ note: DidCloseTextDocumentNotification) {
|
|
orLog("failed to close document", level: .error) {
|
|
try close(note.textDocument.uri)
|
|
}
|
|
}
|
|
|
|
/// Convenience wrapper for `edit(_:newVersion:edits:willEditDocument:updateDocumentTokens:)`
|
|
/// that logs on failure.
|
|
@discardableResult
|
|
func edit(
|
|
_ note: DidChangeTextDocumentNotification,
|
|
willEditDocument: ((_ before: DocumentSnapshot, TextDocumentContentChangeEvent) -> Void)? = nil,
|
|
updateDocumentTokens: ((_ after: DocumentSnapshot) -> DocumentTokens)? = nil
|
|
) -> DocumentSnapshot? {
|
|
return orLog("failed to edit document", level: .error) {
|
|
try edit(
|
|
note.textDocument.uri,
|
|
newVersion: note.textDocument.version,
|
|
edits: note.contentChanges,
|
|
willEditDocument: willEditDocument,
|
|
updateDocumentTokens: updateDocumentTokens
|
|
)
|
|
}
|
|
}
|
|
}
|