Skip to content

Commit

Permalink
Add in support for range-based highlighting (#714)
Browse files Browse the repository at this point in the history
  • Loading branch information
baronfel authored Feb 4, 2021
1 parent ab33c8d commit 7891522
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 59 deletions.
9 changes: 9 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
### 0.42.0 - 03.02.2021

* Many large changes, .Net 5 is required now
* Support for LSP semantic highlighting
* Fantomas upgrade to 4.4.0-beta-003
* FCS 38.0.2 upgrade
* Use Ionide.ProjInfo for the project system instead of the oen built into this repo
* Use local hosted msbuild to crack projects instead of managing builds ourselves

#### 0.41.1 - 23.03.2020

* Fix `PublishDiagnosticsCapabilities` type [#574](https://github.com/fsharp/FsAutoComplete/pull/574) by [@Gastove](https://github.com/Gastove)
Expand Down
14 changes: 5 additions & 9 deletions build.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,17 +116,13 @@ Target.create "ReleaseGitHub" (fun _ ->
Git.Branches.pushTag "" remote release.NugetVersion

let client =
let user =
match Environment.environVarOrNone "github-user" with
let token =
match Environment.environVarOrNone "github-token" with
| Some s when not (String.isNullOrWhiteSpace s) -> s
| _ -> UserInput.getUserInput "Username: "
let pw =
match Environment.environVarOrNone "github-pw" with
| Some s when not (String.isNullOrWhiteSpace s) -> s
| _ -> UserInput.getUserPassword "Password: "
| _ -> UserInput.getUserInput "Token: "

GitHub.createClientWithToken token

// Git.createClient user pw
GitHub.createClient user pw
let files = !! (pkgsDir </> "*.*")

let notes =
Expand Down
7 changes: 5 additions & 2 deletions src/FsAutoComplete.Core/Commands.fs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ type Commands (serialize : Serializer, backgroundServiceEnabled, toolsPath) =
lastSegment
|]

// TODO: LSP technically does now know how to handle overlapping, nested and multiline ranges, but
// as of 3 February 2021 there are no good examples of this that I've found, so we still do this
/// because LSP doesn't know how to handle overlapping/nested ranges, we have to dedupe them here
let scrubRanges (highlights: struct(range * _) array): struct(range * _) array =
let startToken = fun (struct(m: range, _)) -> m.Start.Line, m.Start.Column
Expand Down Expand Up @@ -1142,13 +1144,14 @@ type Commands (serialize : Serializer, backgroundServiceEnabled, toolsPath) =
}
|> AsyncResult.foldResult Some (fun _ -> None)

member x.GetHighlighting (file: string<LocalPath>) =
/// gets the semantic classification ranges for a file, optionally filtered by a given range.
member x.GetHighlighting (file: string<LocalPath>, range: range option) =
async {
let! res = x.TryGetLatestTypeCheckResultsForFile file
let res =
match res with
| Some res ->
let r = res.GetCheckResults.GetSemanticClassification(None)
let r = res.GetCheckResults.GetSemanticClassification(range)
let filteredRanges = scrubRanges r
Some filteredRanges
| None ->
Expand Down
19 changes: 14 additions & 5 deletions src/FsAutoComplete/FsAutoComplete.Lsp.fs
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,7 @@ type FsharpLspServer(commands: Commands, lspClient: FSharpLspClient) =
SelectionRangeProvider = Some true
SemanticTokensProvider = Some {
Legend = createTokenLegend<ClassificationUtils.SemanticTokenTypes, ClassificationUtils.SemanticTokenModifier>
Range = None
Range = Some (U2.First true)
Full = Some (U2.First true)
}
}
Expand Down Expand Up @@ -1781,11 +1781,9 @@ type FsharpLspServer(commands: Commands, lspClient: FSharpLspClient) =
return LspResult.success ()
}

