Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix edge cases that prevent completion #1181

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
96376d1
Tests passing
faldor20 Sep 17, 2023
9bb112a
Added support for whitespace in completion
faldor20 Sep 18, 2023
0b1920e
Addded ability to have whitespace before or after the dot and test.
faldor20 Sep 18, 2023
cfde1e0
finished cleanup and added support for monadic bind.
faldor20 Sep 20, 2023
f6a031c
removed pointless regex
faldor20 Sep 20, 2023
bfddc86
reversed implimentation
faldor20 Sep 20, 2023
f84be2f
fixed everything other than combinators
faldor20 Sep 21, 2023
1fce0d6
fixed formatting and removed unnecissary import
faldor20 Sep 21, 2023
4d720d0
Misc fixes, names, formatting, etc converted to Re syntax for regex, …
faldor20 Sep 25, 2023
59e82fa
removed old prefix_parser
faldor20 Sep 25, 2023
a4c690b
renamed regexes
faldor20 Oct 1, 2023
abb05c9
Removed unnecessary Option extension
faldor20 Oct 1, 2023
5b07aca
spelling
faldor20 Oct 1, 2023
96c104e
converted to forward regex for simplicity
faldor20 Oct 2, 2023
cbaa820
Added Long file for benchmarking and removed unused benchmarks
faldor20 Oct 9, 2023
1841030
Merge branch 'master-origin'
faldor20 Oct 9, 2023
26476a6
Initial code for new-e2e tests for completion
faldor20 Oct 9, 2023
766509c
Finished adding completion tests
faldor20 Oct 10, 2023
0fbea39
converted last few missed tests
faldor20 Oct 10, 2023
b27e3cc
formatting
faldor20 Oct 10, 2023
b746bd5
removed old tests
faldor20 Oct 10, 2023
e7008b9
added interface
faldor20 Oct 10, 2023
c4249a8
fix bad interface
faldor20 Oct 14, 2023
6aa39f3
Merge branch 'master-origin'
faldor20 Oct 21, 2023
bdaa47c
Trying to get benchmarks to build fine
faldor20 Oct 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ install: ## Install the packages on the system
lock: ## Generate the lock files
opam lock -y .

.PHONY: bench
bench: ##
dune exec ocaml-lsp-server/bench/ocaml_lsp_bench.exe --profile release


.PHONY: test-ocaml
test-ocaml: ## Run the unit tests
dune build @lsp/test/runtest @lsp-fiber/runtest @jsonrpc-fiber/runtest @ocaml-lsp-server/runtest
Expand Down
1 change: 1 addition & 0 deletions dune-project
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ possible and does not make any assumptions about IO.
(odoc-parser (and (>= 2.0.0) (< 2.3.0)))
(ppx_expect (and (>= v0.15.0) :with-test))
(ocamlformat (and :with-test (= 0.24.1)))
(ppx_bench (and :with-test (>= 0.16.0)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this for the tests?

(ocamlc-loc (>= 3.7.0))
(uutf (>= 1.0.2))
(pp (>= 1.1.2))
Expand Down
3 changes: 3 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@
ocamlPackages.utop
ocamlPackages.cinaps
ocamlPackages.ppx_yojson_conv
#benchmarking
ocamlPackages.ppx_bench
ocamlPackages.core_bench
]);
inputsFrom = [ fast.ocaml-lsp fast.jsonrpc fast.lsp ];
};
Expand Down
98 changes: 98 additions & 0 deletions ocaml-lsp-server/bench/documents.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
let document =
"let mem = ListLabels.mem\n\nlet _ = mem ~se" |> Merlin_kernel.Msource.make

