Skip to content

Commit d9a24ca

Browse files
author
Omar Tawfik
committed
Implement breakpoints and datatips
1 parent 5d1bd76 commit d9a24ca

File tree

5 files changed

+226
-71
lines changed

5 files changed

+226
-71
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
2+
3+
namespace Microsoft.VisualStudio.FSharp.Editor
4+
5+
open System
6+
open System.Composition
7+
open System.Collections.Concurrent
8+
open System.Collections.Generic
9+
open System.Threading
10+
open System.Threading.Tasks
11+
open System.Linq
12+
13+
open Microsoft.CodeAnalysis
14+
open Microsoft.CodeAnalysis.Classification
15+
open Microsoft.CodeAnalysis.Editor
16+
open Microsoft.CodeAnalysis.Editor.Implementation.Debugging
17+
open Microsoft.CodeAnalysis.Editor.Shared.Utilities
18+
open Microsoft.CodeAnalysis.Formatting
19+
open Microsoft.CodeAnalysis.Host.Mef
20+
open Microsoft.CodeAnalysis.Text
21+
22+
open Microsoft.VisualStudio.FSharp.LanguageService
23+
open Microsoft.VisualStudio.Text
24+
open Microsoft.VisualStudio.Text.Tagging
25+
26+
open Microsoft.FSharp.Compiler.Parser
27+
open Microsoft.FSharp.Compiler.SourceCodeServices
28+
open Microsoft.FSharp.Compiler.Range
29+
30+
[<Shared>]
31+
[<ExportLanguageService(typeof<IBreakpointResolutionService>, FSharpCommonConstants.FSharpLanguageName)>]
32+
type internal FSharpBreakpointResolutionService() =
33+
34+
static member GetBreakpointLocation(sourceText: SourceText, fileName: string, textSpan: TextSpan, options: FSharpProjectOptions) = async {
35+
let! parseResults = FSharpChecker.Instance.ParseFileInProject(fileName, sourceText.ToString(), options)
36+
let textLine = sourceText.Lines.GetLineFromPosition(textSpan.Start)
37+
38+
let textLineNumber = textLine.LineNumber + 1 // Roslyn line numbers are zero-based
39+
let textColumnNumber = textSpan.Start - textLine.Start
40+
41+
return parseResults.ValidateBreakpointLocation(mkPos textLineNumber textColumnNumber)
42+
}
43+
44+
interface IBreakpointResolutionService with
45+
member this.ResolveBreakpointAsync(document: Document, textSpan: TextSpan, cancellationToken: CancellationToken): Task<BreakpointResolutionResult> =
46+
let computation = async {
47+
let options = CommonRoslynHelpers.GetFSharpProjectOptionsForRoslynProject(document.Project)
48+
let! sourceText = document.GetTextAsync(cancellationToken) |> Async.AwaitTask
49+
let! location = FSharpBreakpointResolutionService.GetBreakpointLocation(sourceText, document.Name, textSpan, options)
50+
51+
return match location with
52+
| None -> null
53+
| Some(range) -> BreakpointResolutionResult.CreateSpanResult(document, CommonRoslynHelpers.FSharpRangeToTextSpan(sourceText, range))
54+
}
55+
56+
Async.StartAsTask(computation, TaskCreationOptions.None, cancellationToken).ContinueWith(fun(task: Task<BreakpointResolutionResult>) ->
57+
if task.Status = TaskStatus.RanToCompletion then
58+
task.Result
59+
else
60+
Assert.Exception(task.Exception.GetBaseException())
61+
raise(task.Exception.GetBaseException())
62+
, cancellationToken)
63+
64+
// FSROSLYNTODO: enable placing breakpoints by when user suplies fully-qualified function names
65+
member this.ResolveBreakpointsAsync(_, _, _): Task<IEnumerable<BreakpointResolutionResult>> =
66+
Task.FromResult(Enumerable.Empty<BreakpointResolutionResult>())

vsintegration/src/FSharp.Editor/ColorizationService.fs

Lines changed: 73 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -38,38 +38,39 @@ type internal FSharpColorizationService() =
3838

3939
static let DataCache = ConditionalWeakTable<SourceText, SourceTextData>()
4040

