forked from nosami/visualfsharp
/
RoslynHelpers.fs
217 lines (188 loc) · 10.1 KB
/
RoslynHelpers.fs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
namespace Microsoft.VisualStudio.FSharp.Editor
open System
open System.Collections.Immutable
open System.Collections.Generic
open System.Threading
open System.Threading.Tasks
open Microsoft.CodeAnalysis
open Microsoft.CodeAnalysis.Text
open Microsoft.CodeAnalysis.Diagnostics
open FSharp.Compiler
open FSharp.Compiler.Layout
open FSharp.Compiler.SourceCodeServices
open FSharp.Compiler.Range
open Microsoft.VisualStudio.FSharp.Editor.Logging
open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Diagnostics
[<RequireQualifiedAccess>]
module internal RoslynHelpers =
let FSharpRangeToTextSpan(sourceText: SourceText, range: range) =
// Roslyn TextLineCollection is zero-based, F# range lines are one-based
let startPosition = sourceText.Lines.[max 0 (range.StartLine - 1)].Start + range.StartColumn
let endPosition = sourceText.Lines.[min (range.EndLine - 1) (sourceText.Lines.Count - 1)].Start + range.EndColumn
TextSpan(startPosition, endPosition - startPosition)
let TryFSharpRangeToTextSpan(sourceText: SourceText, range: range) : TextSpan option =
try Some(FSharpRangeToTextSpan(sourceText, range))
with e ->
//Assert.Exception(e)
None
let TextSpanToFSharpRange(fileName: string, textSpan: TextSpan, sourceText: SourceText) : range =
let startLine = sourceText.Lines.GetLineFromPosition textSpan.Start
let endLine = sourceText.Lines.GetLineFromPosition textSpan.End
mkRange
fileName
(Pos.fromZ startLine.LineNumber (textSpan.Start - startLine.Start))
(Pos.fromZ endLine.LineNumber (textSpan.End - endLine.Start))
let GetCompletedTaskResult(task: Task<'TResult>) =
if task.Status = TaskStatus.RanToCompletion then
task.Result
else
Assert.Exception(task.Exception.GetBaseException())
raise(task.Exception.GetBaseException())
/// maps from `LayoutTag` of the F# Compiler to Roslyn `TextTags` for use in tooltips
let roslynTag = function
| LayoutTag.ActivePatternCase
| LayoutTag.ActivePatternResult
| LayoutTag.UnionCase
| LayoutTag.Enum -> TextTags.Enum
| LayoutTag.Struct -> TextTags.Struct
| LayoutTag.TypeParameter -> TextTags.TypeParameter
| LayoutTag.Alias
| LayoutTag.Class
| LayoutTag.Union
| LayoutTag.Record
| LayoutTag.UnknownType // Default to class until/unless we use classification data
| LayoutTag.Module -> TextTags.Class
| LayoutTag.Interface -> TextTags.Interface
| LayoutTag.Keyword -> TextTags.Keyword
| LayoutTag.Member
| LayoutTag.Function
| LayoutTag.Method -> TextTags.Method
| LayoutTag.RecordField
| LayoutTag.Property -> TextTags.Property
| LayoutTag.Parameter // parameter?
| LayoutTag.Local -> TextTags.Local
| LayoutTag.Namespace -> TextTags.Namespace
| LayoutTag.Delegate -> TextTags.Delegate
| LayoutTag.Event -> TextTags.Event
| LayoutTag.Field -> TextTags.Field
| LayoutTag.LineBreak -> TextTags.LineBreak
| LayoutTag.Space -> TextTags.Space
| LayoutTag.NumericLiteral -> TextTags.NumericLiteral
| LayoutTag.Operator -> TextTags.Operator
| LayoutTag.StringLiteral -> TextTags.StringLiteral
| LayoutTag.Punctuation -> TextTags.Punctuation
| LayoutTag.Text
| LayoutTag.ModuleBinding // why no 'Identifier'? Does it matter?
| LayoutTag.UnknownEntity -> TextTags.Text
let CollectTaggedText (list: List<_>) (t:TaggedText) = list.Add(TaggedText(roslynTag t.Tag, t.Text))
type VolatileBarrier() =
[<VolatileField>]
let mutable isStopped = false
member __.Proceed = not isStopped
member __.Stop() = isStopped <- true
// This is like Async.StartAsTask, but
// 1. if cancellation occurs we explicitly associate the cancellation with cancellationToken
// 2. if exception occurs then set result to Unchecked.defaultof<_>, i.e. swallow exceptions
// and hope that Roslyn copes with the null
let StartAsyncAsTask (cancellationToken: CancellationToken) computation =
let tcs = new TaskCompletionSource<_>(TaskCreationOptions.None)
let barrier = VolatileBarrier()
let reg = cancellationToken.Register(fun _ -> if barrier.Proceed then tcs.TrySetCanceled(cancellationToken) |> ignore)
let task = tcs.Task
let disposeReg() = barrier.Stop(); if not task.IsCanceled then reg.Dispose()
Async.StartWithContinuations(
async { do! Async.SwitchToThreadPool()
return! computation },
continuation=(fun result ->
disposeReg()
tcs.TrySetResult(result) |> ignore
),
exceptionContinuation=(fun exn ->
disposeReg()
match exn with
| :? OperationCanceledException ->
tcs.TrySetCanceled(cancellationToken) |> ignore
| exn ->
System.Diagnostics.Trace.WriteLine("Visual F# Tools: exception swallowed and not passed to Roslyn: {0}", exn.Message)
let res = Unchecked.defaultof<_>
tcs.TrySetResult(res) |> ignore
),
cancellationContinuation=(fun _oce ->
disposeReg()
tcs.TrySetCanceled(cancellationToken) |> ignore
),
cancellationToken=cancellationToken)
task
let StartAsyncUnitAsTask cancellationToken (computation:Async<unit>) =
StartAsyncAsTask cancellationToken computation :> Task
let ConvertError(error: FSharpErrorInfo, location: Location) =
// Normalize the error message into the same format that we will receive it from the compiler.
// This ensures that IntelliSense and Compiler errors in the 'Error List' are de-duplicated.
// (i.e the same error does not appear twice, where the only difference is the line endings.)
let normalizedMessage = error.Message |> ErrorLogger.NormalizeErrorString |> ErrorLogger.NewlineifyErrorString
let id = "FS" + error.ErrorNumber.ToString("0000")
let emptyString = LocalizableString.op_Implicit("")
let description = LocalizableString.op_Implicit(normalizedMessage)
let severity = if error.Severity = FSharpErrorSeverity.Error then DiagnosticSeverity.Error else DiagnosticSeverity.Warning
let customTags =
match error.ErrorNumber with
| 1182 -> FSharpDiagnosticCustomTags.Unnecessary
| _ -> null
let descriptor = new DiagnosticDescriptor(id, emptyString, description, error.Subcategory, severity, true, emptyString, String.Empty, customTags)
Diagnostic.Create(descriptor, location)
let RangeToLocation (r: range, sourceText: SourceText, filePath: string) : Location =
let linePositionSpan = LinePositionSpan(LinePosition(Line.toZ r.StartLine, r.StartColumn), LinePosition(Line.toZ r.EndLine, r.EndColumn))
let textSpan = sourceText.Lines.GetTextSpan linePositionSpan
Location.Create(filePath, textSpan, linePositionSpan)
let StartAsyncSafe cancellationToken context computation =
let computation =
async {
try
return! computation
with e ->
logExceptionWithContext(e, context)
return Unchecked.defaultof<_>
}
Async.Start (computation, cancellationToken)
module internal OpenDeclarationHelper =
/// <summary>
/// Inserts open declaration into `SourceText`.
/// </summary>
/// <param name="sourceText">SourceText.</param>
/// <param name="ctx">Insertion context. Typically returned from tryGetInsertionContext</param>
/// <param name="ns">Namespace to open.</param>
let insertOpenDeclaration (sourceText: SourceText) (ctx: InsertContext) (ns: string) : SourceText * int =
let mutable minPos = None
let insert line lineStr (sourceText: SourceText) : SourceText =
let ln = sourceText.Lines.[line]
let pos = ln.Start
minPos <- match minPos with None -> Some pos | Some oldPos -> Some (min oldPos pos)
// find the line break characters on the previous line to use, Environment.NewLine should not be used
// as it makes assumptions on the line endings in the source.
let lineBreak = ln.Text.ToString(TextSpan(ln.End, ln.EndIncludingLineBreak - ln.End))
sourceText.WithChanges(TextChange(TextSpan(pos, 0), lineStr + lineBreak))
let getLineStr line = sourceText.Lines.[line].ToString().Trim()
let pos = ParsedInput.adjustInsertionPoint getLineStr ctx
let docLine = Line.toZ pos.Line
let lineStr = (String.replicate pos.Column " ") + "open " + ns
// If we're at the top of a file (e.g., F# script) then add a newline before adding the open declaration
let sourceText =
if docLine = 0 then
sourceText
|> insert docLine Environment.NewLine
|> insert docLine lineStr
else
sourceText |> insert docLine lineStr
// if there's no a blank line between open declaration block and the rest of the code, we add one
let sourceText =
if sourceText.Lines.[docLine + 1].ToString().Trim() <> "" then
sourceText |> insert (docLine + 1) ""
else sourceText
let sourceText =
// for top level module we add a blank line between the module declaration and first open statement
if (pos.Column = 0 || ctx.ScopeKind = ScopeKind.Namespace) && docLine > 0
&& not (sourceText.Lines.[docLine - 1].ToString().Trim().StartsWith "open") then
sourceText |> insert docLine ""
else sourceText
sourceText, minPos |> Option.defaultValue 0