let long_document_text =
{|let prefix_of_position ~short_path source position =
let open Prefix_parser in
match Msource.text source with
| "" -> ""
| text ->
let end_of_prefix =
let (`Offset index) = Msource.get_offset source position in
min (String.length text - 1) (index - 1)
in
let prefix_text =
let pos =
(*clamp the length of a line to process at 500 chars, this is just a
reasonable limit for regex performance*)
max 0 (end_of_prefix - 500)
in
String.sub text ~pos ~len:(end_of_prefix + 1 - pos)
(*because all whitespace is semantically the same we convert it all to
spaces for easier regex matching*)
|> String.rev_map ~f:(fun x -> if x = '\n' || x = '\t' then ' ' else x)
in

let reconstructed_prefix =
try_parse_with_regex prefix_text
|> Option.value ~default:""
|> String.rev_filter ~f:(fun x -> x <> ' ')
in

if short_path then
match String.split_on_char reconstructed_prefix ~sep:'.' |> List.last with
| Some s -> s
| None -> reconstructed_prefix
else reconstructed_prefix

let suffix_of_position source position =
match Msource.text source with
| "" -> ""
| text ->
let (`Offset index) = Msource.get_offset source position in
let len = String.length text in
if index >= len then ""
else
let from = index in
let len =
let ident_char = function
| 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '\'' | '_' -> true
| _ -> false
in
let until =
String.findi ~from text ~f:(fun c -> not (ident_char c))
|> Option.value ~default:len
in
until - from
in
String.sub text ~pos:from ~len

let reconstruct_ident source position =
let prefix = prefix_of_position ~short_path:false source position in
let suffix = suffix_of_position source position in
let ident = prefix ^ suffix in
Option.some_if (ident <> "") ident

let range_prefix (lsp_position : Position.t) prefix : Range.t =
let start =
let len = String.length prefix in
let character = lsp_position.character - len in
{ lsp_position with character }
in
{ Range.start; end_ = lsp_position }

let sortText_of_index idx = Printf.sprintf "%04d" idx

module Complete_by_prefix = struct
let completionItem_of_completion_entry idx
(entry : Query_protocol.Compl.entry) ~compl_params ~range ~deprecated =
let kind = completion_kind entry.kind in
let textEdit = `TextEdit { TextEdit.range; newText = entry.name } in
CompletionItem.create
~label:entry.name
?kind
~detail:entry.desc
?deprecated:(Option.some_if deprecated entry.deprecated)
(* Without this field the client is not forced to respect the order
provided by merlin. *)
~sortText:(sortText_of_index idx)
?data:compl_params
~textEdit
()

let dispatch_cmd ~prefix position pipeline =
let complete =
Query_protocol.Complete_prefix (prefix, position, [], false, true)
in
Query_commands.dispatch pipeline comp
|}
11 changes: 11 additions & 0 deletions ocaml-lsp-server/bench/dune
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
(executables
(names ocaml_lsp_bench)
(libraries
ocaml_lsp_server
core_unix.command_unix
merlin-lib.kernel
base
core
core_bench)
(preprocess
(pps ppx_bench)))
27 changes: 27 additions & 0 deletions ocaml-lsp-server/bench/ocaml_lsp_bench.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
open Ocaml_lsp_server
open Core
open Core_bench

let () =
let open Documents in
let long_document = long_document_text |> Merlin_kernel.Msource.make in
let position = `Logical (3, 15) in
let long_position = `Logical (92, 41) in
Command_unix.run
(Bench.make_command
[ Bench.Test.create ~name:"get_prefix" (fun _ ->
Testing.Compl.prefix_of_position
~short_path:false
document
position
|> ignore)
; Bench.Test.create ~name:"get_prefix_long" (fun _ ->
Testing.Compl.prefix_of_position
~short_path:false
long_document
long_position
|> ignore)
; Bench.Test.create ~name:"get_offset_long" (fun _ ->
Merlin_kernel.Msource.get_offset long_document long_position
|> ignore)
])
74 changes: 16 additions & 58 deletions ocaml-lsp-server/src/compl.ml
Original file line number Diff line number Diff line change
Expand Up @@ -26,77 +26,35 @@ let completion_kind kind : CompletionItemKind.t option =
| `Constructor -> Some Constructor
| `Type -> Some TypeParameter

(** @see <https://ocaml.org/manual/lex.html> reference *)
let prefix_of_position ~short_path source position =
let open Prefix_parser in
match Msource.text source with
| "" -> ""
| text ->
let from =
let end_of_prefix =
let (`Offset index) = Msource.get_offset source position in
min (String.length text - 1) (index - 1)
in
let pos =
let should_terminate = ref false in
let has_seen_dot = ref false in
let is_prefix_char c =
if !should_terminate then false
else
match c with
| 'a' .. 'z'
| 'A' .. 'Z'
| '0' .. '9'
| '\''
| '_'
(* Infix function characters *)
| '$'
| '&'
| '*'
| '+'
| '-'
| '/'
| '='
| '>'
| '@'
| '^'
| '!'
| '?'
| '%'
| '<'
| ':'
| '~'
| '#' -> true
| '`' ->
if !has_seen_dot then false
else (
should_terminate := true;
true)
| '.' ->
has_seen_dot := true;
not short_path
| _ -> false
in
String.rfindi text ~from ~f:(fun c -> not (is_prefix_char c))
(*clamp the length of a line to process at 500 chars, this is just a
reasonable limit for regex performance*)
max 0 (end_of_prefix - 500)
in
let pos =
match pos with
| None -> 0
| Some pos -> pos + 1

let reconstructed_prefix =
try_parse_with_regex ~pos ~len:(end_of_prefix + 1 - pos) text
|> Option.value ~default:""
(*We remove the whitespace because merlin expects no whitespace and it's
semantically meaningless*)
|> String.filter (fun x -> not (x = ' ' || x = '\n' || x = '\t'))
in
let len = from - pos + 1 in
let reconstructed_prefix = String.sub text ~pos ~len in
(* if we reconstructed [~f:ignore] or [?f:ignore], we should take only
[ignore], so: *)
if
String.is_prefix reconstructed_prefix ~prefix:"~"
|| String.is_prefix reconstructed_prefix ~prefix:"?"
then
match String.lsplit2 reconstructed_prefix ~on:':' with
| Some (_, s) -> s