41-
static let compilerTokenToRoslynToken(colorKind: FSharpTokenColorKind) : string =
42-
match colorKind with
43-
| FSharpTokenColorKind.Comment -> ClassificationTypeNames.Comment
44-
| FSharpTokenColorKind.Identifier -> ClassificationTypeNames.Identifier
45-
| FSharpTokenColorKind.Keyword -> ClassificationTypeNames.Keyword
46-
| FSharpTokenColorKind.String -> ClassificationTypeNames.StringLiteral
47-
| FSharpTokenColorKind.Text -> ClassificationTypeNames.Text
48-
| FSharpTokenColorKind.UpperIdentifier -> ClassificationTypeNames.Identifier
49-
| FSharpTokenColorKind.Number -> ClassificationTypeNames.NumericLiteral
50-
| FSharpTokenColorKind.InactiveCode -> ClassificationTypeNames.ExcludedCode
51-
| FSharpTokenColorKind.PreprocessorKeyword -> ClassificationTypeNames.PreprocessorKeyword
52-
| FSharpTokenColorKind.Operator -> ClassificationTypeNames.Operator
53-
| FSharpTokenColorKind.TypeName -> ClassificationTypeNames.ClassName
54-
| FSharpTokenColorKind.Default | _ -> ClassificationTypeNames.Text
55-
56-
static let scanAndColorNextToken(lineTokenizer: FSharpLineTokenizer, colorMap: string[], lexState: Ref<FSharpTokenizerLexState>) : Option<FSharpTokenInfo> =
57-
let tokenInfoOption, nextLexState = lineTokenizer.ScanToken(lexState.Value)
58-
lexState.Value <- nextLexState
59-
if tokenInfoOption.IsSome then
60-
let classificationType = compilerTokenToRoslynToken(tokenInfoOption.Value.ColorClass)
61-
for i = tokenInfoOption.Value.LeftColumn to tokenInfoOption.Value.RightColumn do
62-
Array.set colorMap i classificationType
63-
tokenInfoOption
64-
6541
static let scanSourceLine(sourceTokenizer: FSharpSourceTokenizer, textLine: TextLine, lineContents: string, lexState: FSharpTokenizerLexState) : SourceLineData =
42+
6643
let colorMap = Array.create textLine.Span.Length ClassificationTypeNames.Text
6744
let lineTokenizer = sourceTokenizer.CreateLineTokenizer(lineContents)
6845

46+
let compilerTokenToRoslynToken(colorKind: FSharpTokenColorKind) : string =
47+
match colorKind with
48+
| FSharpTokenColorKind.Comment -> ClassificationTypeNames.Comment
49+
| FSharpTokenColorKind.Identifier -> ClassificationTypeNames.Identifier
50+
| FSharpTokenColorKind.Keyword -> ClassificationTypeNames.Keyword
51+
| FSharpTokenColorKind.String -> ClassificationTypeNames.StringLiteral
52+
| FSharpTokenColorKind.Text -> ClassificationTypeNames.Text
53+
| FSharpTokenColorKind.UpperIdentifier -> ClassificationTypeNames.Identifier
54+
| FSharpTokenColorKind.Number -> ClassificationTypeNames.NumericLiteral
55+
| FSharpTokenColorKind.InactiveCode -> ClassificationTypeNames.ExcludedCode
56+
| FSharpTokenColorKind.PreprocessorKeyword -> ClassificationTypeNames.PreprocessorKeyword
57+
| FSharpTokenColorKind.Operator -> ClassificationTypeNames.Operator
58+
| FSharpTokenColorKind.TypeName -> ClassificationTypeNames.ClassName
59+
| FSharpTokenColorKind.Default | _ -> ClassificationTypeNames.Text
60+
61+
let scanAndColorNextToken(lineTokenizer: FSharpLineTokenizer, lexState: Ref<FSharpTokenizerLexState>) : Option<FSharpTokenInfo> =
62+
let tokenInfoOption, nextLexState = lineTokenizer.ScanToken(lexState.Value)
63+
lexState.Value <- nextLexState
64+
if tokenInfoOption.IsSome then
65+
let classificationType = compilerTokenToRoslynToken(tokenInfoOption.Value.ColorClass)
66+
for i = tokenInfoOption.Value.LeftColumn to tokenInfoOption.Value.RightColumn do
67+
Array.set colorMap i classificationType
68+
tokenInfoOption
69+
6970
let previousLextState = ref(lexState)
70-
let mutable tokenInfoOption = scanAndColorNextToken(lineTokenizer, colorMap, previousLextState)
71+
let mutable tokenInfoOption = scanAndColorNextToken(lineTokenizer, previousLextState)
7172
while tokenInfoOption.IsSome do
72-
tokenInfoOption <- scanAndColorNextToken(lineTokenizer, colorMap, previousLextState)
73+
tokenInfoOption <- scanAndColorNextToken(lineTokenizer, previousLextState)
7374

