To eliminate parsing differences between Windows and other platforms, the frontend now normalizes all CRLF line endings to LF, like [in Rust](https://github.com/rust-lang/rust/issues/62865). Effects: - This makes Lake hashes be faithful to what Lean sees (Lake already normalizes line endings before computing hashes). - Docstrings now have normalized line endings. In particular, this fixes `#guard_msgs` failing multiline tests for Windows users using CRLF. - Now strings don't have different lengths depending on the platform. Before this PR, the following theorem is true for LF and false for CRLF files. ```lean example : " ".length = 1 := rfl ``` Note: the normalization will take `\r\r\n` and turn it into `\r\n`. In the elaborator, we reject loose `\r`'s that appear in whitespace. Rust instead takes the approach of making the normalization routine fail. They do this so that there's no downstream confusion about any `\r\n` that appears. Implementation note: the LSP maintains its own copy of a source file that it updates when edit operations are applied. We are assuming that edit operations never split or join CRLFs. If this assumption is not correct, then the LSP copy of a source file can become slightly out of sync. If this is an issue, there is some discussion [here](https://github.com/leanprover/lean4/pull/3903#discussion_r1592930085).
195 lines
7 KiB
Text
195 lines
7 KiB
Text
/-
|
||
Copyright (c) 2020 Wojciech Nawrocki. All rights reserved.
|
||
Released under Apache 2.0 license as described in the file LICENSE.
|
||
|
||
Authors: Wojciech Nawrocki, Marc Huisinga
|
||
-/
|
||
prelude
|
||
import Init.System.Uri
|
||
import Lean.Data.Lsp.Communication
|
||
import Lean.Data.Lsp.Diagnostics
|
||
import Lean.Data.Lsp.Extra
|
||
import Lean.Data.Lsp.TextSync
|
||
import Lean.Server.InfoUtils
|
||
|
||
namespace IO
|
||
|
||
/-- Throws an `IO.userError`. -/
|
||
def throwServerError (err : String) : IO α :=
|
||
throw (userError err)
|
||
|
||
namespace FS.Stream
|
||
|
||
/--
|
||
Chains two streams by creating a new stream s.t. writing to it
|
||
just writes to `a` but reading from it also duplicates the read output
|
||
into `b`, c.f. `a | tee b` on Unix.
|
||
NB: if `a` is written to but this stream is never read from,
|
||
the output will *not* be duplicated. Use this if you only care
|
||
about the data that was actually read.
|
||
-/
|
||
def chainRight (a : Stream) (b : Stream) (flushEagerly : Bool := false) : Stream :=
|
||
{ a with
|
||
flush := a.flush *> b.flush
|
||
read := fun sz => do
|
||
let bs ← a.read sz
|
||
b.write bs
|
||
if flushEagerly then b.flush
|
||
pure bs
|
||
getLine := do
|
||
let ln ← a.getLine
|
||
b.putStr ln
|
||
if flushEagerly then b.flush
|
||
pure ln }
|
||
|
||
/-- Like `tee a | b` on Unix. See `chainOut`. -/
|
||
def chainLeft (a : Stream) (b : Stream) (flushEagerly : Bool := false) : Stream :=
|
||
{ b with
|
||
flush := a.flush *> b.flush
|
||
write := fun bs => do
|
||
a.write bs
|
||
if flushEagerly then a.flush
|
||
b.write bs
|
||
putStr := fun s => do
|
||
a.putStr s
|
||
if flushEagerly then a.flush
|
||
b.putStr s }
|
||
|
||
/-- Prefixes all written outputs with `pre`. -/
|
||
def withPrefix (a : Stream) (pre : String) : Stream :=
|
||
{ a with
|
||
write := fun bs => do
|
||
a.putStr pre
|
||
a.write bs
|
||
putStr := fun s =>
|
||
a.putStr (pre ++ s) }
|
||
|
||
end FS.Stream
|
||
end IO
|
||
|
||
namespace Lean.Server
|
||
|
||
/-- Meta-Data of a document. -/
|
||
structure DocumentMeta where
|
||
/-- URI where the document is located. -/
|
||
uri : Lsp.DocumentUri
|
||
/-- Version number of the document. Incremented whenever the document is edited. -/
|
||
version : Nat
|
||
/--
|
||
Current text of the document.
|
||
It is maintained such that it is normalized using `String.crlfToLf`, which preserves logical line/column numbers.
|
||
(Note: we assume that edit operations never split or merge `\r\n` line endings.) -/
|
||
text : FileMap
|
||
/--
|
||
Controls when dependencies of the document are built on `textDocument/didOpen` notifications.
|
||
-/
|
||
dependencyBuildMode : Lsp.DependencyBuildMode
|
||
deriving Inhabited
|
||
|
||
/-- Extracts an `InputContext` from `doc`. -/
|
||
def DocumentMeta.mkInputContext (doc : DocumentMeta) : Parser.InputContext where
|
||
input := doc.text.source
|
||
fileName := (System.Uri.fileUriToPath? doc.uri).getD doc.uri |>.toString
|
||
fileMap := doc.text
|
||
|
||
/--
|
||
Replaces the range `r` (using LSP UTF-16 positions) in `text` (using UTF-8 positions)
|
||
with `newText`.
|
||
-/
|
||
def replaceLspRange (text : FileMap) (r : Lsp.Range) (newText : String) : FileMap :=
|
||
let start := text.lspPosToUtf8Pos r.start
|
||
let «end» := text.lspPosToUtf8Pos r.«end»
|
||
let pre := text.source.extract 0 start
|
||
let post := text.source.extract «end» text.source.endPos
|
||
-- `pre` and `post` already have normalized line endings, so only `newText` needs its endings normalized.
|
||
-- Note: this assumes that editing never separates a `\r\n`.
|
||
-- If `pre` ends with `\r` and `newText` begins with `\n`, the result is potentially inaccurate.
|
||
-- If this is ever a problem, we could store a second unnormalized FileMap, edit it, and normalize it here.
|
||
(pre ++ newText.crlfToLf ++ post).toFileMap
|
||
|
||
open IO
|
||
|
||
/--
|
||
Duplicates an I/O stream to a log file `fName` in LEAN_SERVER_LOG_DIR
|
||
if that envvar is set.
|
||
-/
|
||
def maybeTee (fName : String) (isOut : Bool) (h : FS.Stream) : IO FS.Stream := do
|
||
match (← IO.getEnv "LEAN_SERVER_LOG_DIR") with
|
||
| none => pure h
|
||
| some logDir =>
|
||
IO.FS.createDirAll logDir
|
||
let hTee ← FS.Handle.mk (System.mkFilePath [logDir, fName]) FS.Mode.write
|
||
let hTee := FS.Stream.ofHandle hTee
|
||
pure $ if isOut then
|
||
hTee.chainLeft h true
|
||
else
|
||
h.chainRight hTee true
|
||
|
||
open Lsp
|
||
|
||
/-- Returns the document contents with the change applied. -/
|
||
def applyDocumentChange (oldText : FileMap) : (change : Lsp.TextDocumentContentChangeEvent) → FileMap
|
||
| TextDocumentContentChangeEvent.rangeChange (range : Range) (newText : String) =>
|
||
replaceLspRange oldText range newText
|
||
| TextDocumentContentChangeEvent.fullChange (newText : String) =>
|
||
newText.crlfToLf.toFileMap
|
||
|
||
/-- Returns the document contents with all changes applied. -/
|
||
def foldDocumentChanges (changes : Array Lsp.TextDocumentContentChangeEvent) (oldText : FileMap) : FileMap :=
|
||
changes.foldl applyDocumentChange oldText
|
||
|
||
/-- Constructs a `textDocument/publishDiagnostics` notification. -/
|
||
def mkPublishDiagnosticsNotification (m : DocumentMeta) (diagnostics : Array Lsp.Diagnostic) :
|
||
JsonRpc.Notification Lsp.PublishDiagnosticsParams where
|
||
method := "textDocument/publishDiagnostics"
|
||
param := {
|
||
uri := m.uri
|
||
version? := some m.version
|
||
diagnostics := diagnostics
|
||
}
|
||
|
||
/-- Constructs a `$/lean/fileProgress` notification. -/
|
||
def mkFileProgressNotification (m : DocumentMeta) (processing : Array LeanFileProgressProcessingInfo) :
|
||
JsonRpc.Notification Lsp.LeanFileProgressParams where
|
||
method := "$/lean/fileProgress"
|
||
param := {
|
||
textDocument := { uri := m.uri, version? := m.version }
|
||
processing
|
||
}
|
||
|
||
/-- Constructs a `$/lean/fileProgress` notification from the given position onwards. -/
|
||
def mkFileProgressAtPosNotification (m : DocumentMeta) (pos : String.Pos)
|
||
(kind : LeanFileProgressKind := LeanFileProgressKind.processing) :
|
||
JsonRpc.Notification Lsp.LeanFileProgressParams :=
|
||
mkFileProgressNotification m #[{ range := ⟨m.text.utf8PosToLspPos pos, m.text.utf8PosToLspPos m.text.source.endPos⟩, kind := kind }]
|
||
|
||
/-- Constructs a `$/lean/fileProgress` notification marking processing as done. -/
|
||
def mkFileProgressDoneNotification (m : DocumentMeta) : JsonRpc.Notification Lsp.LeanFileProgressParams :=
|
||
mkFileProgressNotification m #[]
|
||
|
||
-- TODO: should return a request ID (or task?) when we add response handling
|
||
def mkApplyWorkspaceEditRequest (params : ApplyWorkspaceEditParams) :
|
||
JsonRpc.Request ApplyWorkspaceEditParams :=
|
||
⟨"workspace/applyEdit", "workspace/applyEdit", params⟩
|
||
|
||
end Lean.Server
|
||
|
||
/--
|
||
Converts an UTF-8-based `String.range` in `text` to an equivalent LSP UTF-16-based `Lsp.Range`
|
||
in `text`.
|
||
-/
|
||
def String.Range.toLspRange (text : Lean.FileMap) (r : String.Range) : Lean.Lsp.Range :=
|
||
⟨text.utf8PosToLspPos r.start, text.utf8PosToLspPos r.stop⟩
|
||
|
||
open Lean in
|
||
/--
|
||
Attempts to find a module name in the roots denoted by `srcSearchPath` for `uri`.
|
||
Fails if `uri` is not a `file://` uri or if the given `uri` cannot be found in `srcSearchPath`.
|
||
-/
|
||
def System.SearchPath.searchModuleNameOfUri
|
||
(srcSearchPath : SearchPath)
|
||
(uri : Lsp.DocumentUri)
|
||
: IO (Option Name) := do
|
||
let some path := Uri.fileUriToPath? uri
|
||
| return none
|
||
searchModuleNameOfFileName path srcSearchPath
|