if short_path then
match String.split_on_char reconstructed_prefix ~sep:'.' |> List.last with
| Some s -> s
| None -> reconstructed_prefix
else reconstructed_prefix

(** [suffix_of_position source position] computes the suffix of the identifier
after [position]. *)
let suffix_of_position source position =
match Msource.text source with
| "" -> ""
Expand Down
5 changes: 4 additions & 1 deletion ocaml-lsp-server/src/compl.mli
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ val resolve :
-> CompletionItem.t Fiber.t

(** [prefix_of_position ~short_path source position] computes prefix before
given [position].
given [position]. A prefix is essentially a piece of code that refers to one
thing eg a single infix operator "|>", a single reference to a function or
variable: "List.map" a keyword "let" etc If there is semantically irrelivent
whitespace it is removed eg "List. map"->"List.map"

@param short_path
determines whether we want full prefix or cut at ["."], e.g.
Expand Down
7 changes: 7 additions & 0 deletions ocaml-lsp-server/src/import.ml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ include struct
module String = struct
include String

(**Filters a string keeping any chars for which f returns true and
discarding those for which it returns false*)
let filter f s =
let buf = Buffer.create (String.length s) in
iter ~f:(fun c -> if f c then Buffer.add_char buf c) s;
Buffer.contents buf

let findi =
let rec loop s len ~f i =
if i >= len then None
Expand Down
1 change: 1 addition & 0 deletions ocaml-lsp-server/src/ocaml_lsp_server.ml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module Version = Version
module Diagnostics = Diagnostics
module Doc_to_md = Doc_to_md
module Diff = Diff
module Testing = Testing
open Fiber.O

let make_error = Jsonrpc.Response.Error.make
Expand Down
1 change: 1 addition & 0 deletions ocaml-lsp-server/src/ocaml_lsp_server.mli
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ val run : Lsp.Cli.Channel.t -> read_dot_merlin:bool -> unit -> unit
module Diagnostics = Diagnostics
module Version = Version
module Doc_to_md = Doc_to_md
module Testing = Testing
48 changes: 48 additions & 0 deletions ocaml-lsp-server/src/prefix_parser.ml
faldor20 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
open Re

(*Regex based parser*)
let whiteSpace = set "\n\t "

let name_char =
Re.alt [ rg 'a' 'z'; rg 'A' 'Z'; rg '0' '9'; char '_'; char '\'' ]

let name_with_dot =
Re.seq [ name_char; whiteSpace |> rep; char '.'; whiteSpace |> rep ]

let core_operator_str = {|$&*+-/=>@^||}

let operator = core_operator_str ^ {|~!?%<:.|}

let infix = set (operator ^ "#")

let name_or_label =
compile
(seq
[ alt [ set "~?``"; str "let%"; str "and%" ] |> opt
; alt [ name_char; name_with_dot ] |> rep1
; stop
])

(** matches let%lwt and let* style expressions. See
here:https://v2.ocaml.org/manual/bindingops.html *)
let monadic_bind =
compile
(seq
[ alt [ str "let"; str "and" ]
; alt [ infix |> rep1; seq [ name_char |> rep1; char '%' ] ]
; stop
])

let infix_operator = compile (seq [ infix |> rep1; stop ])

open Import

let try_parse_with_regex ?pos ?len text =
(*Attempt to match each of our possible prefix types, the order is important
because there is some overlap between the regexs*)
let matched =
List.find_map
[ name_or_label; monadic_bind; infix_operator ]
~f:(fun regex -> Re.exec_opt ?pos ?len regex text)
in
matched |> Option.map ~f:(fun x -> Group.get x 0)
4 changes: 4 additions & 0 deletions ocaml-lsp-server/src/prefix_parser.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(**Try's the parse the incoming string for a prefix. The string should be the
source code ending at the prefix position. pos and len set the range for the
regex to operate on*)
val try_parse_with_regex : ?pos:int -> ?len:int -> string -> string option
5 changes: 5 additions & 0 deletions ocaml-lsp-server/src/testing.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
(**WARNING: This is for internal use in testing only *)

module Compl = Compl
module Merlin_kernel = Merlin_kernel
module Prefix_parser = Prefix_parser
3 changes: 2 additions & 1 deletion ocaml-lsp-server/test/dune
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
(dirs :standard \ e2e)

(library
(modules ocaml_lsp_tests)
(modules ocaml_lsp_tests position_prefix_tests)
(name ocaml_lsp_tests)
(enabled_if
(>= %{ocaml_version} 4.08))
(inline_tests)
(libraries
stdune
ocaml_lsp_server
merlin-lib.kernel
lsp
yojson
;; This is because of the (implicit_transitive_deps false)
Expand Down
Loading