7475
let mutable startPosition = 0
7576
let mutable endPosition = startPosition
@@ -85,7 +86,48 @@ type internal FSharpColorizationService() =
8586
startPosition <- endPosition
8687

8788
SourceLineData(previousLextState.Value, lineContents.GetHashCode(), classifiedSpans)
88-
89+
90+
static member GetColorizationData(sourceText: SourceText, textSpan: TextSpan, fileName: Option<string>, defines: string list, cancellationToken: CancellationToken) : List<ClassifiedSpan> =
91+
try
92+
let sourceTokenizer = FSharpSourceTokenizer(defines, fileName)
93+
let sourceTextData = DataCache.GetValue(sourceText, fun key -> SourceTextData(key.Lines.Count))
94+
95+
let startLine = sourceText.Lines.GetLineFromPosition(textSpan.Start).LineNumber
96+
let endLine = sourceText.Lines.GetLineFromPosition(textSpan.End).LineNumber
97+
98+
// Get the last cached scanned line
99+
let mutable scanStartLine = startLine
100+
while scanStartLine > 0 && sourceTextData.Lines.[scanStartLine - 1].IsNone do
101+
scanStartLine <- scanStartLine - 1
102+
103+
let result = new List<ClassifiedSpan>()
104+
let mutable lexState = if scanStartLine = 0 then 0L else sourceTextData.Lines.[scanStartLine - 1].Value.LexStateAtEndOfLine
105+
106+
for i = scanStartLine to sourceText.Lines.Count - 1 do
107+
cancellationToken.ThrowIfCancellationRequested()
108+
109+
let textLine = sourceText.Lines.[i]
110+
let lineContents = textLine.Text.ToString(textLine.Span)
111+
let lineHashCode = lineContents.GetHashCode()
112+
113+
let mutable lineData = sourceTextData.Lines.[i]
114+
if lineData.IsNone || lineData.Value.HashCode <> lineHashCode then
115+
lineData <- Some(scanSourceLine(sourceTokenizer, textLine, lineContents, lexState))
116+
117+
lexState <- lineData.Value.LexStateAtEndOfLine
118+
sourceTextData.Lines.[i] <- lineData
119+
120+
if startLine <= i && i <= endLine then
121+
result.AddRange(lineData.Value.ClassifiedSpans |> Seq.filter(fun token ->
122+
textSpan.Contains(token.TextSpan.Start) ||
123+
textSpan.Contains(token.TextSpan.End - 1) ||
124+
(token.TextSpan.Start <= textSpan.Start && textSpan.End <= token.TextSpan.End)))
125+
126+
result
127+
with ex ->
128+
Assert.Exception(ex)
129+
reraise()
130+
89131
interface IEditorClassificationService with
90132

91133
member this.AddLexicalClassifications(text: SourceText, textSpan: TextSpan, result: List<ClassifiedSpan>, cancellationToken: CancellationToken) =
@@ -94,8 +136,11 @@ type internal FSharpColorizationService() =
94136
member this.AddSyntacticClassificationsAsync(document: Document, textSpan: TextSpan, result: List<ClassifiedSpan>, cancellationToken: CancellationToken) =
95137
document.GetTextAsync(cancellationToken).ContinueWith(
96138
fun (sourceTextTask: Task<SourceText>) ->
139+
let options = CommonRoslynHelpers.GetFSharpProjectOptionsForRoslynProject(document.Project)
140+
let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.Name, options.OtherOptions |> Seq.toList)
141+
97142
if sourceTextTask.Status = TaskStatus.RanToCompletion then
98-
result.AddRange(FSharpColorizationService.GetColorizationData(sourceTextTask.Result, textSpan, None, [], cancellationToken))
143+
result.AddRange(FSharpColorizationService.GetColorizationData(sourceTextTask.Result, textSpan, None, defines, cancellationToken))
99144
, cancellationToken)
100145

