未验证 提交 9c6a78fd 编写于 作者: T Tomas Grosup 提交者: GitHub

VisualStudio :: Task List for F# (#14944)

*  Task List for F#
* Fixing a bug - cache miss on tokenization may need to process a lot more lines due to lex state. Applying cancellation to all operations using tokenization
上级 97c2d378
......@@ -50,7 +50,8 @@ type internal FSharpAddMissingFunKeywordCodeFixProvider [<ImportingConstructor>]
defines,
SymbolLookupKind.Greedy,
false,
false
false,
context.CancellationToken
)
let! intendedArgSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, intendedArgLexerSymbol.Range)
......
......@@ -75,7 +75,8 @@ type internal FSharpAddMissingRecToMutuallyRecFunctionsCodeFixProvider [<Importi
defines,
SymbolLookupKind.Greedy,
false,
false
false,
context.CancellationToken
)
let! funcNameSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, funcLexerSymbol.Range)
......
......@@ -113,7 +113,8 @@ type internal FSharpAddOpenCodeFixProvider [<ImportingConstructor>] (assemblyCon
defines,
SymbolLookupKind.Greedy,
false,
false
false,
context.CancellationToken
)
return
......
......@@ -209,7 +209,14 @@ type internal FSharpImplementInterfaceCodeFixProvider [<ImportingConstructor>] (
// Notice that context.Span doesn't return reliable ranges to find tokens at exact positions.
// That's why we tokenize the line and try to find the last successive identifier token
let tokens =
Tokenizer.tokenizeLine (context.Document.Id, sourceText, context.Span.Start, context.Document.FilePath, defines)
Tokenizer.tokenizeLine (
context.Document.Id,
sourceText,
context.Span.Start,
context.Document.FilePath,
defines,
context.CancellationToken
)
let startLeftColumn = context.Span.Start - textLine.Start
......@@ -245,7 +252,8 @@ type internal FSharpImplementInterfaceCodeFixProvider [<ImportingConstructor>] (
defines,
SymbolLookupKind.Greedy,
false,
false
false,
context.CancellationToken
)
let fcsTextLineNumber = textLine.LineNumber + 1
......
......@@ -308,6 +308,8 @@ type internal FSharpSignatureHelpProvider [<ImportingConstructor>] (serviceProvi
return adjustedColumnInSource
}
let! ct = Async.CancellationToken |> liftAsync
let! lexerSymbol =
Tokenizer.getSymbolAtPosition (
documentId,
......@@ -317,7 +319,8 @@ type internal FSharpSignatureHelpProvider [<ImportingConstructor>] (serviceProvi
defines,
SymbolLookupKind.Greedy,
false,
false
false,
ct
)
let! symbolUse =
......
......@@ -18,7 +18,7 @@
<InternalsVisibleTo Include="VisualFSharp.Salsa" />
</ItemGroup>
<ItemGroup>
<ItemGroup>
<EmbeddedResource Include="FSharp.Editor.resx">
<GenerateSource>true</GenerateSource>
<GeneratedModuleName>Microsoft.VisualStudio.FSharp.Editor.SR</GeneratedModuleName>
......@@ -73,6 +73,7 @@
<Compile Include="Diagnostics\UnusedDeclarationsAnalyzer.fs" />
<Compile Include="Diagnostics\UnusedOpensDiagnosticAnalyzer.fs" />
<Compile Include="DocComments\XMLDocumentation.fs" />
<Compile Include="TaskList\TaskListService.fs" />
<Compile Include="Completion\CompletionUtils.fs" />
<Compile Include="Completion\CompletionProvider.fs" />
<Compile Include="Completion\PathCompletionUtilities.fs" />
......
......@@ -32,7 +32,8 @@ type internal FSharpEditorFormattingService [<ImportingConstructor>] (settings:
checker: FSharpChecker,
indentStyle: FormattingOptions.IndentStyle,
parsingOptions: FSharpParsingOptions,
position: int
position: int,
cancellationToken
) =
// Logic for determining formatting changes:
// If first token on the current line is a closing brace,
......@@ -49,7 +50,7 @@ type internal FSharpEditorFormattingService [<ImportingConstructor>] (settings:
let defines = CompilerEnvironment.GetConditionalDefinesForEditing parsingOptions
let tokens =
Tokenizer.tokenizeLine (documentId, sourceText, line.Start, filePath, defines)
Tokenizer.tokenizeLine (documentId, sourceText, line.Start, filePath, defines, cancellationToken)
let! firstMeaningfulToken =
tokens
......@@ -192,7 +193,8 @@ type internal FSharpEditorFormattingService [<ImportingConstructor>] (settings:
document.GetFSharpChecker(),
indentStyle,
parsingOptions,
position
position,
cancellationToken
)
return textChange |> Option.toList |> toIList
......
......@@ -34,7 +34,7 @@ type internal FSharpIndentationService [<ImportingConstructor>] () =
let defines = CompilerEnvironment.GetConditionalDefinesForEditing parsingOptions
let tokens =
Tokenizer.tokenizeLine (documentId, sourceText, position, filePath, defines)
Tokenizer.tokenizeLine (documentId, sourceText, position, filePath, defines, CancellationToken.None)
tokens
|> Array.rev
......
......@@ -40,7 +40,8 @@ module internal SymbolHelpers =
defines,
SymbolLookupKind.Greedy,
false,
false
false,
cancellationToken
)
let! symbolUse =
......
......@@ -39,6 +39,7 @@ type internal LexerSymbolKind =
| String = 6
| Other = 7
| Keyword = 8
| Comment = 9
type internal LexerSymbol =
{
......@@ -362,6 +363,8 @@ module internal Tokenizer =
member token.IsOperator = (token.ColorClass = FSharpTokenColorKind.Operator)
member token.IsPunctuation = (token.ColorClass = FSharpTokenColorKind.Punctuation)
member token.IsString = (token.ColorClass = FSharpTokenColorKind.String)
member token.IsComment = (token.ColorClass = FSharpTokenColorKind.Comment)
member token.IsKeyword = (token.ColorClass = FSharpTokenColorKind.Keyword)
/// This is the information we save for each token in a line for each active document.
/// It is a memory-critical data structure - do not make larger. This used to be ~100 bytes class, is now 8-byte struct
......@@ -398,18 +401,13 @@ module internal Tokenizer =
static member inline Create(token: FSharpTokenInfo) =
let kind =
if token.IsOperator then
LexerSymbolKind.Operator
elif token.IsIdentifier then
LexerSymbolKind.Ident
elif token.IsPunctuation then
LexerSymbolKind.Punctuation
elif token.IsString then
LexerSymbolKind.String
elif token.ColorClass = FSharpTokenColorKind.Keyword then
LexerSymbolKind.Keyword
else
LexerSymbolKind.Other
if token.IsOperator then LexerSymbolKind.Operator
elif token.IsIdentifier then LexerSymbolKind.Ident
elif token.IsPunctuation then LexerSymbolKind.Punctuation
elif token.IsString then LexerSymbolKind.String
elif token.IsKeyword then LexerSymbolKind.Keyword
elif token.IsComment then LexerSymbolKind.Comment
else LexerSymbolKind.Other
Debug.Assert(uint32 token.Tag < 0xFFFFu)
Debug.Assert(uint32 kind < 0xFFu)
......@@ -629,45 +627,36 @@ module internal Tokenizer =
dict.TryAdd(defines, data) |> ignore
data
/// Generates a list of Classified Spans for tokens which undergo syntactic classification (i.e., are not typechecked).
let getClassifiedSpans
let private getFromRefreshedTokenCache
(
documentKey: DocumentId,
sourceText: SourceText,
textSpan: TextSpan,
fileName: string option,
defines: string list,
cancellationToken: CancellationToken
) : List<ClassifiedSpan> =
try
let sourceTokenizer = FSharpSourceTokenizer(defines, fileName)
let lines = sourceText.Lines
let sourceTextData = getSourceTextData (documentKey, defines, lines.Count)
let startLine = lines.GetLineFromPosition(textSpan.Start).LineNumber
let endLine = lines.GetLineFromPosition(textSpan.End).LineNumber
lines: TextLineCollection,
startLine: int,
endLine: int,
sourceTokenizer: FSharpSourceTokenizer,
sourceTextDataCache: SourceTextData,
ct: CancellationToken
) =
[
// Go backwards to find the last cached scanned line that is valid
let scanStartLine =
let mutable i = startLine
while i > 0
&& (match sourceTextData.[i] with
&& (match sourceTextDataCache.[i] with
| Some data -> not (data.IsValid(lines.[i]))
| None -> true) do
i <- i - 1
i
// Rescan the lines if necessary and report the information
let result = new List<ClassifiedSpan>()
let mutable lexState =
if scanStartLine = 0 then
FSharpTokenizerLexState.Initial
else
sourceTextData.[scanStartLine - 1].Value.LexStateAtEndOfLine
sourceTextDataCache.[scanStartLine - 1].Value.LexStateAtEndOfLine
for i = scanStartLine to endLine do
cancellationToken.ThrowIfCancellationRequested()
ct.ThrowIfCancellationRequested()
let textLine = lines.[i]
let lineContents = textLine.Text.ToString(textLine.Span)
......@@ -676,39 +665,65 @@ module internal Tokenizer =
// 1. the line starts at the same overall position
// 2. the hash codes match
// 3. the start-of-line lex states are the same
match sourceTextData.[i] with
match sourceTextDataCache.[i] with
| Some data when data.IsValid(textLine) && data.LexStateAtStartOfLine.Equals(lexState) -> data
| _ ->
// Otherwise, we recompute
let newData = scanSourceLine (sourceTokenizer, textLine, lineContents, lexState)
sourceTextData.[i] <- Some newData
sourceTextDataCache.[i] <- Some newData
newData
lexState <- lineData.LexStateAtEndOfLine
if startLine <= i then
result.AddRange(
lineData.ClassifiedSpans
|> Array.filter (fun token ->
textSpan.Contains(token.TextSpan.Start)
|| textSpan.Contains(token.TextSpan.End - 1)
|| (token.TextSpan.Start <= textSpan.Start && textSpan.End <= token.TextSpan.End))
)
if i >= startLine then
yield lineData, lineContents
// If necessary, invalidate all subsequent lines after endLine
if endLine < lines.Count - 1 then
match sourceTextData.[endLine + 1] with
match sourceTextDataCache.[endLine + 1] with
| Some data ->
if not (data.LexStateAtStartOfLine.Equals(lexState)) then
sourceTextData.ClearFrom(endLine + 1)
sourceTextDataCache.ClearFrom(endLine + 1)
| None -> ()
]
/// Generates a list of Classified Spans for tokens which undergo syntactic classification (i.e., are not typechecked).
let getClassifiedSpans
(
documentKey: DocumentId,
sourceText: SourceText,
textSpan: TextSpan,
fileName: string option,
defines: string list,
cancellationToken: CancellationToken
) : ResizeArray<ClassifiedSpan> =
let result = new ResizeArray<ClassifiedSpan>()
try
let sourceTokenizer = FSharpSourceTokenizer(defines, fileName)
let lines = sourceText.Lines
let sourceTextData = getSourceTextData (documentKey, defines, lines.Count)
let startLine = lines.GetLineFromPosition(textSpan.Start).LineNumber
let endLine = lines.GetLineFromPosition(textSpan.End).LineNumber
let lineDataResults =
getFromRefreshedTokenCache (lines, startLine, endLine, sourceTokenizer, sourceTextData, cancellationToken)
for lineData, _ in lineDataResults do
result.AddRange(
lineData.ClassifiedSpans
|> Array.filter (fun token ->
textSpan.Contains(token.TextSpan.Start)
|| textSpan.Contains(token.TextSpan.End - 1)
|| (token.TextSpan.Start <= textSpan.Start && textSpan.End <= token.TextSpan.End))
)
result
with
| :? System.OperationCanceledException -> reraise ()
| ex ->
Assert.Exception(ex)
List<ClassifiedSpan>()
| ex -> Assert.Exception(ex)
result
/// Returns symbol at a given position.
let private getSymbolFromSavedTokens
......@@ -880,51 +895,27 @@ module internal Tokenizer =
sourceText: SourceText,
position: int,
fileName: string,
defines: string list
defines: string list,
cancellationToken
) =
let textLine = sourceText.Lines.GetLineFromPosition(position)
let textLinePos = sourceText.Lines.GetLinePosition(position)
let lineNumber = textLinePos.Line + 1 // FCS line number
let sourceTokenizer = FSharpSourceTokenizer(defines, Some fileName)
let lines = sourceText.Lines
// We keep incremental data per-document. When text changes we correlate text line-by-line (by hash codes of lines)
let sourceTextData = getSourceTextData (documentKey, defines, lines.Count)
// Go backwards to find the last cached scanned line that is valid
let scanStartLine =
let mutable i = min (lines.Count - 1) lineNumber
while i > 0
&& (match sourceTextData.[i] with
| Some data -> not (data.IsValid(lines.[i]))
| None -> true) do
i <- i - 1
i
let lexState =
if scanStartLine = 0 then
FSharpTokenizerLexState.Initial
else
sourceTextData.[scanStartLine - 1].Value.LexStateAtEndOfLine
let sourceTextData =
getSourceTextData (documentKey, defines, sourceText.Lines.Count)
let lineContents = textLine.Text.ToString(textLine.Span)
let lineNo = textLinePos.Line
// We can reuse the old data when
// 1. the line starts at the same overall position
// 2. the hash codes match
// 3. the start-of-line lex states are the same
match sourceTextData.[lineNumber] with
| Some data when data.IsValid(textLine) && data.LexStateAtStartOfLine = lexState -> data, textLinePos, lineContents
| _ ->
// Otherwise, we recompute
let newData = scanSourceLine (sourceTokenizer, textLine, lineContents, lexState)
sourceTextData.[lineNumber] <- Some newData
newData, textLinePos, lineContents
let lineData, contents =
getFromRefreshedTokenCache (sourceText.Lines, lineNo, lineNo, sourceTokenizer, sourceTextData, cancellationToken)
|> List.exactlyOne
lineData, textLinePos, contents
let tokenizeLine (documentKey, sourceText, position, fileName, defines) =
let tokenizeLine (documentKey, sourceText, position, fileName, defines, cancellationToken) =
try
let lineData, _, _ =
getCachedSourceLineData (documentKey, sourceText, position, fileName, defines)
getCachedSourceLineData (documentKey, sourceText, position, fileName, defines, cancellationToken)
lineData.SavedTokens
with ex ->
......@@ -940,12 +931,13 @@ module internal Tokenizer =
defines: string list,
lookupKind: SymbolLookupKind,
wholeActivePatterns: bool,
allowStringToken: bool
allowStringToken: bool,
cancellationToken
) : LexerSymbol option =
try
let lineData, textLinePos, lineContents =
getCachedSourceLineData (documentKey, sourceText, position, fileName, defines)
getCachedSourceLineData (documentKey, sourceText, position, fileName, defines, cancellationToken)
getSymbolFromSavedTokens (
fileName,
......
......@@ -256,7 +256,8 @@ type Document with
defines,
lookupKind,
wholeActivePattern,
allowStringToken
allowStringToken,
ct
)
}
......
// Microsoft.CodeAnalysis.ExternalAccess.FSharp.TaskList.FSharpTaskListService
namespace Microsoft.VisualStudio.FSharp.Editor
open System
open System.ComponentModel.Composition
open Microsoft.CodeAnalysis.Text
open FSharp.Compiler.CodeAnalysis
open System.Runtime.InteropServices
open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Editor
open Microsoft.CodeAnalysis.ExternalAccess.FSharp.TaskList
open FSharp.Compiler
open System.Collections.Immutable
open System.Diagnostics
[<Export(typeof<IFSharpTaskListService>)>]
type internal FSharpTaskListService [<ImportingConstructor>] () as this =
let getDefines (doc: Microsoft.CodeAnalysis.Document) =
asyncMaybe {
let! _, _, parsingOptions, _ =
doc.GetFSharpCompilationOptionsAsync(nameof (FSharpTaskListService))
|> liftAsync
return CompilerEnvironment.GetConditionalDefinesForEditing parsingOptions
}
|> Async.map (Option.defaultValue [])
let extractContractedComments (tokens: Tokenizer.SavedTokenInfo[]) =
let granularTokens =
tokens |> Array.filter (fun t -> t.Kind = LexerSymbolKind.Comment)
let contractedTokens =
([], granularTokens)
||> Array.fold (fun acc token ->
let token =
{|
Left = token.LeftColumn
Right = token.RightColumn
|}
match acc with
| [] -> [ token ]
| head :: tail when token.Left - head.Right <= 1 -> {| token with Left = head.Left |} :: tail
| _ -> token :: acc)
contractedTokens
member _.GetTaskListItems
(
doc: Microsoft.CodeAnalysis.Document,
sourceText: SourceText,
defines: string list,
descriptors: (string * FSharpTaskListDescriptor)[],
cancellationToken
) =
let foundTaskItems = ImmutableArray.CreateBuilder(initialCapacity = 0)
for line in sourceText.Lines do
let contractedTokens =
Tokenizer.tokenizeLine (doc.Id, sourceText, line.Span.Start, doc.FilePath, defines, cancellationToken)
|> extractContractedComments
for ct in contractedTokens do
let lineTxt = line.ToString()
let tokenSize = 1 + (ct.Right - ct.Left)
for (dText, d) in descriptors do
let idx =
lineTxt.IndexOf(dText, ct.Left, tokenSize, StringComparison.OrdinalIgnoreCase)
if idx > -1 then
let taskLength = 1 + ct.Right - idx
let idxAfterDesc = idx + dText.Length
// A descriptor followed by another letter is not a todocomment, like todoabc. But TODO, TODO2 or TODO: should be.
if idxAfterDesc >= lineTxt.Length || not (Char.IsLetter(lineTxt.[idxAfterDesc])) then
let taskText = lineTxt.Substring(idx, taskLength).TrimEnd([| '*'; ')' |])
let taskSpan = new TextSpan(line.Span.Start + idx, taskText.Length)
foundTaskItems.Add(new FSharpTaskListItem(d, taskText, doc, taskSpan))
foundTaskItems.ToImmutable()
interface IFSharpTaskListService with
member _.GetTaskListItemsAsync(doc, desc, cancellationToken) =
backgroundTask {
let descriptors = desc |> Seq.map (fun d -> d.Text, d) |> Array.ofSeq
let! sourceText = doc.GetTextAsync(cancellationToken)
let! defines = doc |> getDefines
return this.GetTaskListItems(doc, sourceText, defines, descriptors, cancellationToken)
}
......@@ -77,7 +77,8 @@ marker4"""
checker,
indentStyle,
parsingOptions,
position
position,
System.Threading.CancellationToken.None
)
|> Async.RunSynchronously
......
......@@ -28,6 +28,7 @@
<Compile Include="GoToDefinitionServiceTests.fs" />
<Compile Include="HelpContextServiceTests.fs" />
<Compile Include="QuickInfoTests.fs" />
<Compile Include="TaskListServiceTests.fs" />
<Compile Include="NavigateToSearchServiceTests.fs" />
<Compile Include="Hints\HintTestFramework.fs" />
<Compile Include="Hints\OptionParserTests.fs" />
......
......@@ -29,7 +29,8 @@ module GoToDefinitionServiceTests =
defines,
SymbolLookupKind.Greedy,
false,
false
false,
System.Threading.CancellationToken.None
)
let _, checkFileResults =
......
module FSharp.Editor.Tests.TaskListServiceTests
open System
open System.Threading
open Xunit
open Microsoft.CodeAnalysis
open Microsoft.VisualStudio.FSharp.Editor
open Microsoft.IO
open FSharp.Editor.Tests.Helpers
open Microsoft.CodeAnalysis.Text
let createDocument (fileContents: string) =
RoslynTestHelpers.CreateSolution(fileContents)
|> RoslynTestHelpers.GetSingleDocument
let private service =
new Microsoft.VisualStudio.FSharp.Editor.FSharpTaskListService()
let private ct = CancellationToken.None
let private descriptors =
[| "TODO"; "HACK" |] |> Array.map (fun s -> s, Unchecked.defaultof<_>)
let assertTasks expectedTasks fileContents =
let doc = createDocument fileContents
let sourceText = doc.GetTextAsync().Result
let t = service.GetTaskListItems(doc, sourceText, [], descriptors, ct)
let tasks = t |> Seq.map (fun t -> t.Message) |> List.ofSeq
Assert.Equal<string list>(expectedTasks |> List.sort, tasks |> List.sort)
[<Fact>]
let ``End of line comment is a task`` () =
assertTasks [ "TODO improve" ] "let x = 1 // TODO improve"
[<Fact>]
let ``Inline comments are tasks`` () =
assertTasks [ "TODO first "; "HACK second " ] "let x = 1 (* TODO first *) + 2 (* HACK second *)"
[<Fact>]
let ``ifdef code is not a task`` () =
"""
let x = 1
#if UNDEFINED_VAR
// TODO not here
#endif
"""
|> assertTasks []
[<Fact>]
let ``Multiline comment can have more tasks`` () =
"""
(* TODO first
TODO second
TODO third *)
"""
|> assertTasks [ "TODO first"; "TODO second"; "TODO third " ]
[<Fact>]
let ``Lowercase todo is still a task`` () =
assertTasks [ "todo improve" ] "let x = 1 // todo improve"
[<Fact>]
let ``Descriptor followed by letter is NOT a task`` () =
assertTasks [] "let x = 1 // hackathon solution"
[<Fact>]
let ``Descriptor followed by non-letter is OK`` () =
assertTasks [ "HACK2: using 1" ] "let x = 1 // HACK2: using 1"
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册