override __.TextDocumentSemanticTokensFull (p: SemanticTokensParams): AsyncLspResult<SemanticTokens option> = asyncResult {
logger.info (Log.setMessage "Semantic highlighing request: {parms}" >> Log.addContextDestructured "parms" p )
let fn = p.TextDocument.GetFilePath() |> Utils.normalizePath

match! commands.GetHighlighting fn |> AsyncResult.ofCoreResponse with
member private x.handleSemanticTokens(getTokens: Async<CoreResponse<option<(struct(FSharp.Compiler.Range.range * SemanticClassificationType)) array>>>): AsyncLspResult<SemanticTokens option> = asyncResult {
match! getTokens |> AsyncResult.ofCoreResponse with
| None ->
return! LspResult.internalError "No highlights found"
| Some rangesAndHighlights ->
Expand All @@ -1802,6 +1800,17 @@ type FsharpLspServer(commands: Commands, lspClient: FSharpLspClient) =
return! success (Some { Data = encoded; ResultId = None }) // TODO: provide a resultId when we support delta ranges
}

override x.TextDocumentSemanticTokensFull (p: SemanticTokensParams): AsyncLspResult<SemanticTokens option> =
logger.info (Log.setMessage "Semantic highlighing request: {parms}" >> Log.addContextDestructured "parms" p )
let fn = p.TextDocument.GetFilePath() |> Utils.normalizePath
x.handleSemanticTokens(commands.GetHighlighting(fn, None))

override x.TextDocumentSemanticTokensRange (p: SemanticTokensRangeParams): AsyncLspResult<SemanticTokens option> =
logger.info (Log.setMessage "Semantic highlighing range request: {parms}" >> Log.addContextDestructured "parms" p )
let fn = p.TextDocument.GetFilePath() |> Utils.normalizePath
let fcsRange = protocolRangeToRange (UMX.untag fn) p.Range
x.handleSemanticTokens(commands.GetHighlighting(fn, Some fcsRange))

member __.ScriptFileProjectOptions = commands.ScriptFileProjectOptions

member __.FSharpLiterate (p: FSharpLiterateRequest) = async {
Expand Down
105 changes: 64 additions & 41 deletions test/FsAutoComplete.Tests.Lsp/CoreTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -923,47 +923,49 @@ let tooltipTests toolsPath =


let highlightingTests toolsPath =
let serverStart = lazy (
let path = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "HighlightingTest")
let (server, event) = serverInitialize path defaultConfigDto toolsPath
let path = Path.Combine(path, "Script.fsx")
let tdop : DidOpenTextDocumentParams = { TextDocument = loadDocument path}
let testPath = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "HighlightingTest")
let scriptPath = Path.Combine(testPath, "Script.fsx")

let serverParsed = lazy (
let (server, event) = serverInitialize testPath defaultConfigDto toolsPath
let tdop : DidOpenTextDocumentParams = { TextDocument = loadDocument scriptPath }

do server.TextDocumentDidOpen tdop |> Async.RunSynchronously
match waitForParseResultsForFile "Script.fsx" event with
| Ok () -> ()
| Ok () -> server
| Error e -> failwithf "Errors while parsing highlighting script %A" e
)

let decodeHighlighting (data: uint32 []) =
let zeroLine = [| 0u; 0u; 0u; 0u; 0u |]

let lines =
Array.append [| zeroLine |] (Array.chunkBySize 5 data)

let structures =
let mutable lastLine = 0
let mutable lastCol = 0
lines
|> Array.map (fun current ->
let startLine = lastLine + int current.[0]
let startCol = if current.[0] = 0u then lastCol + int current.[1] else int current.[1]
let endLine = int startLine // assuming no multiline for now
let endCol = startCol + int current.[2]
lastLine <- startLine
lastCol <- startCol
let tokenType = enum<ClassificationUtils.SemanticTokenTypes> (int current.[3])
let tokenMods = enum<ClassificationUtils.SemanticTokenModifier> (int current.[4])
let range =
{ Start = { Line = startLine; Character = startCol }
End = { Line = endLine; Character = endCol }}
range, tokenType, tokenMods
)

structures


let p : SemanticTokensParams = { TextDocument = { Uri = Path.FilePathToUri path } }
let highlights = server.TextDocumentSemanticTokensFull p |> Async.RunSynchronously
let decodeHighlighting (data: uint32 []) =
let zeroLine = [| 0u; 0u; 0u; 0u; 0u |]

let lines =
Array.append [| zeroLine |] (Array.chunkBySize 5 data)

let structures =
let mutable lastLine = 0
let mutable lastCol = 0
lines
|> Array.map (fun current ->
let startLine = lastLine + int current.[0]
let startCol = if current.[0] = 0u then lastCol + int current.[1] else int current.[1]
let endLine = int startLine // assuming no multiline for now
let endCol = startCol + int current.[2]
lastLine <- startLine
lastCol <- startCol
let tokenType = enum<ClassificationUtils.SemanticTokenTypes> (int current.[3])
let tokenMods = enum<ClassificationUtils.SemanticTokenModifier> (int current.[4])
let range =
{ Start = { Line = startLine; Character = startCol }
End = { Line = endLine; Character = endCol }}
range, tokenType, tokenMods
)

structures

let fullHighlights = lazy (
let p : SemanticTokensParams = { TextDocument = { Uri = Path.FilePathToUri scriptPath } }
let highlights = serverParsed.Value.TextDocumentSemanticTokensFull p |> Async.RunSynchronously
match highlights with
| Ok (Some highlights) ->
let decoded =
Expand Down Expand Up @@ -994,12 +996,33 @@ let highlightingTests toolsPath =
"Could not find a highlighting range that contained the given position"
)

/// this tests the range endpoint by getting highlighting for a range then doing the normal highlighting test
let tokenIsOfTypeInRange ((startLine, startChar), (endLine, endChar)) ((line, char)) testTokenType (server: FsAutoComplete.Lsp.FsharpLspServer Lazy) =
testCase $"can find token of type {testTokenType} in a subrange from ({startLine}, {startChar})-({endLine}, {endChar})" (fun () ->
let range: Types.Range =
{ Start = { Line = startLine; Character = startChar}
End = { Line = endLine; Character = endChar }}
let pos = { Line = line; Character = char }
match server.Value.TextDocumentSemanticTokensRange { Range = range; TextDocument = { Uri = Path.FilePathToUri scriptPath } } |> Async.RunSynchronously with
| Ok (Some highlights) ->
let decoded = decodeHighlighting highlights.Data
Expect.exists
decoded
(fun (r, token, _modifiers) ->
rangeContainsRange r pos
&& token = testTokenType)
"Could not find a highlighting range that contained the given position"
| Ok None -> failwithf "Expected to get some highlighting"
| Error e -> failwithf "error of %A" e
)

testList "Document Highlighting Tests" [
tokenIsOfType (0, 29) ClassificationUtils.SemanticTokenTypes.TypeParameter serverStart // the `^a` type parameter in the SRTP constraint
tokenIsOfType (0, 44) ClassificationUtils.SemanticTokenTypes.Member serverStart // the `PeePee` member in the SRTP constraint
tokenIsOfType (3, 52) ClassificationUtils.SemanticTokenTypes.Type serverStart // the `string` type annotation in the PooPoo srtp member
tokenIsOfType (6, 21) ClassificationUtils.SemanticTokenTypes.EnumMember serverStart // the `PeePee` AP application in the `yeet` function definition
tokenIsOfType (14, 10) ClassificationUtils.SemanticTokenTypes.Type serverStart //the `SomeJson` type should be a type
tokenIsOfType (0, 29) ClassificationUtils.SemanticTokenTypes.TypeParameter fullHighlights // the `^a` type parameter in the SRTP constraint
tokenIsOfType (0, 44) ClassificationUtils.SemanticTokenTypes.Member fullHighlights // the `PeePee` member in the SRTP constraint
tokenIsOfType (3, 52) ClassificationUtils.SemanticTokenTypes.Type fullHighlights // the `string` type annotation in the PooPoo srtp member
tokenIsOfType (6, 21) ClassificationUtils.SemanticTokenTypes.EnumMember fullHighlights // the `PeePee` AP application in the `yeet` function definition
tokenIsOfType (14, 10) ClassificationUtils.SemanticTokenTypes.Type fullHighlights //the `SomeJson` type should be a type
tokenIsOfTypeInRange ((0, 0), (0, 100)) (0, 29) ClassificationUtils.SemanticTokenTypes.TypeParameter serverParsed
]

let signatureHelpTests toolsPath =
Expand Down
4 changes: 2 additions & 2 deletions test/FsAutoComplete.Tests.Lsp/Helpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ let clientCaps : ClientCapabilities =
{
DynamicRegistration = Some true
Requests = {
Range = None
Full = None
Range = Some (U2.First true)
Full = Some (U2.First true)
}
TokenTypes = [| |]
TokenModifiers = [| |]
Expand Down

0 comments on commit 7891522

Please sign in to comment.