101146
// FSROSLYNTODO: Due to issue 12732 on Roslyn side, semantic classification is tied to C#/VB only.
@@ -130,44 +175,3 @@ type internal FSharpColorizationService() =
130175
tokens.First()
131176
else
132177
new ClassifiedSpan(ClassificationTypeNames.WhiteSpace, classifiedSpan.TextSpan)
133-
134-
static member GetColorizationData(sourceText: SourceText, textSpan: TextSpan, fileName: Option<string>, defines: string list, cancellationToken: CancellationToken) : List<ClassifiedSpan> =
135-
try
136-
let sourceTokenizer = FSharpSourceTokenizer(defines, fileName)
137-
let sourceTextData = DataCache.GetValue(sourceText, fun key -> SourceTextData(key.Lines.Count))
138-
139-
let startLine = sourceText.Lines.GetLineFromPosition(textSpan.Start).LineNumber
140-
let endLine = sourceText.Lines.GetLineFromPosition(textSpan.End).LineNumber
141-
142-
// Get the last cached scanned line
143-
let mutable scanStartLine = startLine
144-
while scanStartLine > 0 && sourceTextData.Lines.[scanStartLine - 1].IsNone do
145-
scanStartLine <- scanStartLine - 1
146-
147-
let result = new List<ClassifiedSpan>()
148-
let mutable lexState = if scanStartLine = 0 then 0L else sourceTextData.Lines.[scanStartLine - 1].Value.LexStateAtEndOfLine
149-
150-
for i = scanStartLine to sourceText.Lines.Count - 1 do
151-
cancellationToken.ThrowIfCancellationRequested()
152-
153-
let textLine = sourceText.Lines.[i]
154-
let lineContents = textLine.Text.ToString(textLine.Span)
155-
let lineHashCode = lineContents.GetHashCode()
156-
157-
let mutable lineData = sourceTextData.Lines.[i]
158-
if lineData.IsNone || lineData.Value.HashCode <> lineHashCode then
159-
lineData <- Some(scanSourceLine(sourceTokenizer, textLine, lineContents, lexState))
160-
161-
lexState <- lineData.Value.LexStateAtEndOfLine
162-
sourceTextData.Lines.[i] <- lineData
163-
164-
if startLine <= i && i<= endLine then
165-
result.AddRange(lineData.Value.ClassifiedSpans |> Seq.filter(fun token ->
166-
textSpan.Contains(token.TextSpan.Start) ||
167-
textSpan.Contains(token.TextSpan.End - 1) ||
168-
(token.TextSpan.Start <= textSpan.Start && textSpan.End <= token.TextSpan.End)))
169-
170-
result
171-
with ex ->
172-
Assert.Exception(ex)
173-
reraise()

vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
<Compile Include="ColorizationService.fs" />
3434
<Compile Include="BraceMatchingService.fs" />
3535
<Compile Include="IndentationService.fs" />
36+
<Compile Include="BreakpointResolutionService.fs" />
37+
<Compile Include="LanguageDebugInfoService.fs" />
3638
<Compile Include="ProjectSiteService.fs" />
3739
<Compile Include="ContentType.fs" />
3840
</ItemGroup>
@@ -110,7 +112,6 @@
110112
<Reference Include="Microsoft.VisualStudio.Shell.Interop.9.0.dll" />
111113
<Reference Include="Microsoft.VisualStudio.Shell.Interop.10.0.dll" />
112114
<Reference Include="Microsoft.VisualStudio.Shell.Interop.11.0.dll" />
113-
<Reference Include="Microsoft.VisualStudio.Shell.$(RoslynVSBinariesVersion)" />
114115
<Reference Include="Microsoft.VisualStudio.TextManager.Interop" />
115116
<Reference Include="Microsoft.CodeAnalysis">
116117
<HintPath>$(FSharpSourcesRoot)\..\packages\Microsoft.CodeAnalysis.Common.$(RoslynVersion)\lib\net45\Microsoft.CodeAnalysis.dll</HintPath>
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
2+
3+
namespace Microsoft.VisualStudio.FSharp.Editor
4+
5+
open System
6+
open System.Composition
7+
open System.Collections.Concurrent
8+
open System.Collections.Generic
9+
open System.Threading
10+
open System.Threading.Tasks
11+
open System.Linq
12+
13+
open Microsoft.CodeAnalysis
14+
open Microsoft.CodeAnalysis.Classification
15+
open Microsoft.CodeAnalysis.Editor
16+
open Microsoft.CodeAnalysis.Editor.Implementation.Debugging
17+
open Microsoft.CodeAnalysis.Editor.Shared.Utilities
18+
open Microsoft.CodeAnalysis.Formatting
19+
open Microsoft.CodeAnalysis.Host.Mef
20+
open Microsoft.CodeAnalysis.Text
21+
22+
open Microsoft.VisualStudio.FSharp.LanguageService
23+
open Microsoft.VisualStudio.Text
24+
open Microsoft.VisualStudio.Text.Tagging
25+
26+
open Microsoft.FSharp.Compiler.Parser
27+
open Microsoft.FSharp.Compiler.SourceCodeServices
28+
open Microsoft.FSharp.Compiler.Range
29+
30+
[<Shared>]
31+
[<ExportLanguageService(typeof<ILanguageDebugInfoService>, FSharpCommonConstants.FSharpLanguageName)>]
32+
type internal FSharpLanguageDebugInfoService() =
33+
34+
static member GetDataTipInformation(sourceText: SourceText, position: int, tokens: List<ClassifiedSpan>): DebugDataTipInfo =
35+
let tokenIndex = tokens |> Seq.tryFindIndex(fun t -> t.TextSpan.Contains(position))
36+
37+
if tokenIndex.IsNone then
38+
Unchecked.defaultof<DebugDataTipInfo>
39+
else
40+
let token = tokens.[tokenIndex.Value]
41+
42+
let constructLiteralDataTip() =
43+
new DebugDataTipInfo(token.TextSpan, sourceText.GetSubText(token.TextSpan).ToString())
44+
45+
let constructIdentifierDataTip() =
46+
let textLine = sourceText.Lines.GetLineFromPosition(position)
47+
match QuickParse.GetCompleteIdentifierIsland false (textLine.ToString()) (position - textLine.Start) with
48+
| None -> Unchecked.defaultof<DebugDataTipInfo>
49+
| Some(island, islandPosition, _) -> new DebugDataTipInfo(TextSpan.FromBounds(islandPosition, islandPosition + island.Length), island)
50+
51+
match token.ClassificationType with
52+
| ClassificationTypeNames.NumericLiteral -> constructLiteralDataTip()
53+
| ClassificationTypeNames.StringLiteral -> constructLiteralDataTip()
54+
| ClassificationTypeNames.Identifier -> constructIdentifierDataTip()
55+
| _ -> Unchecked.defaultof<DebugDataTipInfo>
56+
57+
58+
interface ILanguageDebugInfoService with
59+
60+
// FSROSLYNTODO: This is used to get function names in breakpoint window. It should return fully qualified function name and line offset from the start of the function.
61+
member this.GetLocationInfoAsync(_, _, _): Task<DebugLocationInfo> =
62+
Task.FromResult(Unchecked.defaultof<DebugLocationInfo>)
63+
64+
member this.GetDataTipInfoAsync(document: Document, position: int, cancellationToken: CancellationToken): Task<DebugDataTipInfo> =
65+
let computation = async {
66+
let options = CommonRoslynHelpers.GetFSharpProjectOptionsForRoslynProject(document.Project)
67+
let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.Name, options.OtherOptions |> Seq.toList)
68+
69+
let! sourceText = document.GetTextAsync(cancellationToken) |> Async.AwaitTask
70+
let textSpan = TextSpan.FromBounds(0, sourceText.Length)
71+
let tokens = FSharpColorizationService.GetColorizationData(sourceText, textSpan, Some(document.Name), defines, cancellationToken)
72+
73+
return FSharpLanguageDebugInfoService.GetDataTipInformation(sourceText, position, tokens)
74+
}
75+
76+
Async.StartAsTask(computation, TaskCreationOptions.None, cancellationToken).ContinueWith(fun(task: Task<DebugDataTipInfo>) ->
77+
if task.Status = TaskStatus.RanToCompletion then
78+
task.Result
79+
else
80+
Assert.Exception(task.Exception.GetBaseException())
81+
raise(task.Exception.GetBaseException())
82+
, cancellationToken)
83+

vsintegration/src/FSharp.LanguageService/CommonRoslynHelpers.fs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ open Microsoft.FSharp.Compiler.SourceCodeServices
99
open Microsoft.FSharp.Compiler.Range
1010

1111
module internal CommonRoslynHelpers =
12-
/// Create F# project options for a Roslyn project.
12+
13+
// Create F# project options for a Roslyn project.
1314
let rec GetFSharpProjectOptionsForRoslynProject(project: Project) : FSharpProjectOptions = {
1415
ProjectFileName = project.FilePath
1516
ProjectFileNames = project.Documents

0 commit comments

Comments
 (0)