summaryrefslogtreecommitdiff
path: root/support
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2021-05-23 03:00:39 +0000
committerNorbert Preining <norbert@preining.info>2021-05-23 03:00:39 +0000
commitf1261b349e875b842745b63258c3e338cb1fe3bf (patch)
treeb5d402b3e80818cde2c079a42249f3dcb9732247 /support
parent58aa1ac09b1d9e4769d0a0661cf12e2b2db41b14 (diff)
CTAN sync 202105230300
Diffstat (limited to 'support')
-rw-r--r--support/ltximg/README.md6
-rw-r--r--support/ltximg/ltximg-doc.pdfbin316588 -> 319369 bytes
-rw-r--r--support/ltximg/ltximg-doc.tex208
-rw-r--r--support/ltximg/ltximg.14
-rw-r--r--support/ltximg/ltximg.man1.pdfbin27866 -> 27866 bytes
-rw-r--r--support/ltximg/ltximg.pl25
-rw-r--r--support/texlab/CHANGELOG.md46
-rw-r--r--support/texlab/Cargo.lock1897
-rw-r--r--support/texlab/Cargo.toml125
-rw-r--r--support/texlab/README.md41
-rw-r--r--support/texlab/benches/bench_main.rs7
-rw-r--r--support/texlab/benches/benchmarks/completion.rs135
-rw-r--r--support/texlab/benches/benchmarks/mod.rs1
-rw-r--r--support/texlab/build.rs2
-rw-r--r--support/texlab/crates/jsonrpc/Cargo.toml20
-rw-r--r--support/texlab/crates/jsonrpc/src/client.rs77
-rw-r--r--support/texlab/crates/jsonrpc/src/lib.rs67
-rw-r--r--support/texlab/crates/jsonrpc/src/server.rs131
-rw-r--r--support/texlab/crates/jsonrpc/src/types.rs144
-rw-r--r--support/texlab/crates/jsonrpc_derive/Cargo.toml15
-rw-r--r--support/texlab/crates/jsonrpc_derive/src/lib.rs207
-rw-r--r--support/texlab/data/lang_data.json1122
-rw-r--r--support/texlab/docs/custom_messages.md102
-rw-r--r--support/texlab/docs/demo.gifbin0 -> 380190 bytes
-rw-r--r--support/texlab/docs/lsp_kinds.md31
-rw-r--r--support/texlab/docs/options.md145
-rw-r--r--support/texlab/docs/previewing.md217
-rw-r--r--support/texlab/docs/tectonic.md26
-rw-r--r--support/texlab/src/build.rs203
-rw-r--r--support/texlab/src/capabilities.rs (renamed from support/texlab/src/protocol/capabilities.rs)29
-rw-r--r--support/texlab/src/citation.rs (renamed from support/texlab/src/citeproc/mod.rs)138
-rw-r--r--support/texlab/src/citation/apa.csl (renamed from support/texlab/src/citeproc/apa.csl)0
-rw-r--r--support/texlab/src/citation/bibutils.rs (renamed from support/texlab/src/citeproc/bibutils.rs)9
-rw-r--r--support/texlab/src/citation/name.rs (renamed from support/texlab/src/citeproc/name/mod.rs)45
-rw-r--r--support/texlab/src/citation/name/parser.lalrpop (renamed from support/texlab/src/citeproc/name/parser.lalrpop)0
-rw-r--r--support/texlab/src/citation/ris.rs (renamed from support/texlab/src/citeproc/ris.rs)3
-rw-r--r--support/texlab/src/client.rs53
-rw-r--r--support/texlab/src/completion/bibtex/cmd.rs177
-rw-r--r--support/texlab/src/completion/bibtex/entry_type.rs164
-rw-r--r--support/texlab/src/completion/bibtex/field_name.rs190
-rw-r--r--support/texlab/src/completion/bibtex/mod.rs3
-rw-r--r--support/texlab/src/completion/latex/argument.rs215
-rw-r--r--support/texlab/src/completion/latex/begin_cmd.rs70
-rw-r--r--support/texlab/src/completion/latex/citation.rs261
-rw-r--r--support/texlab/src/completion/latex/color.rs91
-rw-r--r--support/texlab/src/completion/latex/color_model.rs101
-rw-r--r--support/texlab/src/completion/latex/combinators.rs143
-rw-r--r--support/texlab/src/completion/latex/component.rs351
-rw-r--r--support/texlab/src/completion/latex/glossary.rs114
-rw-r--r--support/texlab/src/completion/latex/import.rs188
-rw-r--r--support/texlab/src/completion/latex/include.rs133
-rw-r--r--support/texlab/src/completion/latex/label.rs230
-rw-r--r--support/texlab/src/completion/latex/mod.rs14
-rw-r--r--support/texlab/src/completion/latex/theorem.rs115
-rw-r--r--support/texlab/src/completion/latex/tikz_lib.rs138
-rw-r--r--support/texlab/src/completion/latex/user.rs204
-rw-r--r--support/texlab/src/completion/mod.rs419
-rw-r--r--support/texlab/src/completion/util.rs121
-rw-r--r--support/texlab/src/component_db.rs (renamed from support/texlab/src/components.rs)77
-rw-r--r--support/texlab/src/config.rs161
-rw-r--r--support/texlab/src/context.rs34
-rw-r--r--support/texlab/src/definition/bibtex_string.rs171
-rw-r--r--support/texlab/src/definition/latex_citation.rs118
-rw-r--r--support/texlab/src/definition/latex_cmd.rs154
-rw-r--r--support/texlab/src/definition/latex_label.rs155
-rw-r--r--support/texlab/src/definition/mod.rs47
-rw-r--r--support/texlab/src/diagnostics.rs55
-rw-r--r--support/texlab/src/diagnostics/bibtex.rs450
-rw-r--r--support/texlab/src/diagnostics/build_log.rs65
-rw-r--r--support/texlab/src/diagnostics/chktex.rs105
-rw-r--r--support/texlab/src/diagnostics/debouncer.rs77
-rw-r--r--support/texlab/src/diagnostics/latex.rs204
-rw-r--r--support/texlab/src/diagnostics/mod.rs28
-rw-r--r--support/texlab/src/dispatch.rs81
-rw-r--r--support/texlab/src/distro.rs74
-rw-r--r--support/texlab/src/distro/kpsewhich.rs74
-rw-r--r--support/texlab/src/distro/miktex.rs (renamed from support/texlab/src/tex/miktex.rs)51
-rw-r--r--support/texlab/src/distro/texlive.rs43
-rw-r--r--support/texlab/src/feature.rs405
-rw-r--r--support/texlab/src/features.rs272
-rw-r--r--support/texlab/src/features/build.rs133
-rw-r--r--support/texlab/src/features/completion.rs567
-rw-r--r--support/texlab/src/features/completion/acronym_ref.rs169
-rw-r--r--support/texlab/src/features/completion/argument.rs189
-rw-r--r--support/texlab/src/features/completion/begin_command.rs84
-rw-r--r--support/texlab/src/features/completion/citation.rs244
-rw-r--r--support/texlab/src/features/completion/color.rs122
-rw-r--r--support/texlab/src/features/completion/color_model.rs188
-rw-r--r--support/texlab/src/features/completion/component_command.rs177
-rw-r--r--support/texlab/src/features/completion/component_environment.rs174
-rw-r--r--support/texlab/src/features/completion/entry_type.rs231
-rw-r--r--support/texlab/src/features/completion/field.rs200
-rw-r--r--support/texlab/src/features/completion/glossary_ref.rs141
-rw-r--r--support/texlab/src/features/completion/import.rs196
-rw-r--r--support/texlab/src/features/completion/include.rs181
-rw-r--r--support/texlab/src/features/completion/label.rs196
-rw-r--r--support/texlab/src/features/completion/theorem.rs152
-rw-r--r--support/texlab/src/features/completion/tikz_library.rs169
-rw-r--r--support/texlab/src/features/completion/types.rs (renamed from support/texlab/src/completion/types.rs)142
-rw-r--r--support/texlab/src/features/completion/user_command.rs100
-rw-r--r--support/texlab/src/features/completion/user_environment.rs116
-rw-r--r--support/texlab/src/features/completion/util.rs66
-rw-r--r--support/texlab/src/features/cursor.rs252
-rw-r--r--support/texlab/src/features/definition.rs29
-rw-r--r--support/texlab/src/features/definition/command.rs131
-rw-r--r--support/texlab/src/features/definition/document.rs112
-rw-r--r--support/texlab/src/features/definition/entry.rs125
-rw-r--r--support/texlab/src/features/definition/label.rs80
-rw-r--r--support/texlab/src/features/definition/string.rs176
-rw-r--r--support/texlab/src/features/folding.rs237
-rw-r--r--support/texlab/src/features/formatting.rs24
-rw-r--r--support/texlab/src/features/formatting/bibtex_internal.rs381
-rw-r--r--support/texlab/src/features/formatting/latexindent.rs50
-rw-r--r--support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__command.snap8
-rw-r--r--support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__concatenation.snap8
-rw-r--r--support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__insert_braces.snap8
-rw-r--r--support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__multiple_entries.snap9
-rw-r--r--support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__parens.snap7
-rw-r--r--support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__preamble.snap6
-rw-r--r--support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__string.snap6
-rw-r--r--support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__trailing_comma.snap8
-rw-r--r--support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__wrap_long_lines.snap10
-rw-r--r--support/texlab/src/features/forward_search.rs124
-rw-r--r--support/texlab/src/features/highlight.rs15
-rw-r--r--support/texlab/src/features/highlight/label.rs167
-rw-r--r--support/texlab/src/features/hover.rs43
-rw-r--r--support/texlab/src/features/hover/citation.rs133
-rw-r--r--support/texlab/src/features/hover/component.rs105
-rw-r--r--support/texlab/src/features/hover/entry_type.rs128
-rw-r--r--support/texlab/src/features/hover/field.rs128
-rw-r--r--support/texlab/src/features/hover/label.rs79
-rw-r--r--support/texlab/src/features/hover/string_ref.rs141
-rw-r--r--support/texlab/src/features/link.rs115
-rw-r--r--support/texlab/src/features/lsp_kinds.rs (renamed from support/texlab/src/syntax/lsp_kind.rs)5
-rw-r--r--support/texlab/src/features/reference.rs25
-rw-r--r--support/texlab/src/features/reference/entry.rs198
-rw-r--r--support/texlab/src/features/reference/label.rs187
-rw-r--r--support/texlab/src/features/reference/string.rs206
-rw-r--r--support/texlab/src/features/rename.rs34
-rw-r--r--support/texlab/src/features/rename/command.rs98
-rw-r--r--support/texlab/src/features/rename/entry.rs161
-rw-r--r--support/texlab/src/features/rename/label.rs142
-rw-r--r--support/texlab/src/features/semantic.rs21
-rw-r--r--support/texlab/src/features/semantic/bibtex.rs289
-rw-r--r--support/texlab/src/features/semantic/legend.rs83
-rw-r--r--support/texlab/src/features/symbol.rs125
-rw-r--r--support/texlab/src/features/symbol/bibtex.rs88
-rw-r--r--support/texlab/src/features/symbol/latex.rs488
-rw-r--r--support/texlab/src/features/symbol/project_order.rs223
-rw-r--r--support/texlab/src/features/symbol/types.rs (renamed from support/texlab/src/symbol/types.rs)83
-rw-r--r--support/texlab/src/folding/bibtex_decl.rs159
-rw-r--r--support/texlab/src/folding/latex_env.rs80
-rw-r--r--support/texlab/src/folding/latex_section.rs111
-rw-r--r--support/texlab/src/folding/mod.rs45
-rw-r--r--support/texlab/src/forward_search.rs90
-rw-r--r--support/texlab/src/highlight/latex_label.rs109
-rw-r--r--support/texlab/src/highlight/mod.rs36
-rw-r--r--support/texlab/src/hover/bibtex/entry_type.rs116
-rw-r--r--support/texlab/src/hover/bibtex/field.rs111
-rw-r--r--support/texlab/src/hover/bibtex/mod.rs3
-rw-r--r--support/texlab/src/hover/bibtex/string_reference.rs165
-rw-r--r--support/texlab/src/hover/latex/citation.rs179
-rw-r--r--support/texlab/src/hover/latex/component.rs95
-rw-r--r--support/texlab/src/hover/latex/label.rs118
-rw-r--r--support/texlab/src/hover/latex/mod.rs6
-rw-r--r--support/texlab/src/hover/latex/preview.rs338
-rw-r--r--support/texlab/src/hover/mod.rs58
-rw-r--r--support/texlab/src/label.rs291
-rw-r--r--support/texlab/src/lang_data.rs67
-rw-r--r--support/texlab/src/language.rs33
-rw-r--r--support/texlab/src/lib.rs65
-rw-r--r--support/texlab/src/line_index.rs217
-rw-r--r--support/texlab/src/line_index_ext.rs42
-rw-r--r--support/texlab/src/link/latex_import.rs99
-rw-r--r--support/texlab/src/link/latex_include.rs101
-rw-r--r--support/texlab/src/link/mod.rs40
-rw-r--r--support/texlab/src/main.rs53
-rw-r--r--support/texlab/src/options.rs86
-rw-r--r--support/texlab/src/outline.rs437
-rw-r--r--support/texlab/src/protocol/client.rs27
-rw-r--r--support/texlab/src/protocol/codec.rs144
-rw-r--r--support/texlab/src/protocol/edit.rs12
-rw-r--r--support/texlab/src/protocol/mod.rs71
-rw-r--r--support/texlab/src/protocol/options.rs104
-rw-r--r--support/texlab/src/protocol/range.rs97
-rw-r--r--support/texlab/src/range.rs20
-rw-r--r--support/texlab/src/reference/bibtex_entry.rs218
-rw-r--r--support/texlab/src/reference/bibtex_string.rs253
-rw-r--r--support/texlab/src/reference/latex_label.rs211
-rw-r--r--support/texlab/src/reference/mod.rs45
-rw-r--r--support/texlab/src/rename/bibtex_entry.rs192
-rw-r--r--support/texlab/src/rename/latex_cmd.rs128
-rw-r--r--support/texlab/src/rename/latex_env.rs157
-rw-r--r--support/texlab/src/rename/latex_label.rs146
-rw-r--r--support/texlab/src/rename/mod.rs88
-rw-r--r--support/texlab/src/req_queue.rs13
-rw-r--r--support/texlab/src/server.rs1369
-rw-r--r--support/texlab/src/symbol/bibtex_entry.rs153
-rw-r--r--support/texlab/src/symbol/bibtex_string.rs102
-rw-r--r--support/texlab/src/symbol/latex_section/enumeration.rs79
-rw-r--r--support/texlab/src/symbol/latex_section/equation.rs41
-rw-r--r--support/texlab/src/symbol/latex_section/float.rs59
-rw-r--r--support/texlab/src/symbol/latex_section/mod.rs671
-rw-r--r--support/texlab/src/symbol/latex_section/theorem.rs70
-rw-r--r--support/texlab/src/symbol/mod.rs178
-rw-r--r--support/texlab/src/symbol/project_order.rs181
-rw-r--r--support/texlab/src/syntax.rs17
-rw-r--r--support/texlab/src/syntax/bibtex.rs34
-rw-r--r--support/texlab/src/syntax/bibtex/ast.rs431
-rw-r--r--support/texlab/src/syntax/bibtex/cst.rs212
-rw-r--r--support/texlab/src/syntax/bibtex/formatter.rs329
-rw-r--r--support/texlab/src/syntax/bibtex/kind.rs51
-rw-r--r--support/texlab/src/syntax/bibtex/lexer.rs215
-rw-r--r--support/texlab/src/syntax/bibtex/mod.rs396
-rw-r--r--support/texlab/src/syntax/bibtex/parser.rs706
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__empty.snap7
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_complete_parens.snap40
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_no_fields.snap13
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_no_fields_missing_comma.snap12
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field.snap27
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field_number_key.snap27
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field_trailing_comma.snap28
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_two_fields.snap42
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_two_fields_incomplete.snap33
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__junk.snap11
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_casing.snap21
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_complete.snap21
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_missing_end.snap20
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_casing.snap23
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_complete.snap23
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_concatenation.snap43
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_incomplete.snap23
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_missing_quote.snap23
-rw-r--r--support/texlab/src/syntax/build_log.rs (renamed from support/texlab/src/diagnostics/build.rs)694
-rw-r--r--support/texlab/src/syntax/generic_ast.rs51
-rw-r--r--support/texlab/src/syntax/lang_data.rs231
-rw-r--r--support/texlab/src/syntax/latex.rs36
-rw-r--r--support/texlab/src/syntax/latex/analysis.rs943
-rw-r--r--support/texlab/src/syntax/latex/analysis/command.rs12
-rw-r--r--support/texlab/src/syntax/latex/analysis/distro_file.rs71
-rw-r--r--support/texlab/src/syntax/latex/analysis/environment.rs11
-rw-r--r--support/texlab/src/syntax/latex/analysis/explicit_link.rs72
-rw-r--r--support/texlab/src/syntax/latex/analysis/implicit_link.rs41
-rw-r--r--support/texlab/src/syntax/latex/analysis/label_name.rs66
-rw-r--r--support/texlab/src/syntax/latex/analysis/label_number.rs22
-rw-r--r--support/texlab/src/syntax/latex/analysis/theorem.rs23
-rw-r--r--support/texlab/src/syntax/latex/analysis/types.rs73
-rw-r--r--support/texlab/src/syntax/latex/ast.rs414
-rw-r--r--support/texlab/src/syntax/latex/cst.rs639
-rw-r--r--support/texlab/src/syntax/latex/kind.rs180
-rw-r--r--support/texlab/src/syntax/latex/lexer.rs353
-rw-r--r--support/texlab/src/syntax/latex/mod.rs686
-rw-r--r--support/texlab/src/syntax/latex/parser.rs1809
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__command_with_parameter.snap47
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__command_with_star.snap23
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__comment.snap35
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__delimiters.snap79
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__empty.snap6
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__escape_sequence.snap19
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__formula.snap55
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__invalid_parameter.snap11
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acronym_declaration.snap49
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_definition_options.snap46
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_definition_simple.snap28
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_reference_options.snap29
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_reference_simple.snap14
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__biblatex_include_options.snap30
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__biblatex_include_simple.snap14
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__bibtex_include_simple.snap14
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__brace_group_missing_end.snap15
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__brace_group_simple.snap15
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption.snap26
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_error.snap26
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_figure.snap28
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_minimal.snap21
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_minimal_error.snap21
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_empty.snap13
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_missing_brace.snap14
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_multiple_keys.snap17
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_prenote.snap19
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_prenote_postnote.snap24
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_redundant_comma.snap16
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_simple.snap14
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_star.snap14
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__class_include_empty.snap13
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__class_include_options.snap37
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__class_include_simple.snap14
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_definition_simple.snap27
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_reference_simple.snap14
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error1.snap29
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error2.snap22
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error3.snap19
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error4.snap14
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_simple.snap32
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_no_argc.snap19
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_no_impl.snap15
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_no_impl_error.snap15
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_simple.snap24
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__empty.snap8
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_definition.snap38
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_nested.snap37
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_nested_missing_braces.snap42
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_simple.snap27
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation.snap17
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation_label_reference_simple.snap14
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation_missing_begin.snap30
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation_missing_end.snap31
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__escaped_brackets.snap19
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__generic_command_args.snap20
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__generic_command_empty.snap10
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__generic_command_escape.snap10
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__glossary_entry_definition_simple.snap33
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__glossary_entry_reference_options.snap32
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__glossary_entry_reference_simple.snap14
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__graphics_include_options.snap25
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__graphics_include_simple.snap14
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__hello_world.snap12
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__import_incomplete.snap15
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__import_simple.snap18
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inkscape_include_options.snap25
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inkscape_include_simple.snap14
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inline.snap26
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inline_double_dollar.snap26
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_definition_simple.snap14
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_number.snap23
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_multiple.snap17
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_range_error.snap18
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_range_incomplete.snap15
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_range_simple.snap18
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_simple.snap14
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__latex_include_simple.snap14
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__latex_input_simple.snap14
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__math_operator_no_impl.snap15
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__math_operator_simple.snap19
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_empty.snap13
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_multiple.snap17
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_options.snap37
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_simple.snap14
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__parameter.snap9
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__parameter_error.snap9
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__pgf_library_import_simple.snap14
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_enum_item.snap45
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_invalid_nesting.snap23
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_nested.snap57
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_siblings.snap29
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__svg_include_options.snap25
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__svg_include_simple.snap14
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_full.snap27
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_name_with_counter.snap19
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_name_with_description.snap19
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_name_with_description_and_counter.snap23
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_only_name.snap15
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__tikz_library_import_simple.snap14
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__unmatched_braces.snap13
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__unmatched_brackets.snap13
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__unmatched_brackets_with_group.snap16
-rw-r--r--support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__verbatim_include_simple.snap14
-rw-r--r--support/texlab/src/syntax/latexindent.rs21
-rw-r--r--support/texlab/src/syntax/mod.rs14
-rw-r--r--support/texlab/src/syntax/text.rs260
-rw-r--r--support/texlab/src/test/client.rs73
-rw-r--r--support/texlab/src/test/mod.rs632
-rw-r--r--support/texlab/src/test/server.rs87
-rw-r--r--support/texlab/src/tex/compile.rs97
-rw-r--r--support/texlab/src/tex/kpsewhich.rs112
-rw-r--r--support/texlab/src/tex/mod.rs154
-rw-r--r--support/texlab/src/tex/tectonic.rs36
-rw-r--r--support/texlab/src/tex/texlive.rs71
-rw-r--r--support/texlab/src/uri.rs (renamed from support/texlab/src/protocol/uri.rs)24
-rw-r--r--support/texlab/src/workspace.rs980
-rw-r--r--support/texlab/src/workspace/api.rs86
-rw-r--r--support/texlab/src/workspace/children_expand.rs92
-rw-r--r--support/texlab/src/workspace/document.rs127
-rw-r--r--support/texlab/src/workspace/parent_expand.rs123
-rw-r--r--support/texlab/src/workspace/storage.rs136
-rw-r--r--support/texlab/src/workspace/watch.rs107
-rw-r--r--support/texlab/tests/integration/common.rs318
-rw-r--r--support/texlab/tests/integration/completion.rs1927
-rw-r--r--support/texlab/tests/integration/definition.rs482
-rw-r--r--support/texlab/tests/integration/document_symbol.rs836
-rw-r--r--support/texlab/tests/integration/folding.rs126
-rw-r--r--support/texlab/tests/integration/highlight.rs83
-rw-r--r--support/texlab/tests/integration/hover.rs163
-rw-r--r--support/texlab/tests/integration/issues.rs389
-rw-r--r--support/texlab/tests/integration/link.rs173
-rw-r--r--support/texlab/tests/integration/main.rs9
-rw-r--r--support/texlab/tests/integration/prepare_rename.rs151
-rw-r--r--support/texlab/tests/integration/reference.rs150
-rw-r--r--support/texlab/tests/integration/rename.rs236
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__bibtex__command_complete_entry.snap150
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__bibtex__command_incomplete_entry.snap150
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__bibtex__empty_document.snap9
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__bibtex__entry_type_complete.snap33
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__bibtex__entry_type_empty_name.snap159
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__bibtex__entry_type_empty_name_before.snap9
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__bibtex__entry_type_incomplete.snap79
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__bibtex__field_complete_entry.snap171
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__bibtex__field_incomplete_entry.snap171
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__bibtex__junk.snap9
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__latex__begin_command.snap162
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__latex__citation.snap54
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__latex__citation_after.snap9
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__latex__citation_open_brace.snap34
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__latex__class_command.snap150
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__latex__class_environment.snap30
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__latex__class_import.snap171
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__latex__color_model_define_color.snap105
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__latex__color_model_define_color_set.snap67
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__latex__color_name.snap143
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__latex__empty_document.snap9
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__latex__kernel_command.snap30
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__latex__kernel_command_environment.snap30
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__latex__kernel_command_glyph.snap34
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__latex__label.snap137
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__latex__package_command.snap34
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__latex__package_environment.snap150
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__latex__package_import.snap102
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__latex__pgf_library.snap143
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__latex__preselect_environment.snap150
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__latex__theorem_environment.snap130
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__latex__user_command.snap30
-rw-r--r--support/texlab/tests/integration/snapshots/integration__completion__latex__user_environment.snap50
-rw-r--r--support/texlab/tests/integration/snapshots/integration__document_symbol__enumerate_flat.snap97
-rw-r--r--support/texlab/tests/integration/snapshots/integration__document_symbol__enumerate_nested.snap141
-rw-r--r--support/texlab/tests/integration/snapshots/integration__document_symbol__equation_flat.snap61
-rw-r--r--support/texlab/tests/integration/snapshots/integration__document_symbol__equation_nested.snap87
-rw-r--r--support/texlab/tests/integration/snapshots/integration__document_symbol__float_flat.snap61
-rw-r--r--support/texlab/tests/integration/snapshots/integration__document_symbol__float_nested.snap87
-rw-r--r--support/texlab/tests/integration/snapshots/integration__document_symbol__section_flat.snap61
-rw-r--r--support/texlab/tests/integration/snapshots/integration__document_symbol__section_nested.snap88
-rw-r--r--support/texlab/tests/integration/snapshots/integration__document_symbol__theorem_flat.snap79
-rw-r--r--support/texlab/tests/integration/snapshots/integration__document_symbol__theorem_nested.snap114
-rw-r--r--support/texlab/tests/integration/snapshots/integration__hover__empty_bibtex_document.snap6
-rw-r--r--support/texlab/tests/integration/snapshots/integration__hover__empty_labtex_document.snap6
-rw-r--r--support/texlab/tests/integration/snapshots/integration__hover__label_theorem_child_file.snap21
-rw-r--r--support/texlab/tests/integration/snapshots/integration__hover__label_theorem_child_file_mumber.snap21
-rw-r--r--support/texlab/tests/integration/snapshots/integration__workspace_symbol__filter_bibtex.snap43
-rw-r--r--support/texlab/tests/integration/snapshots/integration__workspace_symbol__filter_type_figure.snap25
-rw-r--r--support/texlab/tests/integration/snapshots/integration__workspace_symbol__filter_type_item.snap61
-rw-r--r--support/texlab/tests/integration/snapshots/integration__workspace_symbol__filter_type_math.snap43
-rw-r--r--support/texlab/tests/integration/snapshots/integration__workspace_symbol__filter_type_section.snap79
-rw-r--r--support/texlab/tests/integration/workspace_symbol.rs323
442 files changed, 26972 insertions, 26290 deletions
diff --git a/support/ltximg/README.md b/support/ltximg/README.md
index 955b3b7598..8ac445354b 100644
--- a/support/ltximg/README.md
+++ b/support/ltximg/README.md
@@ -1,6 +1,6 @@
## LTXimg &ndash; LaTeX environments to image and standalone files
-Release v2.0 \[2021-01-24\]
+Release v2.1 \[2021-05-21\]
## Description
@@ -42,7 +42,7 @@ and saved in the `./images` directory using `pdflatex` and `preview` package to
```
[default]
-h, --help Display command line help and exit [off]
--v, --version Display current version (2.0) and exit [off]
+-v, --version Display current version (2.1) and exit [off]
-V, --verbose Verbose printing information [off]
-l, --log Write .log file with debug information [off]
-t, --tif Create .tif files using ghostscript [gs]
@@ -127,7 +127,7 @@ Pablo González L, `<pablgonz@yahoo.com>`.
## Copyright and Licence
-Copyright 2013 - 2020 Pablo González L `<pablgonz@yahoo.com>`.
+Copyright 2013 - 2021 Pablo González L `<pablgonz@yahoo.com>`.
This program is free software; you can redistribute it and/or modify it under the terms of the GNU
General Public License as published by the Free Software Foundation; either version 3 of the License,
diff --git a/support/ltximg/ltximg-doc.pdf b/support/ltximg/ltximg-doc.pdf
index 0a6f44fe49..377d2ae9bb 100644
--- a/support/ltximg/ltximg-doc.pdf
+++ b/support/ltximg/ltximg-doc.pdf
Binary files differ
diff --git a/support/ltximg/ltximg-doc.tex b/support/ltximg/ltximg-doc.tex
index 3d41f5c6cb..72908c0233 100644
--- a/support/ltximg/ltximg-doc.tex
+++ b/support/ltximg/ltximg-doc.tex
@@ -85,8 +85,8 @@
% Identification
\def\myscript{ltximg}
-\def\fileversion{2.0}
-\def\filedate{2021-01-24}
+\def\fileversion{2.1}
+\def\filedate{2021-05-21}
% Logo with fetamont and libertuns font
\ExplSyntaxOn
@@ -400,7 +400,17 @@
\SortIndex{#2}{\texttt{\small\textbackslash#2}}
}
-% file extention
+% \module{m}
+\NewDocumentCommand{\module}{m}
+ {
+ \SortIndex{Perl ~ module}{Perl ~ module >\small\texttt{#1}}
+ \tl_set:Nn \l_tmpa_tl { #1 }
+ \regex_replace_all:nnN { (\:\:) } { \c{textcolor}\cB\{MediumOrchid\cE\}\cB\{\1\cE\} } \l_tmpa_tl
+ \textcolor{blue!75}{\ttfamily{\tl_use:N \l_tmpa_tl}}
+ %\SortIndex{Perl ~ module}{Perl ~ module >\small\texttt{\tl_use:N \l_tmpa_tl}}
+ }
+
+% File extention
\NewDocumentCommand{\fext}{m}
{
\mbox{\textcolor{OrangeRed}{\texttt{.#1}}}
@@ -414,8 +424,7 @@
\SortIndex{file}{File >\small\texttt{#1.#2}}
}
-
-% image format/extention
+% Image format/extention
\NewDocumentCommand{\iext}{m}
{
\textcolor{optcolor}{\texttt{#1}}
@@ -572,8 +581,8 @@
% Important words 2 (macros)
keywordstyle=[2]{\color{blue!75}},%
keywords=[2]{graphicspath,RequirePackage,renewcommand,includegraphics,coordinate,draw,approx,%
- PreviewBbAdjust,setlength,parindent,usetikzlibrary,tikzexternalize,psset,tikzset,PrependGraphicsExtensions,%
- DefineShortVerb,lstMakeShortInline,MakeSpecialShortVerb,UndefineShortVerb,verb,myverb,macro,put,line},%
+ PreviewBbAdjust,setlength,parindent,usetikzlibrary,tikzexternalize,psset,tikzset,PrependGraphicsExtensions,cmd,%
+ DefineShortVerb,lstMakeShortInline,MakeSpecialShortVerb,UndefineShortVerb,verb,myverb,macro,put,line,Win32::Console::ANSI},%
% Important words 3, options in input file
keywordstyle=[3]{\color{optcolor!85}},%
keywords=[3]{extrenv,deltenv,skipenv,options,arara,extensions,luatex,env,norun,%
@@ -756,16 +765,19 @@ to \emph{any type} of environment covering three central points:
\item Generate a standalone files with only the extracted environments.
\item Generate a file replacing the environments by \ics{includegraphics}.
+
\end{enumerate}
-From the side of \hologo{TeX} there are some packages that cover several of these
-points such as the \pkgname{preview}\cite{preview}, \pkgname{xcomment}\cite{xcomment}, \pkgname{extract}\cite{extract}
-and \pkgname{cachepic}\cite{cachepic} packages among others, but none covered all points.
+From the side of \hologo{TeX} there are some packages that cover
+several of these points such as the \pkgname{preview}\cite{preview},
+\pkgname{xcomment}\cite{xcomment}, \pkgname{extract}\cite{extract} and
+\pkgname{cachepic}\cite{cachepic} packages among others, but none
+covered all points.
-In the network there are some solutions in \texttt{bash} that were able to
-extract and convert environments, but in general they presented problems
-when the document contained \emph{\enquote{verbatim style}} code or were only
-available for \OSsystem{Linux}.
+In the network there are some solutions in \texttt{bash} that were able
+to extract and convert environments, but in general they presented
+problems when the document contained \emph{\enquote{verbatim style}}
+code or were only available for \OSsystem{Linux}.
\thispagestyle{plain}
@@ -773,9 +785,9 @@ available for \OSsystem{Linux}.
\pagestyle{myheader}
-Analysed the situation the best thing was to create a new \emph{\enquote{script}} that
-was able to cover the three points and was multi platform, the union of all
-these ideas is born \ltximg.
+Analysed the situation the best thing was to create a new
+\emph{\enquote{script}} that was able to cover the three points and was
+multi platform, the union of all these ideas is born \ltximg.
This script would not be possible without the great work of Herbert Voß
author of \scriptname{pst2pdf}\footnote{\url{https://ctan.org/pkg/pst2pdf}}
@@ -791,49 +803,91 @@ a version equal to or greater than \liningnums{5.28} of \prgname{perl}, a
version equal to or greater than \liningnums{9.52} of \prgname{ghostscript}, a
version equal to or greater than \liningnums{1.40} of \scriptname{pdfcrop}
and have a version equal to or greater than \liningnums{0.52} of %
-\prgname{poppler-utils}.
-
-The script uses only packages from the core of the \prgname{perl}, the
-distribution encapsulated in \hologo{TeX} Live 2020 for \OSsystem{Windows}
-does not have the \verb|Win32::Console::ANSI| package, this does not
-affect the operation of the script, but it does affect the presentation
-of the messages when invoked from \verb|cmd|, it is recommended to use
-a more modern (and comfortable) application such as \prgname{Windows Terminal}.
-\hologo{MiKTeX} users must install the appropriate software for full
-operation.
-
-The script auto detects the \prgname{ghostscript}, but not \prgname{poppler-utils}.
+\prgname{poppler-utils}. \hologo{MiKTeX} and \prgname{cygwin} users
+must install the appropriate software for full operation.
+
+\ltximg{} auto detects the \prgname{ghostscript}, but not \prgname{poppler-utils}.
You should keep this in mind if you are using the script directly and not
the version provided in your \hologo{TeX} distribution.
-The script has been tested on \OSsystem{Windows} v10, \prgname{cygwin} v3.1.6, \prgname{Git for Windows} v2.30 and %
-\OSsystem{Linux} (fedora 33) using \prgname{ghostscript} v9.53.3, \prgname{poppler-utils} v0.90, \prgname{perl} v5.32
-and the standard classes offers by \hologo{LaTeX}: \myclass{book}, \myclass{report}, \myclass{article} and
-\myclass{letter}. The \pkgname{preview}\cite{preview} and \pkgname{pst-pdf}\cite{pst-pdf} packages are
-required to process the \meta{input file} and if an \meta{output file} is generated, the \pkgname{graphicx}\cite{graphicx}
-and \pkgname{grfext}\cite{grfext} packages will be needed.
+The script has been tested on \OSsystem{Windows} 10, \prgname{cygwin}
+3.1.6, \prgname{Git for Windows} 2.30 and \OSsystem{Linux} (fedora 34)
+using \prgname{ghostscript} 9.53.3, \prgname{poppler-utils} 0.90,
+\prgname{perl} 5.32 and the standard classes offers by \hologo{LaTeX}:
+\myclass{book}, \myclass{report}, \myclass{article} and
+\myclass{letter}. The \pkgname{preview}\cite{preview} and
+\pkgname{pst-pdf}\cite{pst-pdf} packages are required to process the
+\meta{input file} and if an \meta{output file} is generated, the
+\pkgname{graphicx}\cite{graphicx} and \pkgname{grfext}\cite{grfext}
+packages will be needed.
+
+
+\subsection{Modules required}
+
+\ltximg{} uses only packages from the core of the \prgname{perl}, the
+distribution encapsulated in \hologo{TeX} Live 2020 for
+\OSsystem{Windows} does not have the module
+\module{Win32::Console::ANSI}, this does not affect the operation of
+the script, but it does affect the presentation of the messages when
+invoked from \emph{standart} \prgname{cmd}, it is recommended to use a
+more modern (and comfortable) application such as \prgname{Windows
+Terminal}.
+
+\setlength{\multicolsep}{2ex}
+\begin{multicols}{2}
+\begin{itemize}[font=\sffamily\small,first=\small,partopsep=5pt,parsep=5pt,nosep,leftmargin=*]
+ \item \module{Getopt::Long}
+ \item \module{File::Spec::Functions}
+ \item \module{File::Basename}
+ \item \module{Archive::Tar}
+ \item \module{Data::Dumper}
+ \item \module{FileHandle}
+ \item \module{IO::Compress::Zip}
+ \item \module{File::Path}
+ \item \module{File::Temp}
+ \item \module{POSIX}
+ \item \module{File::Copy}
+ \item \module{File::Find}
+ \item \module{Env}
+ \item \module{autodie}
+ \item \module{Config}
+ \item \module{Cwd}
+ \item \module{Term::ANSIColor}
+ \item \module{Module::Load::Conditional}
+ \item Only on \OSsystem{Windows}:
+ \begin{itemize}[font=\sffamily\small,first=\small,nosep]
+ \item \module{Win32}
+ \item \module{Win32::Console::ANSI}
+ \item \module{Win32::TieRegistry}
+ \end{itemize}
+\end{itemize}
+\end{multicols}
\section{How it works}
\label{sec:howtowork}
-It is important to have a general idea of how the \emph{\enquote{extraction and conversion}}
-process works and the requirements that must be fulfilled so
-that everything works correctly, for this we must be clear about some
-concepts related to how to work with the \meta[cf=optcolor]{input file}, the \meta{verbatim content}
+It is important to have a general idea of how the
+\emph{\enquote{extraction and conversion}} process works and the
+requirements that must be fulfilled so that everything works correctly,
+for this we must be clear about some concepts related to how to work
+with the \meta[cf=optcolor]{input file}, the \meta{verbatim content}
and the \meta{steps process}.
\subsection{The input file}
\label{sec:inputfile}
-The \meta[cf=optcolor]{input file} must comply with \emph{certain characteristics} in order to
-be processed, the content at the beginning and at the end of the \meta{input
-file} is treated in a special way, before \lstinline|\documentclass| and after
-\lstinline|\end{document}| can go any type of content, internally the script will
-\emph{\enquote{split}} the \meta[cf=optcolor]{input file} at this points.
-
-If the \meta[cf=optcolor]{input file} contains files using \ics{input}\marg[type=tt]{file} or
-\ics{include}\marg[type=tt]{file} these will not be processed, from the side of
-the \emph{script} they only represent lines within the file, if you want
+The \meta[cf=optcolor]{input file} must comply with \emph{certain
+characteristics} in order to be processed, the content at the beginning
+and at the end of the \meta{input file} is treated in a special way,
+before \lstinline|\documentclass| and after \lstinline|\end{document}|
+can go any type of content, internally the script will
+\emph{\enquote{split}} the \meta[cf=optcolor]{input file} at this
+points.
+
+If the \meta[cf=optcolor]{input file} contains files using
+\ics{input}\marg[type=tt]{file} or \ics{include}\marg[type=tt]{file}
+these will not be processed, from the side of the \emph{script} they
+only represent lines within the file, if you want
them to be processed it is better to use the \scriptname{latexpand}\footnote{\url{https://www.ctan.org/pkg/latexpand}}
first and then process the file.
@@ -865,22 +919,22 @@ parts and then processing them:
% Part Four: Everything after \end{document}
\end{examplecode}
-If for some reason you have an environment \LWenv{filecontens} before \lstinline|\documentclass| or
-in the preamble of the \meta{input file} that contains a \emph{sub-document} or \emph{environment}
-you want to extract, the script will ignore them. Similarly, the content after \lstinline|\end{document}|
-is ignored in the extraction process.
-
-\newpage
+If for some reason you have an environment \LWenv{filecontents} before
+\lstinline|\documentclass| or in the preamble of the \meta{input file}
+that contains a \emph{sub-document} or \emph{environment} you want to
+extract, the script will ignore them. Similarly, the content after
+\lstinline|\end{document}| is ignored in the extraction process.
\subsection{Verbatim contents}
\label{sec:verbatim}
-One of the greatest capabilities of this script is to \emph{\enquote{skip}}
-the complications that \meta{verbatim content} produces with the extraction
-of environments using tools outside the \enquote{\hologo{TeX} world}\footnote{Only \hologo{TeX} can
-understand \hologo{TeX}, all other languages and programs are just lines in a file.}.
-In order to \emph{\enquote{skip}} the complications, the \meta{verbatim content} is classified into
-three types:
+One of the greatest capabilities of this script is to
+\emph{\enquote{skip}} the complications that \meta{verbatim content}
+produces with the extraction of environments using tools outside the
+\enquote{\hologo{TeX} world}\footnote{Only \hologo{TeX} can understand
+\hologo{TeX}, all other languages and programs are just lines in a
+file.}. In order to \emph{\enquote{skip}} the complications, the
+\meta{verbatim content} is classified into three types:
\begin{itemize}[nosep]
\item Verbatim in line.
@@ -936,9 +990,9 @@ it correctly. The solution is quite simple, just add in \meta[cf=optcolor]{input
\DefineShortVerb{\|}
\end{examplecode}
-depending on the package you are using. If your \emph{\enquote{verbatim macro}} is not
-supported by default or can not detect, use the options described in \ref%
-{sec:optline} and \ref{sec:optfile}.
+depending on the package you are using. If your \emph{\enquote{verbatim macro}}
+is not supported by default or can not detect, use the options
+described in \ref{sec:optline} and \ref{sec:optfile}.
\subsection*{Verbatim standard}
\label{sec:verbatim:std}
@@ -1110,8 +1164,6 @@ and create a temporary directory \sysdir{/tmp/hG45uVklv9}.
The entire \sysfile{test.tex} file is loaded into memory and \emph{\enquote{split}} to start the extraction process.
-\newpage
-
\subsection*{Comment and ignore}
In the second step, once the file \sysfile{test.tex} is loaded and divided in memory,
proceeds (in general terms) as follows:
@@ -1283,7 +1335,7 @@ package with the [\pkgopt{swpl}] or [\pkgopt{tcb}] option and run the script
using \cmdopt{latex}{} or \cmdopt{xetex}. This environment is reserved for the internal process of extraction and conversion, it
cannot be passed as an argument to the option \cmdopt{skipenv}{}.
-\vspace{0.25\baselineskip}
+\newpage
\DescribeTE{pspicture}
Environment provide by \pkgname{PStricks}\cite{pstricks} package. The plain \hologo{TeX}
@@ -1297,7 +1349,7 @@ Environment provide by \pkgname{pst-plot}\cite{pst-plot} package. The plain \hol
its converted to \hologo{LaTeX} syntax \lstinline|\begin{psgraph} ... \end{psgraph}| if
not within the \env{PSTexample} or \env{postscript} environments.
-\newpage
+\vspace{0.75\baselineskip}
\DescribeTE{tikzpicture}
Environment provide by \pkgname{TikZ}\cite{tikz} package. The plain \hologo{TeX} syntax \lstinline|\tikzpicture ... \tikzpicture|
@@ -1344,8 +1396,6 @@ unless the option \cmdopt[n]{noprew}{} is used. This environment is
reserved for the internal process of extraction and conversion, it
cannot be passed as an argument to the option \cmdopt{extrenv}.
-\vspace{\baselineskip}
-
\DescribeTE*{noltximg}
All content betwen \lstinline|%<*noltximg> ... %</noltximg>| are ignored and no extract.
The tags can \emph{not} be nested and should be at the beginning of the line and in separate lines.
@@ -1395,6 +1445,8 @@ The image format generated using \prgname{pdftoeps}. The line executed by the sy
[user@machine ~:]§ pdftops -q -eps
\end{examplecmd}
+\newpage
+
\DescribeIF{png}
The image format generated using \prgname{ghostscript}. The line executed by the system is:
@@ -1467,7 +1519,6 @@ Options that accept a \meta{value} require either a blank space \verb*| | or \te
between the option and the \meta{value}. Multiple short options can be bundling and if the last option takes a
\meta{comma separated list} you need \textcolor{optcolor}{\bfseries\texttt{-\/-}} at the end.
-\vspace{\baselineskip}
\DescribeCmd[h]{help}{boolean}{off}
Display a command line help and exit.
@@ -1587,7 +1638,7 @@ and \meta[cf=optcolor]{output file}, is mutually exclusive with \emph{\enquote{a
Using \prgname*{xelatex} compiler \meta[cf=optcolor]{input file} and \meta[cf=optcolor]{output file}.
In the execution \prgname*{xelatex} is called with the \prgopt*{no-pdf} option
generating a \fext{xvd} file and then it is processed using \prgname*{xdvipdfmx},
-this is only to execute \emph{faster} the conversion of environments to images
+this is only to execute \emph{faster} conversion of environments to images
and it is only done on the \meta[cf=optcolor]{input file}.
\DescribeCmd{latex}{boolean}{off}
@@ -1653,6 +1704,8 @@ at the end. The environments \env{PSTexample} and \env{preview} are not supporte
Add \meta{verbatim standard} environment support, if it's the last option
passed need \textcolor{optcolor}{\textbf{\texttt{-\/-}}} at the end.\par
+\newpage
+
\DescribeCmd{writenv}{comma separated list}{empty}
Add \meta{verbatim write} environment support, if it's the last option
passed need \textcolor{optcolor}{\textbf{\texttt{-\/-}}} at the end.\par
@@ -1670,6 +1723,7 @@ An example of usage from command line:
\begin{examplecmd}
[user@machine ~:]§ ltximg --latex -s -o test-out test-in.ltx
\end{examplecmd}
+
Create a \sysdir{./images} directory (if it does not exist) with all extracted environments converted to image
formats (\iext{pdf}, \iext{svg}) in individual files, an output file \meta[ac=LightSlateGrey,cf=OrangeRed]{test-out.ltx}
with all extracted environments converted to \ics{includegraphics} and a single file
@@ -1717,7 +1771,7 @@ This line is to indicate to the script which environments are deleted.
\DescribeOptFile{options}{option one = value, option two = value, option three = value, \ldots}
This line is to indicate to the script which options(other than those listed above) need to process.
-The options passed from the \meta[cf=optcolor]{input file} are validated by the script after they are read. If you are going to
+All options passed from the \meta[cf=optcolor]{input file} are validated by the \ltximg{} after they are read. If you are going to
create an \meta[cf=optcolor]{output file} and you do not want these lines to remain, it is better to
place them inside the \lstinline|%<*remove> ... %</remove>|. Like this:
@@ -1752,7 +1806,8 @@ and \meta[ac=LightSlateGrey,cf=OrangeRed]{file-out.tex}.
\section{The way of \texttt{arara}}
\label{sec:arara}
-By design, the script only runs \emph{\enquote{one or more compilation}} on top
+
+By design, \ltximg{} only runs \emph{\enquote{one or more compilation}} on top
of the \meta[cf=optcolor]{input file}, but, sometimes you need to process in a specific mode the \meta[cf=optcolor]{input file}
or needs to be processed with something other than \hologo{LaTeX}, \hologo{XeLaTeX}, \hologo{pdfLaTeX} or \hologo{LuaLaTeX} engine.
This is where \arara\cite{arara} comes in, this \emph{\enquote{great little tool}},
@@ -1880,7 +1935,6 @@ to generate an image in \iext{svg}, \iext{png} and \iext{pdf} format from enviro
\begin{document}
\section{How to get Tikzmark to work}
By taking logarithms of both sides:
-
\[
t = \frac{30\cdot\ln(3/22)}{\ln(15/22)}
\tikzmark{calculator}\approx\tikzmark{otherside}
@@ -1890,7 +1944,6 @@ By taking logarithms of both sides:
\coordinate (target) at ($(pic cs:calculator)!1/2!(pic cs:otherside) - (0,.5ex)$);
\draw[arrows=->] (target) ++(0,-2ex) node [anchor=north] {use calculator} -- (target);
\end{tikzpicture}
-
\section{Draw an aircraft with Tikz}
The best airplane ever drawn by David Carlise. No TikZ used, just the
classic and perhaps forgotten \verb|\begin{picture} ... \end{picture}|.
@@ -1957,6 +2010,11 @@ versions, both development (devp) and public (ctan).
\begin{description}[font=\small\sffamily,style=nextline,nosep,noitemsep]
\item [v\fileversion\ (ctan), \filedate] \leavevmode\vspace{-\baselineskip}
\begin{itemize}
+ \item Maintenance update.
+ \item Fix internal \texttt{hash} for regex.
+ \end{itemize}
+\item [v2.0 (ctan), 2021-01-24] \leavevmode\vspace{-\baselineskip}
+ \begin{itemize}
\item Add \texttt{-dALLOWPSTRANSPARENCY} to \texttt{ps2pdf}.
\item The \texttt{--xetex} option now uses \texttt{xelatex} and then \texttt{xdvipdfmx}.
\item Fix module detection under \hologo{TeX} Live on Windows.
diff --git a/support/ltximg/ltximg.1 b/support/ltximg/ltximg.1
index 81545b5470..918b2626dd 100644
--- a/support/ltximg/ltximg.1
+++ b/support/ltximg/ltximg.1
@@ -1,4 +1,4 @@
-.\" Automatically generated by Pod::Man 4.14 (Pod::Simple 3.40)
+.\" Automatically generated by Pod::Man 4.14 (Pod::Simple 3.42)
.\"
.\" Standard preamble:
.\" ========================================================================
@@ -71,7 +71,7 @@
.\" ========================================================================
.\"
.IX Title "ltximg 1"
-.TH ltximg 1 "2021-01-24" "2.0" "General Commands Manual"
+.TH ltximg 1 "2021-05-21" "2.1" "General Commands Manual"
.\" For nroff, turn off justification. Always turn off hyphenation; it makes
.\" way too many mistakes in technical documents.
.if n .ad l
diff --git a/support/ltximg/ltximg.man1.pdf b/support/ltximg/ltximg.man1.pdf
index 0cb98fe28c..57f87af07b 100644
--- a/support/ltximg/ltximg.man1.pdf
+++ b/support/ltximg/ltximg.man1.pdf
Binary files differ
diff --git a/support/ltximg/ltximg.pl b/support/ltximg/ltximg.pl
index 791c931685..2582d2d61d 100644
--- a/support/ltximg/ltximg.pl
+++ b/support/ltximg/ltximg.pl
@@ -31,7 +31,7 @@ use Config;
use Cwd;
use if $^O eq 'MSWin32', 'Win32';
use if $^O eq 'MSWin32', 'Module::Load::Conditional' => qw(can_load check_install requires);
-# Need on Windows cmd for colors, TeX Live not provide 'Win32::Console::ANSI'
+# Need for colors in Windows cmd, TeX Live not provide 'Win32::Console::ANSI'
if($^O eq 'MSWin32' && check_install( module => 'Win32::Console::ANSI')) {
require Win32::Console::ANSI;
}
@@ -44,8 +44,8 @@ my $workdir = cwd;
### Script identification
my $scriptname = 'ltximg';
my $program = 'LTXimg';
-my $nv = 'v2.0';
-my $date = '2021-01-24';
+my $nv = 'v2.1';
+my $date = '2021-05-21';
my $copyright = <<"END_COPYRIGHT" ;
[$date] - LaTeX environments to image and standalone files
END_COPYRIGHT
@@ -601,7 +601,7 @@ sub SearchRegistry {
return $found;
} # end GS search registry
-### This part is only necessary if you're using Git on windows and don't
+### This part is only necessary if you're using Git-bash on windows and don't
### have gs configured in PATH. Git for windows don't have a Win32::TieRegistry
### module for perl (is not supported in the current versions of msys).
sub Searchbyregquery {
@@ -2544,17 +2544,13 @@ if (@pst_exa and $outfile) {
$PALABRAS = qr/\b (?: pst-\w+ | pstricks (?: -add | -pdf )? | psfrag |psgo |vaucanson-g| auto-pst-pdf(?: -lua )? )/x;
$FAMILIA = qr/\{ \s* $PALABRAS (?: \s* [,] \s* $PALABRAS )* \s* \}(\%*)?/x;
-### Clean PST content in preamble
+### Clean PSTricks content in preamble
if ($clean{pst} and $outfile) {
Log("Remove pstricks packages in preamble for $opts_cmd{string}{output}$outext");
$preamble =~ s/\%<\*$dtxverb> .+?\%<\/$dtxverb>(*SKIP)(*F)|
^ $USEPACK (?: $CORCHETES )? $FAMILIA \s*//msxg;
$preamble =~ s/\%<\*$dtxverb> .+?\%<\/$dtxverb>(*SKIP)(*F)|
(?: ^ $USEPACK \{ | \G) [^}]*? \K (,?) \s* $PALABRAS (\s*) (,?) /$1 and $3 ? ',' : $1 ? $2 : ''/gemsx;
- if (@pst_exa) {
- Log("Uncomment pst-exa package in preamble for $opts_cmd{string}{output}$outext");
- $preamble =~ s/(?:\%)(\\usepackage\[\s*)(swpl|tcb)(,pdf\s*\]\{pst-exa\})/$1$2$3/msxg;
- }
Log("Remove \\psset\{...\} in preamble for $opts_cmd{string}{output}$outext");
$preamble =~ s/\%<\*$dtxverb> .+?\%<\/$dtxverb>(*SKIP)(*F)|
\\psset\{(?:\{.*?\}|[^\{])*\}(?:[\t ]*(?:\r?\n|\r))+//gmsx;
@@ -2566,6 +2562,12 @@ if ($clean{pst} and $outfile) {
^\\usepackage\{\}(?:[\t ]*(?:\r?\n|\r))+/\n/gmsx;
}
+### Uncomment pst-exa package
+if (@pst_exa and $outfile) {
+ Log("Uncomment pst-exa package in preamble for $opts_cmd{string}{output}$outext");
+ $preamble =~ s/(?:\%)(\\usepackage\[\s*)(swpl|tcb)(,pdf\s*\]\{pst-exa\})/$1$2$3/msxg;
+}
+
### To be sure that the package graphicx and \graphicspath is in the main
### document and not in a verbatim write environment we make the changes
my %tmpreplace = (
@@ -2807,9 +2809,10 @@ if ($outfile) {
}
# Remove internal mark for verbatim and verbatim write environments
$out_file =~ s/\%<\*$dtxverb>\s*(.+?)\s*\%<\/$dtxverb>/$1/gmsx;
- %replace = (%changes_out);
+ %replace = (%changes_out,%tmpoutreplace);
$find = join q{|}, map {quotemeta} sort { length $a <=> length $b } keys %replace;
$out_file =~ s/($find)/$replace{$1}/g;
+ # Write <output file>
if (-e "$opts_cmd{string}{output}$outext") {
Log("Rewriting the file $opts_cmd{string}{output}$outext in $workdir");
Infocolor('Warning', "The file [$opts_cmd{string}{output}$outext] already exists and will be rewritten");
@@ -2818,7 +2821,6 @@ if ($outfile) {
Infoline("Creating the file $opts_cmd{string}{output}$outext");
Log("Write the file $opts_cmd{string}{output}$outext in $workdir");
}
- # Write <output file>
open my $OUTfile, '>', "$opts_cmd{string}{output}$outext";
print {$OUTfile} $out_file;
close $OUTfile;
@@ -3281,4 +3283,3 @@ General Public License for more details.
gs(1), dvips(1), ps2pdf(1), pdfcrop(1), pdftops(1), pdftocairo(1), pdftoppm(1)
=cut
-
diff --git a/support/texlab/CHANGELOG.md b/support/texlab/CHANGELOG.md
index 70863a7c18..df4134084d 100644
--- a/support/texlab/CHANGELOG.md
+++ b/support/texlab/CHANGELOG.md
@@ -5,6 +5,52 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [3.0.1] - 22.05.2021
+
+### Fixed
+
+- Sometimes the log parser does not pick up errors from the log file ([#426](https://github.com/latex-lsp/texlab/issues/426))
+- Fix a bug involving characters that are not part of the ASCII charset ([#428](https://github.com/latex-lsp/texlab/issues/428))
+
+## [3.0.0] - 16.05.2021
+
+### Added
+
+- Basic error analysis for LaTeX files ([#323](https://github.com/latex-lsp/texlab/issues/323))
+- Parse LaTeX3 commands correctly ([#410](https://github.com/latex-lsp/texlab/issues/410))
+- Allow configuring ChkTeX using a `chktexrc` file ([#309](https://github.com/latex-lsp/texlab/issues/309))
+- Implement goto definition for includes ([#386](https://github.com/latex-lsp/texlab/issues/386))
+- Provide completion for `\citeA{...}` ([#409](https://github.com/latex-lsp/texlab/issues/409))
+- Allow passing additional arguments to `latexindent` ([#365](https://github.com/latex-lsp/texlab/issues/365))
+- Document symbols and label completion now correctly handle `subequations`.
+
+### Changed
+
+- _BREAKING_: The configuration format has changed.
+ Every setting is now under the `texlab` scope instead of the two scopes `latex` and `bibtex`. For a list of possible options, please see [here](docs/options.md).
+ The `latex.build.onSave` setting has been removed in favor of `-pvc` of `latexmk`. In the VSCode extension, the `latex.build.onSave` setting is still available along with the `latex.build.forwardSearchAfter` setting. The reasoning is that that `latex.build.forwardSearchAfter` cannot reliably implemented in the server because it requires the current cursor position, which the LSP spec does not offer. In previous versions, TexLab had to guess the cursor position. We encourage editor extensions, to still support these settings under the `texlab` scope.
+- _BREAKING_: Previewing equations has been removed for now until
+ a better solution is found. The existing approach is way too slow and does not work reliably.
+- Distribution detection no longer produces an error message in the client.
+ Instead, a log message is generated. A TeX distribution is only required to compile documents.
+- Improve compile times a bit.
+
+### Fixed
+
+- Do not send snippets if the client does not support them ([#413](https://github.com/latex-lsp/texlab/issues/413))
+- Fix protocol violation when exiting the server ([#310](https://github.com/latex-lsp/texlab/issues/310))
+- Fix reporting compile-time diagnostics using file watching ([#339](https://github.com/latex-lsp/texlab/issues/339))
+- Fix compilation warnings ([#359](https://github.com/latex-lsp/texlab/issues/359))
+- Fix crash when exiting with NeoVim LSP client ([#405](https://github.com/latex-lsp/texlab/issues/405))
+- Hopefully fixes the ChkTeX spamming issue ([#186](https://github.com/latex-lsp/texlab/issues/186))
+- Reduce CPU-load when idle ([#400](https://github.com/latex-lsp/texlab/issues/400))
+
+## [2.2.2] - 10.01.2021
+
+### Fixed
+
+- Fix compilation on `arm64-apple-darwin` (Apple Silicon) ([#343](https://github.com/latex-lsp/texlab/issues/343))
+
## [2.2.1] - 06.01.2021
### Added
diff --git a/support/texlab/Cargo.lock b/support/texlab/Cargo.lock
index 953089e524..c224bce2d3 100644
--- a/support/texlab/Cargo.lock
+++ b/support/texlab/Cargo.lock
@@ -1,16 +1,16 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
-name = "adler32"
-version = "1.0.4"
+name = "ahash"
+version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2"
+checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e"
[[package]]
name = "aho-corasick"
-version = "0.7.10"
+version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
dependencies = [
"memchr",
]
@@ -21,24 +21,14 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
dependencies = [
- "winapi 0.3.8",
+ "winapi",
]
[[package]]
-name = "aovec"
-version = "1.1.0"
+name = "anyhow"
+version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6cfde8df73bd42d9bcbfd8fa4578d4d2a37512efe0436351739e025ab791cd9"
-dependencies = [
- "parking_lot 0.4.8",
- "smallvec 0.4.5",
-]
-
-[[package]]
-name = "arc-swap"
-version = "0.4.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7b8a9123b8027467bce0099fe556c628a53c8d83df0507084c31e9ba2e39aff"
+checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b"
[[package]]
name = "arrayref"
@@ -48,24 +38,9 @@ checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
[[package]]
name = "arrayvec"
-version = "0.4.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9"
-dependencies = [
- "nodrop",
-]
-
-[[package]]
-name = "arrayvec"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8"
-
-[[package]]
-name = "ascii"
-version = "0.9.3"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e"
+checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "ascii-canvas"
@@ -77,25 +52,14 @@ dependencies = [
]
[[package]]
-name = "async-trait"
-version = "0.1.41"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b246867b8b3b6ae56035f1eb1ed557c1d8eae97f0d53696138a50fa0e3a3b8c0"
-dependencies = [
- "proc-macro2 1.0.13",
- "quote 1.0.7",
- "syn 1.0.39",
-]
-
-[[package]]
name = "atty"
-version = "0.2.11"
+version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
+ "hermit-abi",
"libc",
- "termion",
- "winapi 0.3.8",
+ "winapi",
]
[[package]]
@@ -106,51 +70,45 @@ checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
[[package]]
name = "autocfg"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
-
-[[package]]
-name = "base64"
-version = "0.11.0"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "base64"
-version = "0.12.3"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
-name = "base64"
-version = "0.13.0"
+name = "beef"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+checksum = "6736e2428df2ca2848d846c43e88745121a6654696e349ce0054a420815a7409"
[[package]]
name = "bibutils-sys"
-version = "0.1.0"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "819e96544dd4aadc8416a2fdd0205a886664b6eb0c17704de46c836373e9c515"
+checksum = "701178dfb147521ab87f4729feff1c21b69449b3f09fc263dd429aa43afb4f5c"
dependencies = [
"cc",
]
[[package]]
name = "bit-set"
-version = "0.5.1"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e84c238982c4b1e1ee668d136c510c67a13465279c0cb367ea6baf6310620a80"
+checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
-version = "0.5.1"
+version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f59bbe95d4e52a6398ec21238d31577f2b28a9d86807f06ca59d191d8440d0bb"
+checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bitflags"
@@ -159,105 +117,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
-name = "bitvec"
-version = "0.19.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7ba35e9565969edb811639dbebfe34edc0368e472c5018474c8eb2543397f81"
-dependencies = [
- "funty",
- "radium",
- "tap",
- "wyz",
-]
-
-[[package]]
name = "blake2b_simd"
-version = "0.5.10"
+version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a"
+checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
dependencies = [
"arrayref",
- "arrayvec 0.5.1",
+ "arrayvec",
"constant_time_eq",
]
[[package]]
-name = "block-buffer"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
-dependencies = [
- "block-padding",
- "byte-tools",
- "byteorder",
- "generic-array",
-]
-
-[[package]]
-name = "block-padding"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
-dependencies = [
- "byte-tools",
-]
-
-[[package]]
-name = "bstr"
-version = "0.2.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2889e6d50f394968c8bf4240dc3f2a7eb4680844d27308f798229ac9d4725f41"
-dependencies = [
- "lazy_static",
- "memchr",
- "regex-automata",
- "serde",
-]
-
-[[package]]
-name = "bumpalo"
-version = "3.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f359dc14ff8911330a51ef78022d376f25ed00248912803b58f00cb1c27f742"
-
-[[package]]
-name = "byte-tools"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
-
-[[package]]
-name = "bytemuck"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37fa13df2292ecb479ec23aa06f4507928bef07839be9ef15281411076629431"
-
-[[package]]
name = "byteorder"
-version = "1.3.4"
+version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
-version = "0.5.6"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
+checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040"
[[package]]
-name = "cast"
-version = "0.2.3"
+name = "cancellation"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b9434b9a5aa1450faa3f9cb14ea0e8c53bb5d2b3c1bfd1ab4fc03e9f33fbfb0"
-dependencies = [
- "rustc_version",
-]
+checksum = "f7a879c84c21f354f13535f87ad119ac3be22ebb9097b552a0af6a78f86628c4"
[[package]]
name = "cc"
-version = "1.0.52"
+version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3d87b23d6a92cd03af510a5ade527033f6aa6fa92161e2d5863a907d4c5e31d"
+checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd"
[[package]]
name = "cesu8"
@@ -361,21 +253,21 @@ dependencies = [
"salsa",
"serde",
"serde_derive",
- "smallvec 0.6.13",
+ "smallvec 0.6.14",
"strum",
"ucd-trie",
]
[[package]]
name = "clap"
-version = "2.33.0"
+version = "2.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9"
+checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
dependencies = [
"ansi_term",
"atty",
"bitflags",
- "strsim 0.8.0",
+ "strsim",
"textwrap",
"unicode-width",
"vec_map",
@@ -391,29 +283,27 @@ dependencies = [
]
[[package]]
-name = "color_quant"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0dbbb57365263e881e805dc77d94697c9118fd94d8da011240555aa7b23445bd"
-
-[[package]]
name = "combine"
-version = "3.8.1"
+version = "4.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680"
+checksum = "cc4369b5e4c0cddf64ad8981c0111e7df4f7078f4d6ba98fb31f2e17c4c57b7e"
dependencies = [
- "ascii",
- "byteorder",
- "either",
+ "bytes",
"memchr",
- "unreachable",
]
[[package]]
-name = "const_fn"
-version = "0.4.3"
+name = "console"
+version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c478836e029dcef17fb47c89023448c64f781a046e0300e257ad8225ae59afab"
+checksum = "3993e6445baa160675931ec041a5e03ca84b9c6e32a056150d3aa2bdda0a1f45"
+dependencies = [
+ "encode_unicode",
+ "lazy_static",
+ "libc",
+ "terminal_size",
+ "winapi",
+]
[[package]]
name = "constant_time_eq"
@@ -422,49 +312,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
-name = "crc32fast"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1"
-dependencies = [
- "cfg-if 0.1.10",
-]
-
-[[package]]
-name = "criterion"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70daa7ceec6cf143990669a04c7df13391d55fb27bd4079d252fca774ba244d8"
-dependencies = [
- "atty",
- "cast",
- "clap",
- "criterion-plot",
- "csv",
- "itertools 0.9.0",
- "lazy_static",
- "num-traits",
- "oorandom",
- "plotters",
- "rayon",
- "regex",
- "serde",
- "serde_cbor",
- "serde_derive",
- "serde_json",
- "tinytemplate",
- "walkdir",
-]
-
-[[package]]
-name = "criterion-plot"
-version = "0.4.3"
+name = "convert_case"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e022feadec601fba1649cfa83586381a4ad31c6bf3a9ab7d408118b05dd9889d"
-dependencies = [
- "cast",
- "itertools 0.9.0",
-]
+checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "crossbeam"
@@ -473,7 +324,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69323bff1fb41c635347b8ead484a5ca6c3f11914d784170b158d8449ab07f8e"
dependencies = [
"cfg-if 0.1.10",
- "crossbeam-channel 0.4.2",
+ "crossbeam-channel 0.4.4",
"crossbeam-deque 0.7.3",
"crossbeam-epoch 0.8.2",
"crossbeam-queue",
@@ -482,9 +333,9 @@ dependencies = [
[[package]]
name = "crossbeam-channel"
-version = "0.4.2"
+version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cced8691919c02aac3cb0a1bc2e9b73d89e832bf9a06fc579d4e71b68a2da061"
+checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87"
dependencies = [
"crossbeam-utils 0.7.2",
"maybe-uninit",
@@ -492,12 +343,12 @@ dependencies = [
[[package]]
name = "crossbeam-channel"
-version = "0.5.0"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775"
+checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4"
dependencies = [
"cfg-if 1.0.0",
- "crossbeam-utils 0.8.0",
+ "crossbeam-utils 0.8.3",
]
[[package]]
@@ -518,8 +369,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9"
dependencies = [
"cfg-if 1.0.0",
- "crossbeam-epoch 0.9.0",
- "crossbeam-utils 0.8.0",
+ "crossbeam-epoch 0.9.3",
+ "crossbeam-utils 0.8.3",
]
[[package]]
@@ -528,37 +379,37 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace"
dependencies = [
- "autocfg 1.0.0",
+ "autocfg 1.0.1",
"cfg-if 0.1.10",
"crossbeam-utils 0.7.2",
"lazy_static",
"maybe-uninit",
- "memoffset",
+ "memoffset 0.5.6",
"scopeguard",
]
[[package]]
name = "crossbeam-epoch"
-version = "0.9.0"
+version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec0f606a85340376eef0d6d8fec399e6d4a544d648386c6645eb6d0653b27d9f"
+checksum = "2584f639eb95fea8c798496315b297cf81b9b58b6d30ab066a75455333cf4b12"
dependencies = [
"cfg-if 1.0.0",
- "const_fn",
- "crossbeam-utils 0.8.0",
+ "crossbeam-utils 0.8.3",
"lazy_static",
- "memoffset",
+ "memoffset 0.6.3",
"scopeguard",
]
[[package]]
name = "crossbeam-queue"
-version = "0.2.1"
+version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c695eeca1e7173472a32221542ae469b3e9aac3a4fc81f7696bcad82029493db"
+checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570"
dependencies = [
"cfg-if 0.1.10",
"crossbeam-utils 0.7.2",
+ "maybe-uninit",
]
[[package]]
@@ -567,24 +418,29 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
dependencies = [
- "autocfg 1.0.0",
+ "autocfg 1.0.1",
"cfg-if 0.1.10",
"lazy_static",
]
[[package]]
name = "crossbeam-utils"
-version = "0.8.0"
+version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec91540d98355f690a86367e566ecad2e9e579f230230eb7c21398372be73ea5"
+checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49"
dependencies = [
- "autocfg 1.0.0",
+ "autocfg 1.0.1",
"cfg-if 1.0.0",
- "const_fn",
"lazy_static",
]
[[package]]
+name = "crunchy"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
+
+[[package]]
name = "csl"
version = "0.0.1"
source = "git+https://github.com/cormacrelf/citeproc-rs?rev=68c7a7c16088b5d7751641ab37299ad064cdb221#68c7a7c16088b5d7751641ab37299ad064cdb221"
@@ -602,46 +458,39 @@ dependencies = [
]
[[package]]
-name = "csv"
-version = "1.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00affe7f6ab566df61b4be3ce8cf16bc2576bca0963ceb0955e45d514bf9a279"
-dependencies = [
- "bstr",
- "csv-core",
- "itoa",
- "ryu",
- "serde",
-]
-
-[[package]]
-name = "csv-core"
-version = "0.1.10"
+name = "cstree"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
+checksum = "88c3cc8aae0153c915c6752c6a8ab5ea741930f32011b1fe453bdf5c57e9c1ed"
dependencies = [
- "memchr",
+ "fxhash",
+ "lasso",
+ "parking_lot 0.11.1",
+ "text-size",
+ "triomphe",
]
[[package]]
-name = "deflate"
-version = "0.8.4"
+name = "derive-new"
+version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7e5d2a2273fed52a7f947ee55b092c4057025d7a3e04e5ecdbd25d6c3fb1bd7"
+checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535"
dependencies = [
- "adler32",
- "byteorder",
+ "proc-macro2 1.0.24",
+ "quote 1.0.9",
+ "syn 1.0.65",
]
[[package]]
-name = "derive-new"
-version = "0.5.8"
+name = "derive_more"
+version = "0.99.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71f31892cd5c62e414316f2963c5689242c43d8e7bbcaaeca97e5e28c95d91d9"
+checksum = "5cc7b9cef1e351660e5443924e4f43ab25fbbed3e9a5f052df3677deb4d6b320"
dependencies = [
- "proc-macro2 1.0.13",
- "quote 1.0.7",
- "syn 1.0.39",
+ "convert_case",
+ "proc-macro2 1.0.24",
+ "quote 1.0.9",
+ "syn 1.0.65",
]
[[package]]
@@ -651,15 +500,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
[[package]]
-name = "digest"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
-dependencies = [
- "generic-array",
-]
-
-[[package]]
name = "dirs"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -667,50 +507,53 @@ checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901"
dependencies = [
"libc",
"redox_users",
- "winapi 0.3.8",
+ "winapi",
]
[[package]]
-name = "docopt"
-version = "1.1.0"
+name = "dtoa"
+version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f525a586d310c87df72ebcd98009e57f1cc030c8c268305287a476beb653969"
-dependencies = [
- "lazy_static",
- "regex",
- "serde",
- "strsim 0.9.3",
-]
+checksum = "88d7ed2934d741c6b37e33e3832298e8850b53fd2d2bea03873375596c7cea4e"
[[package]]
name = "either"
-version = "1.5.3"
+version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
+checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "ena"
-version = "0.13.1"
+version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8944dc8fa28ce4a38f778bd46bf7d923fe73eed5a439398507246c8e017e6f36"
+checksum = "d7402b94a93c24e742487327a7cd839dc9d36fec9de9fb25b09f2dae459f36c3"
dependencies = [
"log",
]
[[package]]
-name = "error-chain"
-version = "0.12.2"
+name = "encode_unicode"
+version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d371106cc88ffdfb1eabd7111e432da544f16f3e2d7bf1dfe8bf575f1df045cd"
+checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065"
dependencies = [
- "version_check 0.9.1",
+ "cfg-if 1.0.0",
]
[[package]]
-name = "fake-simd"
-version = "0.1.2"
+name = "encoding_rs_io"
+version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
+checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83"
+dependencies = [
+ "encoding_rs",
+]
[[package]]
name = "fern"
@@ -722,6 +565,18 @@ dependencies = [
]
[[package]]
+name = "filetime"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "redox_syscall 0.2.5",
+ "winapi",
+]
+
+[[package]]
name = "fixedbitset"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -740,32 +595,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
-name = "fuchsia-cprng"
-version = "0.1.1"
+name = "form_urlencoded"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
+checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+dependencies = [
+ "matches",
+ "percent-encoding",
+]
[[package]]
-name = "fuchsia-zircon"
-version = "0.3.3"
+name = "fsevent"
+version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
+checksum = "97f347202c95c98805c216f9e1df210e8ebaec9fdb2365700a43c10797a35e63"
dependencies = [
"bitflags",
- "fuchsia-zircon-sys",
+ "fsevent-sys",
]
[[package]]
-name = "fuchsia-zircon-sys"
-version = "0.3.3"
+name = "fsevent-sys"
+version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
+checksum = "ca6f5e6817058771c10f0eb0f05ddf1e35844266f972004fe8e4b21fda295bd5"
+dependencies = [
+ "libc",
+]
[[package]]
-name = "funty"
-version = "1.0.1"
+name = "fuchsia-cprng"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ba62103ce691c2fd80fbae2213dfdda9ce60804973ac6b6e97de818ea7f52c8"
+checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]]
name = "futf"
@@ -778,177 +640,86 @@ dependencies = [
]
[[package]]
-name = "futures"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e05b85ec287aac0dc34db7d4a569323df697f9c55b99b15d6b4ef8cde49f613"
-dependencies = [
- "futures-channel",
- "futures-core",
- "futures-executor",
- "futures-io",
- "futures-sink",
- "futures-task",
- "futures-util",
-]
-
-[[package]]
-name = "futures-channel"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5"
-dependencies = [
- "futures-core",
- "futures-sink",
-]
-
-[[package]]
-name = "futures-core"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399"
-
-[[package]]
-name = "futures-executor"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314"
-dependencies = [
- "futures-core",
- "futures-task",
- "futures-util",
-]
-
-[[package]]
-name = "futures-io"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789"
-
-[[package]]
-name = "futures-macro"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39"
-dependencies = [
- "proc-macro-hack",
- "proc-macro2 1.0.13",
- "quote 1.0.7",
- "syn 1.0.39",
-]
-
-[[package]]
-name = "futures-sink"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc"
-
-[[package]]
-name = "futures-task"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626"
-dependencies = [
- "once_cell",
-]
-
-[[package]]
-name = "futures-util"
-version = "0.3.5"
+name = "fuzzy-matcher"
+version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6"
+checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
dependencies = [
- "futures-channel",
- "futures-core",
- "futures-io",
- "futures-macro",
- "futures-sink",
- "futures-task",
- "memchr",
- "pin-project",
- "pin-utils",
- "proc-macro-hack",
- "proc-macro-nested",
- "slab",
+ "thread_local",
]
[[package]]
-name = "fuzzy-matcher"
-version = "0.3.4"
+name = "fxhash"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75a03d6d8629fcd151ece9d3a7f59a87fc38a620ab0290bf2888c2ad73821170"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
dependencies = [
- "thread_local",
+ "byteorder",
]
[[package]]
name = "generational-arena"
-version = "0.2.7"
+version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e40d0cee2e2fb4fba18b55a27bf96faf49fa86d49f178695bd3bf4500b156b4"
+checksum = "8e1d3b771574f62d0548cee0ad9057857e9fc25d7a3335f140c84f6acd0bf601"
dependencies = [
"cfg-if 0.1.10",
]
[[package]]
-name = "generic-array"
-version = "0.12.3"
+name = "getrandom"
+version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
- "typenum",
+ "cfg-if 1.0.0",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
-version = "0.1.14"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
+checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8"
dependencies = [
- "cfg-if 0.1.10",
+ "cfg-if 1.0.0",
"libc",
- "wasi",
+ "wasi 0.10.2+wasi-snapshot-preview1",
]
[[package]]
-name = "gif"
-version = "0.10.3"
+name = "hashbrown"
+version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "471d90201b3b223f3451cd4ad53e34295f16a1df17b1edf3736d47761c3981af"
+checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
dependencies = [
- "color_quant",
- "lzw",
+ "ahash",
]
[[package]]
-name = "half"
-version = "1.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d36fab90f82edc3c747f9d438e06cf0a491055896f2a279638bb5beed6c40177"
-
-[[package]]
name = "heck"
-version = "0.3.1"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
+checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "hermit-abi"
-version = "0.1.8"
+version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1010591b26bbfe835e9faeabeb11866061cc7dcebffd56ad7d0942d0e61aefd8"
+checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c"
dependencies = [
"libc",
]
[[package]]
name = "html2md"
-version = "0.2.10"
+version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc63d976a14675a0be3c02eb5d0d233f5d51cf6cb6e8277521abef883380f190"
+checksum = "65122619e0f12bd9c4d061415265d5835bc36499d3ae24a83b5451ea8ba28444"
dependencies = [
"html5ever 0.25.1",
"jni",
@@ -967,9 +738,9 @@ dependencies = [
"log",
"mac",
"markup5ever 0.9.0",
- "proc-macro2 1.0.13",
- "quote 1.0.7",
- "syn 1.0.39",
+ "proc-macro2 1.0.24",
+ "quote 1.0.9",
+ "syn 1.0.65",
]
[[package]]
@@ -980,17 +751,17 @@ checksum = "aafcf38a1a36118242d29b92e1b08ef84e67e4a5ed06e0a80be20e6a32bfed6b"
dependencies = [
"log",
"mac",
- "markup5ever 0.10.0",
- "proc-macro2 1.0.13",
- "quote 1.0.7",
- "syn 1.0.39",
+ "markup5ever 0.10.1",
+ "proc-macro2 1.0.24",
+ "quote 1.0.9",
+ "syn 1.0.65",
]
[[package]]
name = "idna"
-version = "0.2.0"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9"
+checksum = "89829a5d69c23d348314a7ac337fe39173b61149a9864deabd260983aed48c21"
dependencies = [
"matches",
"unicode-bidi",
@@ -998,30 +769,13 @@ dependencies = [
]
[[package]]
-name = "image"
-version = "0.23.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "543904170510c1b5fb65140485d84de4a57fddb2ed685481e9020ce3d2c9f64c"
-dependencies = [
- "bytemuck",
- "byteorder",
- "gif",
- "jpeg-decoder",
- "num-iter",
- "num-rational",
- "num-traits",
- "png",
- "scoped_threadpool",
- "tiff",
-]
-
-[[package]]
name = "indexmap"
-version = "1.3.2"
+version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292"
+checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3"
dependencies = [
- "autocfg 1.0.0",
+ "autocfg 1.0.1",
+ "hashbrown",
]
[[package]]
@@ -1034,15 +788,50 @@ dependencies = [
]
[[package]]
-name = "iovec"
-version = "0.1.4"
+name = "inotify"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d19f57db1baad9d09e43a3cd76dcf82ebdafd37d75c9498b87762dba77c93f15"
+dependencies = [
+ "bitflags",
+ "inotify-sys",
+ "libc",
+]
+
+[[package]]
+name = "inotify-sys"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
+checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]]
+name = "insta"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4a1b21a2971cea49ca4613c0e9fe8225ecaf5de64090fddc6002284726e9244"
+dependencies = [
+ "console",
+ "lazy_static",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "similar",
+ "uuid",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
name = "itertools"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1053,30 +842,30 @@ dependencies = [
[[package]]
name = "itertools"
-version = "0.9.0"
+version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
+checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319"
dependencies = [
"either",
]
[[package]]
name = "itoa"
-version = "0.4.5"
+version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e"
+checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
[[package]]
name = "jni"
-version = "0.14.0"
+version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1981310da491a4f0f815238097d0d43d8072732b5ae5f8bd0d8eadf5bf245402"
+checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec"
dependencies = [
"cesu8",
"combine",
- "error-chain",
"jni-sys",
"log",
+ "thiserror",
"walkdir",
]
@@ -1087,140 +876,133 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
-name = "jpeg-decoder"
-version = "0.1.18"
+name = "lalrpop"
+version = "0.19.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0256f0aec7352539102a9efbcb75543227b7ab1117e0f95450023af730128451"
+checksum = "46962a8c71b91c3524b117dfdd70844d4265a173c4c9109f98171aebdcf1195f"
dependencies = [
- "byteorder",
- "rayon",
+ "ascii-canvas",
+ "atty",
+ "bit-set",
+ "diff",
+ "ena",
+ "itertools 0.10.0",
+ "lalrpop-util",
+ "petgraph 0.5.1",
+ "pico-args",
+ "regex",
+ "regex-syntax",
+ "string_cache 0.8.1",
+ "term",
+ "tiny-keccak",
+ "unicode-xid 0.2.1",
]
[[package]]
-name = "js-sys"
-version = "0.3.36"
+name = "lalrpop-util"
+version = "0.19.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cb931d43e71f560c81badb0191596562bafad2be06a3f9025b845c847c60df5"
+checksum = "7a708007b751af124d09e9c5d97515257902bc6b486a56b40bcafd939e8ff467"
dependencies = [
- "wasm-bindgen",
+ "regex",
]
[[package]]
-name = "jsonrpc"
-version = "0.1.0"
+name = "lasso"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4efb7b456e95cc1ae2de7b18b1e4d791467b46f0a3d02464e5a16ea502091640"
dependencies = [
- "async-trait",
- "chashmap",
- "futures",
- "log",
- "serde",
- "serde_json",
- "serde_repr",
- "tokio",
+ "hashbrown",
]
[[package]]
-name = "jsonrpc-derive"
-version = "0.1.0"
-dependencies = [
- "quote 1.0.7",
- "syn 1.0.39",
-]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
-name = "kernel32-sys"
-version = "0.2.2"
+name = "libc"
+version = "0.2.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
-dependencies = [
- "winapi 0.2.8",
- "winapi-build",
-]
+checksum = "8916b1f6ca17130ec6568feccee27c156ad12037880833a3b842a823236502e7"
[[package]]
-name = "lalrpop"
-version = "0.18.1"
+name = "linked-hash-map"
+version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de5e019883a6e9734d093f34216a3857160c6bc2a9a1ec196a177aaa737c74af"
-dependencies = [
- "ascii-canvas",
- "atty",
- "bit-set",
- "diff",
- "docopt",
- "ena",
- "itertools 0.8.2",
- "lalrpop-util",
- "petgraph 0.4.13",
- "regex",
- "regex-syntax",
- "serde",
- "serde_derive",
- "sha2",
- "string_cache 0.7.5",
- "term",
- "unicode-xid 0.2.0",
-]
+checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
[[package]]
-name = "lalrpop-util"
-version = "0.18.1"
+name = "lock_api"
+version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d6e9bc1801eb54529fd6a020aaf9514e8193bb6b42d96d0fe7da99187efa93d"
+checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75"
dependencies = [
- "regex",
+ "scopeguard",
]
[[package]]
-name = "lazy_static"
-version = "1.4.0"
+name = "lock_api"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312"
+dependencies = [
+ "scopeguard",
+]
[[package]]
-name = "lexical-core"
-version = "0.6.2"
+name = "log"
+version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7043aa5c05dd34fb73b47acb8c3708eac428de4545ea3682ed2f11293ebd890"
+checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
- "arrayvec 0.4.12",
- "cfg-if 0.1.10",
- "rustc_version",
- "ryu",
- "static_assertions",
+ "cfg-if 1.0.0",
]
[[package]]
-name = "libc"
-version = "0.2.67"
+name = "logos"
+version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018"
+checksum = "427e2abca5be13136da9afdbf874e6b34ad9001dd70f2b103b083a85daa7b345"
+dependencies = [
+ "logos-derive",
+]
[[package]]
-name = "lock_api"
-version = "0.3.3"
+name = "logos-derive"
+version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79b2de95ecb4691949fea4716ca53cdbcfccb2c612e19644a8bad05edcf9f47b"
+checksum = "56a7d287fd2ac3f75b11f19a1c8a874a7d55744bd91f7a1b3e7cf87d4343c36d"
dependencies = [
- "scopeguard",
+ "beef",
+ "fnv",
+ "proc-macro2 1.0.24",
+ "quote 1.0.9",
+ "regex-syntax",
+ "syn 1.0.65",
+ "utf8-ranges",
]
[[package]]
-name = "log"
-version = "0.4.11"
+name = "lsp-server"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
+checksum = "6825d7042d5ca1825a366c40c9446928ec7b30e2be97243a13b164aee6583992"
dependencies = [
- "cfg-if 0.1.10",
+ "crossbeam-channel 0.5.1",
+ "log",
+ "serde",
+ "serde_json",
]
[[package]]
name = "lsp-types"
-version = "0.74.1"
+version = "0.89.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57c0e6a2b8837d27b29deb3f3e6dc1c6d2f57947677f9be1024e482ec5b59525"
+checksum = "07731ecd4ee0654728359a5b95e2a254c857876c04b85225496a35d60345daa7"
dependencies = [
- "base64 0.12.3",
"bitflags",
"serde",
"serde_json",
@@ -1229,12 +1011,6 @@ dependencies = [
]
[[package]]
-name = "lzw"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084"
-
-[[package]]
name = "mac"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1259,17 +1035,14 @@ dependencies = [
[[package]]
name = "markup5ever"
-version = "0.10.0"
+version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aae38d669396ca9b707bfc3db254bc382ddb94f57cc5c235f34623a669a01dab"
+checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd"
dependencies = [
"log",
"phf 0.8.0",
"phf_codegen 0.8.0",
- "serde",
- "serde_derive",
- "serde_json",
- "string_cache 0.8.0",
+ "string_cache 0.8.1",
"string_cache_codegen 0.5.1",
"tendril",
]
@@ -1281,7 +1054,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f015da43bcd8d4f144559a3423f4591d69b8ce0652c905374da7205df336ae2b"
dependencies = [
"html5ever 0.25.1",
- "markup5ever 0.10.0",
+ "markup5ever 0.10.1",
"tendril",
"xml5ever",
]
@@ -1300,9 +1073,9 @@ checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
[[package]]
name = "memchr"
-version = "2.3.3"
+version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
+checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
[[package]]
name = "memoffset"
@@ -1310,91 +1083,47 @@ version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa"
dependencies = [
- "autocfg 1.0.0",
+ "autocfg 1.0.1",
]
[[package]]
-name = "miniz_oxide"
-version = "0.3.6"
+name = "memoffset"
+version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa679ff6578b1cddee93d7e82e263b94a575e0bfced07284eb0c037c1d2416a5"
+checksum = "f83fb6581e8ed1f85fd45c116db8405483899489e38406156c25eb743554361d"
dependencies = [
- "adler32",
+ "autocfg 1.0.1",
]
[[package]]
name = "mio"
-version = "0.6.21"
+version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "302dec22bcf6bae6dfb69c647187f4b4d0fb6f535521f7bc022430ce8e12008f"
+checksum = "cf80d3e903b34e0bd7282b218398aec54e082c840d9baf8339e0080a0c542956"
dependencies = [
- "cfg-if 0.1.10",
- "fuchsia-zircon",
- "fuchsia-zircon-sys",
- "iovec",
- "kernel32-sys",
"libc",
"log",
- "miow 0.2.1",
- "net2",
- "slab",
- "winapi 0.2.8",
-]
-
-[[package]]
-name = "mio-named-pipes"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5e374eff525ce1c5b7687c4cef63943e7686524a387933ad27ca7ec43779cb3"
-dependencies = [
- "log",
- "mio",
- "miow 0.3.3",
- "winapi 0.3.8",
-]
-
-[[package]]
-name = "mio-uds"
-version = "0.6.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "966257a94e196b11bb43aca423754d87429960a768de9414f3691d6957abf125"
-dependencies = [
- "iovec",
- "libc",
- "mio",
-]
-
-[[package]]
-name = "miow"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919"
-dependencies = [
- "kernel32-sys",
- "net2",
- "winapi 0.2.8",
- "ws2_32-sys",
+ "miow",
+ "ntapi",
+ "winapi",
]
[[package]]
name = "miow"
-version = "0.3.3"
+version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "396aa0f2003d7df8395cb93e09871561ccc3e785f0acb369170e8cc74ddf9226"
+checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
dependencies = [
- "socket2",
- "winapi 0.3.8",
+ "winapi",
]
[[package]]
-name = "net2"
-version = "0.2.33"
+name = "multimap"
+version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88"
+checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
dependencies = [
- "cfg-if 0.1.10",
- "libc",
- "winapi 0.3.8",
+ "serde",
]
[[package]]
@@ -1404,12 +1133,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
[[package]]
-name = "nodrop"
-version = "0.1.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
-
-[[package]]
name = "nom"
version = "4.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1426,95 +1149,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
dependencies = [
"memchr",
- "version_check 0.9.1",
-]
-
-[[package]]
-name = "nom"
-version = "6.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4489ccc7d668957ddf64af7cd027c081728903afa6479d35da7e99bf5728f75f"
-dependencies = [
- "bitvec",
- "lexical-core",
- "memchr",
- "version_check 0.9.1",
+ "version_check 0.9.3",
]
[[package]]
-name = "num-integer"
-version = "0.1.42"
+name = "notify"
+version = "5.0.0-pre.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba"
+checksum = "46bbbcd078f1f00ddb7a9abe70b96e91229b44b0b3afdec610f8e5137f8f014b"
dependencies = [
- "autocfg 1.0.0",
- "num-traits",
-]
-
-[[package]]
-name = "num-iter"
-version = "0.1.40"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfb0800a0291891dd9f4fe7bd9c19384f98f7fbe0cd0f39a2c6b88b9868bbc00"
-dependencies = [
- "autocfg 1.0.0",
- "num-integer",
- "num-traits",
-]
-
-[[package]]
-name = "num-rational"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a5b4d7360f362cfb50dde8143501e6940b22f644be75a4cc90b2d81968908138"
-dependencies = [
- "autocfg 1.0.0",
- "num-integer",
- "num-traits",
+ "bitflags",
+ "crossbeam-channel 0.5.1",
+ "filetime",
+ "fsevent",
+ "fsevent-sys",
+ "inotify",
+ "libc",
+ "mio",
+ "walkdir",
+ "winapi",
]
[[package]]
-name = "num-traits"
-version = "0.2.11"
+name = "ntapi"
+version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096"
+checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
dependencies = [
- "autocfg 1.0.0",
+ "winapi",
]
[[package]]
name = "num_cpus"
-version = "1.12.0"
+version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46203554f085ff89c235cd12f7075f3233af9b11ed7c9e16dfe2560d03313ce6"
+checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
-name = "numtoa"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
-
-[[package]]
name = "once_cell"
-version = "1.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d"
-
-[[package]]
-name = "oorandom"
-version = "11.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebcec7c9c2a95cacc7cd0ecb89d8a8454eca13906f6deb55258ffff0adeb9405"
-
-[[package]]
-name = "opaque-debug"
-version = "0.2.3"
+version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
+checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
[[package]]
name = "ordermap"
@@ -1547,12 +1226,23 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252"
dependencies = [
- "lock_api",
+ "lock_api 0.3.4",
"parking_lot_core 0.6.2",
"rustc_version",
]
[[package]]
+name = "parking_lot"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb"
+dependencies = [
+ "instant",
+ "lock_api 0.4.2",
+ "parking_lot_core 0.8.3",
+]
+
+[[package]]
name = "parking_lot_core"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1560,8 +1250,8 @@ checksum = "4db1a8ccf734a7bce794cc19b3df06ed87ab2f3907036b693c68f56b4d4537fa"
dependencies = [
"libc",
"rand 0.4.6",
- "smallvec 0.6.13",
- "winapi 0.3.8",
+ "smallvec 0.6.14",
+ "winapi",
]
[[package]]
@@ -1573,10 +1263,24 @@ dependencies = [
"cfg-if 0.1.10",
"cloudabi",
"libc",
- "redox_syscall",
+ "redox_syscall 0.1.57",
"rustc_version",
- "smallvec 0.6.13",
- "winapi 0.3.8",
+ "smallvec 0.6.14",
+ "winapi",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018"
+dependencies = [
+ "cfg-if 1.0.0",
+ "instant",
+ "libc",
+ "redox_syscall 0.2.5",
+ "smallvec 1.6.1",
+ "winapi",
]
[[package]]
@@ -1603,8 +1307,6 @@ checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7"
dependencies = [
"fixedbitset 0.2.0",
"indexmap",
- "serde",
- "serde_derive",
]
[[package]]
@@ -1676,9 +1378,9 @@ dependencies = [
"phf_generator 0.8.0",
"phf_shared 0.8.0",
"proc-macro-hack",
- "proc-macro2 1.0.13",
- "quote 1.0.7",
- "syn 1.0.39",
+ "proc-macro2 1.0.24",
+ "quote 1.0.9",
+ "syn 1.0.65",
]
[[package]]
@@ -1696,70 +1398,20 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
dependencies = [
- "siphasher 0.3.1",
-]
-
-[[package]]
-name = "pin-project"
-version = "0.4.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81d480cb4e89522ccda96d0eed9af94180b7a5f93fb28f66e1fd7d68431663d1"
-dependencies = [
- "pin-project-internal",
-]
-
-[[package]]
-name = "pin-project-internal"
-version = "0.4.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a82996f11efccb19b685b14b5df818de31c1edcee3daa256ab5775dd98e72feb"
-dependencies = [
- "proc-macro2 1.0.13",
- "quote 1.0.7",
- "syn 1.0.39",
-]
-
-[[package]]
-name = "pin-project-lite"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "237844750cfbb86f67afe27eee600dfbbcb6188d734139b534cbfbf4f96792ae"
-
-[[package]]
-name = "pin-utils"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
-
-[[package]]
-name = "plotters"
-version = "0.2.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e3bb8da247d27ae212529352020f3e5ee16e83c0c258061d27b08ab92675eeb"
-dependencies = [
- "js-sys",
- "num-traits",
- "wasm-bindgen",
- "web-sys",
+ "siphasher 0.3.5",
]
[[package]]
-name = "png"
-version = "0.16.7"
+name = "pico-args"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfe7f9f1c730833200b134370e1d5098964231af8450bce9b78ee3ab5278b970"
-dependencies = [
- "bitflags",
- "crc32fast",
- "deflate",
- "miniz_oxide",
-]
+checksum = "7d7afeb98c5a10e0bffcc7fc16e105b04d06729fac5fd6384aebf7ff5cb5a67d"
[[package]]
name = "ppv-lite86"
-version = "0.2.6"
+version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b"
+checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
[[package]]
name = "precomputed-hash"
@@ -1769,46 +1421,33 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "proc-macro-error"
-version = "1.0.2"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98e9e4b82e0ef281812565ea4751049f1bdcdfccda7d3f459f2e138a40c08678"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
- "proc-macro2 1.0.13",
- "quote 1.0.7",
- "syn 1.0.39",
- "version_check 0.9.1",
+ "proc-macro2 1.0.24",
+ "quote 1.0.9",
+ "syn 1.0.65",
+ "version_check 0.9.3",
]
[[package]]
name = "proc-macro-error-attr"
-version = "1.0.2"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f5444ead4e9935abd7f27dc51f7e852a0569ac888096d5ec2499470794e2e53"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
- "proc-macro2 1.0.13",
- "quote 1.0.7",
- "syn 1.0.39",
- "syn-mid",
- "version_check 0.9.1",
+ "proc-macro2 1.0.24",
+ "quote 1.0.9",
+ "version_check 0.9.3",
]
[[package]]
name = "proc-macro-hack"
-version = "0.5.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ecd45702f76d6d3c75a80564378ae228a85f0b59d2f3ed43c91b4a69eb2ebfc5"
-dependencies = [
- "proc-macro2 1.0.13",
- "quote 1.0.7",
- "syn 1.0.39",
-]
-
-[[package]]
-name = "proc-macro-nested"
-version = "0.1.3"
+version = "0.5.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "369a6ed065f249a159e06c45752c780bda2fb53c995718f9e484d08daa9eb42e"
+checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]]
name = "proc-macro2"
@@ -1821,11 +1460,11 @@ dependencies = [
[[package]]
name = "proc-macro2"
-version = "1.0.13"
+version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53f5ffe53a6b28e37c9c1ce74893477864d64f74778a93a4beb43c8fa167f639"
+checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
dependencies = [
- "unicode-xid 0.2.0",
+ "unicode-xid 0.2.1",
]
[[package]]
@@ -1839,20 +1478,14 @@ dependencies = [
[[package]]
name = "quote"
-version = "1.0.7"
+version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
+checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
dependencies = [
- "proc-macro2 1.0.13",
+ "proc-macro2 1.0.24",
]
[[package]]
-name = "radium"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
-
-[[package]]
name = "rand"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1862,7 +1495,7 @@ dependencies = [
"libc",
"rand_core 0.3.1",
"rdrand",
- "winapi 0.3.8",
+ "winapi",
]
[[package]]
@@ -1881,7 +1514,7 @@ dependencies = [
"rand_os",
"rand_pcg 0.1.2",
"rand_xorshift",
- "winapi 0.3.8",
+ "winapi",
]
[[package]]
@@ -1890,7 +1523,7 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
- "getrandom",
+ "getrandom 0.1.16",
"libc",
"rand_chacha 0.2.2",
"rand_core 0.5.1",
@@ -1899,6 +1532,18 @@ dependencies = [
]
[[package]]
+name = "rand"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.0",
+ "rand_core 0.6.2",
+ "rand_hc 0.3.0",
+]
+
+[[package]]
name = "rand_chacha"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1919,6 +1564,16 @@ dependencies = [
]
[[package]]
+name = "rand_chacha"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.2",
+]
+
+[[package]]
name = "rand_core"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1939,7 +1594,16 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [
- "getrandom",
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7"
+dependencies = [
+ "getrandom 0.2.2",
]
[[package]]
@@ -1961,6 +1625,15 @@ dependencies = [
]
[[package]]
+name = "rand_hc"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73"
+dependencies = [
+ "rand_core 0.6.2",
+]
+
+[[package]]
name = "rand_isaac"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1977,7 +1650,7 @@ checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b"
dependencies = [
"libc",
"rand_core 0.4.2",
- "winapi 0.3.8",
+ "winapi",
]
[[package]]
@@ -1991,7 +1664,7 @@ dependencies = [
"libc",
"rand_core 0.4.2",
"rdrand",
- "winapi 0.3.8",
+ "winapi",
]
[[package]]
@@ -2028,7 +1701,7 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b0d8e0819fadc20c74ea8373106ead0600e3a67ef1fe8da56e39b9ae7275674"
dependencies = [
- "autocfg 1.0.0",
+ "autocfg 1.0.1",
"crossbeam-deque 0.8.0",
"either",
"rayon-core",
@@ -2040,9 +1713,9 @@ version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ab346ac5921dc62ffa9f89b7a773907511cdfa5490c572ae9be1be33e8afa4a"
dependencies = [
- "crossbeam-channel 0.5.0",
+ "crossbeam-channel 0.5.1",
"crossbeam-deque 0.8.0",
- "crossbeam-utils 0.8.0",
+ "crossbeam-utils 0.8.3",
"lazy_static",
"num_cpus",
]
@@ -2058,64 +1731,54 @@ dependencies = [
[[package]]
name = "redox_syscall"
-version = "0.1.56"
+version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
+checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]]
-name = "redox_termios"
-version = "0.1.1"
+name = "redox_syscall"
+version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
+checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9"
dependencies = [
- "redox_syscall",
+ "bitflags",
]
[[package]]
name = "redox_users"
-version = "0.3.4"
+version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431"
+checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
dependencies = [
- "getrandom",
- "redox_syscall",
+ "getrandom 0.1.16",
+ "redox_syscall 0.1.57",
"rust-argon2",
]
[[package]]
name = "regex"
-version = "1.3.9"
+version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6"
+checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
- "thread_local",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4"
-dependencies = [
- "byteorder",
]
[[package]]
name = "regex-syntax"
-version = "0.6.18"
+version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8"
+checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "remove_dir_all"
-version = "0.5.2"
+version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
dependencies = [
- "winapi 0.3.8",
+ "winapi",
]
[[package]]
@@ -2129,14 +1792,14 @@ dependencies = [
[[package]]
name = "rust-argon2"
-version = "0.7.0"
+version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017"
+checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb"
dependencies = [
- "base64 0.11.0",
+ "base64",
"blake2b_simd",
"constant_time_eq",
- "crossbeam-utils 0.7.2",
+ "crossbeam-utils 0.8.3",
]
[[package]]
@@ -2156,9 +1819,9 @@ dependencies = [
[[package]]
name = "ryu"
-version = "1.0.3"
+version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "535622e6be132bccd223f4bb2b8ac8d53cda3c7a6394944d3b2b33fb974f9d76"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "salsa"
@@ -2174,7 +1837,7 @@ dependencies = [
"rand 0.7.3",
"rustc-hash",
"salsa-macros",
- "smallvec 0.6.13",
+ "smallvec 0.6.14",
]
[[package]]
@@ -2184,9 +1847,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cac182212d3a1db75ddc42399ff1461b258a694b8318ee7e0baf6c976e39efee"
dependencies = [
"heck",
- "proc-macro2 1.0.13",
- "quote 1.0.7",
- "syn 1.0.39",
+ "proc-macro2 1.0.24",
+ "quote 1.0.9",
+ "syn 1.0.65",
]
[[package]]
@@ -2199,12 +1862,6 @@ dependencies = [
]
[[package]]
-name = "scoped_threadpool"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
-
-[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2227,39 +1884,29 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
-version = "1.0.115"
+version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e54c9a88f2da7238af84b5101443f0c0d0a3bbdc455e34a5c9497b1903ed55d5"
+checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
dependencies = [
"serde_derive",
]
[[package]]
-name = "serde_cbor"
-version = "0.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e18acfa2f90e8b735b2836ab8d538de304cbb6729a7360729ea5a895d15a622"
-dependencies = [
- "half",
- "serde",
-]
-
-[[package]]
name = "serde_derive"
-version = "1.0.115"
+version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "609feed1d0a73cc36a0182a840a9b37b4a82f0b1150369f0536a9e3f2a31dc48"
+checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43"
dependencies = [
- "proc-macro2 1.0.13",
- "quote 1.0.7",
- "syn 1.0.39",
+ "proc-macro2 1.0.24",
+ "quote 1.0.9",
+ "syn 1.0.65",
]
[[package]]
name = "serde_json"
-version = "1.0.57"
+version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c"
+checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
dependencies = [
"itoa",
"ryu",
@@ -2268,36 +1915,32 @@ dependencies = [
[[package]]
name = "serde_repr"
-version = "0.1.6"
+version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dc6b7951b17b051f3210b063f12cc17320e2fe30ae05b0fe2a3abb068551c76"
+checksum = "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5"
dependencies = [
- "proc-macro2 1.0.13",
- "quote 1.0.7",
- "syn 1.0.39",
+ "proc-macro2 1.0.24",
+ "quote 1.0.9",
+ "syn 1.0.65",
]
[[package]]
-name = "sha2"
-version = "0.8.1"
+name = "serde_yaml"
+version = "0.8.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27044adfd2e1f077f649f59deb9490d3941d674002f7d062870a60ebe9bd47a0"
+checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23"
dependencies = [
- "block-buffer",
- "digest",
- "fake-simd",
- "opaque-debug",
+ "dtoa",
+ "linked-hash-map",
+ "serde",
+ "yaml-rust",
]
[[package]]
-name = "signal-hook-registry"
-version = "1.2.0"
+name = "similar"
+version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94f478ede9f64724c5d173d7bb56099ec3e2d9fc2774aac65d34b8b890405f41"
-dependencies = [
- "arc-swap",
- "libc",
-]
+checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec"
[[package]]
name = "siphasher"
@@ -2307,60 +1950,39 @@ checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
[[package]]
name = "siphasher"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83da420ee8d1a89e640d0948c646c1c088758d3a3c538f943bfa97bdac17929d"
-
-[[package]]
-name = "slab"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
-
-[[package]]
-name = "smallvec"
-version = "0.4.5"
+version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f90c5e5fe535e48807ab94fc611d323935f39d4660c52b26b96446a7b33aef10"
+checksum = "cbce6d4507c7e4a3962091436e56e95290cb71fa302d0d270e32130b75fbff27"
[[package]]
name = "smallvec"
-version = "0.6.13"
+version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7b0758c52e15a8b5e3691eae6cc559f08eee9406e548a4477ba4e67770a82b6"
+checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0"
dependencies = [
"maybe-uninit",
]
[[package]]
name = "smallvec"
-version = "1.4.0"
+version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4"
+checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
[[package]]
-name = "socket2"
-version = "0.3.11"
+name = "smol_str"
+version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8b74de517221a2cb01a53349cf54182acdc31a074727d3079068448c0676d85"
+checksum = "6ca0f7ce3a29234210f0f4f0b56f8be2e722488b95cb522077943212da3b32eb"
dependencies = [
- "cfg-if 0.1.10",
- "libc",
- "redox_syscall",
- "winapi 0.3.8",
+ "serde",
]
[[package]]
name = "stable_deref_trait"
-version = "1.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8"
-
-[[package]]
-name = "static_assertions"
-version = "0.3.4"
+version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "string_cache"
@@ -2379,9 +2001,9 @@ dependencies = [
[[package]]
name = "string_cache"
-version = "0.8.0"
+version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2940c75beb4e3bf3a494cef919a747a2cb81e52571e212bfbd185074add7208a"
+checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a"
dependencies = [
"lazy_static",
"new_debug_unreachable",
@@ -2398,8 +2020,8 @@ checksum = "f0f45ed1b65bf9a4bf2f7b7dc59212d1926e9eaf00fa998988e420fd124467c6"
dependencies = [
"phf_generator 0.7.24",
"phf_shared 0.7.24",
- "proc-macro2 1.0.13",
- "quote 1.0.7",
+ "proc-macro2 1.0.24",
+ "quote 1.0.9",
"string_cache_shared",
]
@@ -2411,8 +2033,8 @@ checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97"
dependencies = [
"phf_generator 0.8.0",
"phf_shared 0.8.0",
- "proc-macro2 1.0.13",
- "quote 1.0.7",
+ "proc-macro2 1.0.24",
+ "quote 1.0.9",
]
[[package]]
@@ -2434,16 +2056,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
-name = "strsim"
-version = "0.9.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
-
-[[package]]
name = "structopt"
-version = "0.3.20"
+version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "126d630294ec449fae0b16f964e35bf3c74f940da9dca17ee9b905f7b3112eb8"
+checksum = "5277acd7ee46e63e5168a80734c9f6ee81b1367a7d8772a2d765df2a3705d28c"
dependencies = [
"clap",
"lazy_static",
@@ -2452,15 +2068,15 @@ dependencies = [
[[package]]
name = "structopt-derive"
-version = "0.4.13"
+version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "65e51c492f9e23a220534971ff5afc14037289de430e3c83f9daf6a1b6ae91e8"
+checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90"
dependencies = [
"heck",
"proc-macro-error",
- "proc-macro2 1.0.13",
- "quote 1.0.7",
- "syn 1.0.39",
+ "proc-macro2 1.0.24",
+ "quote 1.0.9",
+ "syn 1.0.65",
]
[[package]]
@@ -2494,51 +2110,34 @@ dependencies = [
[[package]]
name = "syn"
-version = "1.0.39"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "891d8d6567fe7c7f8835a3a98af4208f3846fba258c1bc3c31d6e506239f11f9"
-dependencies = [
- "proc-macro2 1.0.13",
- "quote 1.0.7",
- "unicode-xid 0.2.0",
-]
-
-[[package]]
-name = "syn-mid"
-version = "0.5.0"
+version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a"
+checksum = "f3a1d708c221c5a612956ef9f75b37e454e88d1f7b899fbd3a18d4252012d663"
dependencies = [
- "proc-macro2 1.0.13",
- "quote 1.0.7",
- "syn 1.0.39",
+ "proc-macro2 1.0.24",
+ "quote 1.0.9",
+ "unicode-xid 0.2.1",
]
[[package]]
-name = "tap"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36474e732d1affd3a6ed582781b3683df3d0563714c59c39591e8ff707cf078e"
-
-[[package]]
name = "tempfile"
-version = "3.1.0"
+version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
+checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
dependencies = [
- "cfg-if 0.1.10",
+ "cfg-if 1.0.0",
"libc",
- "rand 0.7.3",
- "redox_syscall",
+ "rand 0.8.3",
+ "redox_syscall 0.2.5",
"remove_dir_all",
- "winapi 0.3.8",
+ "winapi",
]
[[package]]
name = "tendril"
-version = "0.4.1"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "707feda9f2582d5d680d733e38755547a3e8fb471e7ba11452ecfd9ce93a5d3b"
+checksum = "a9ef557cb397a4f0a5a3a628f06515f78563f2209e64d47055d9dc6052bf5e33"
dependencies = [
"futf",
"mac",
@@ -2553,71 +2152,78 @@ checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42"
dependencies = [
"byteorder",
"dirs",
- "winapi 0.3.8",
+ "winapi",
]
[[package]]
-name = "termion"
-version = "1.5.5"
+name = "terminal_size"
+version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c22cec9d8978d906be5ac94bceb5a010d885c626c4c8855721a4dbd20e3ac905"
+checksum = "86ca8ced750734db02076f44132d802af0b33b09942331f4459dde8636fd2406"
dependencies = [
"libc",
- "numtoa",
- "redox_syscall",
- "redox_termios",
+ "winapi",
]
[[package]]
name = "texlab"
-version = "2.2.1"
+version = "3.0.1"
dependencies = [
- "aovec",
- "async-trait",
- "base64 0.13.0",
+ "anyhow",
"bibutils-sys",
"byteorder",
- "bytes",
- "cfg-if 0.1.10",
+ "cancellation",
+ "cfg-if 1.0.0",
"chashmap",
"citeproc",
"citeproc-db",
"citeproc-io",
- "criterion",
+ "crossbeam-channel 0.5.1",
"csl",
+ "cstree",
+ "derive_more",
+ "encoding_rs",
+ "encoding_rs_io",
"fern",
"fnv",
- "futures",
"fuzzy-matcher",
"html2md",
- "image",
"indoc",
- "itertools 0.9.0",
- "jsonrpc",
- "jsonrpc-derive",
+ "insta",
+ "itertools 0.10.0",
"lalrpop",
"lalrpop-util",
"log",
+ "logos",
+ "lsp-server",
"lsp-types",
- "nom 6.0.0",
+ "multimap",
+ "notify",
"once_cell",
"petgraph 0.5.1",
"rayon",
"regex",
+ "rustc-hash",
"serde",
"serde_json",
"serde_repr",
+ "smol_str",
"structopt",
"tempfile",
- "thiserror",
+ "threadpool",
"titlecase",
- "tokio",
- "tokio-util",
+ "typed-builder",
+ "unindent",
"url",
- "uuid",
]
[[package]]
+name = "text-size"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "288cb548dbe72b652243ea797201f3d481a0609a967980fcc5b2315ea811560a"
+
+[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2628,126 +2234,107 @@ dependencies = [
[[package]]
name = "thiserror"
-version = "1.0.19"
+version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b13f926965ad00595dd129fa12823b04bbf866e9085ab0a5f2b05b850fbfc344"
+checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
-version = "1.0.19"
+version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "893582086c2f98cde18f906265a65b5030a074b1046c674ae898be6519a7f479"
+checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0"
dependencies = [
- "proc-macro2 1.0.13",
- "quote 1.0.7",
- "syn 1.0.39",
+ "proc-macro2 1.0.24",
+ "quote 1.0.9",
+ "syn 1.0.65",
]
[[package]]
name = "thread_local"
-version = "1.0.1"
+version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
+checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd"
dependencies = [
- "lazy_static",
+ "once_cell",
]
[[package]]
-name = "tiff"
-version = "0.5.0"
+name = "threadpool"
+version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f3b8a87c4da944c3f27e5943289171ac71a6150a79ff6bacfff06d159dfff2f"
+checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
dependencies = [
- "byteorder",
- "lzw",
- "miniz_oxide",
+ "num_cpus",
]
[[package]]
name = "time"
-version = "0.1.42"
+version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
+checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
dependencies = [
"libc",
- "redox_syscall",
- "winapi 0.3.8",
+ "winapi",
]
[[package]]
-name = "tinytemplate"
-version = "1.1.0"
+name = "tiny-keccak"
+version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d3dc76004a03cec1c5932bca4cdc2e39aaa798e3f82363dd94f9adf6098c12f"
+checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
- "serde",
- "serde_json",
+ "crunchy",
]
[[package]]
-name = "titlecase"
-version = "1.1.0"
+name = "tinyvec"
+version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f565e410cfc24c2f2a89960b023ca192689d7f77d3f8d4f4af50c2d8affe1117"
+checksum = "317cca572a0e89c3ce0ca1f1bdc9369547fe318a683418e42ac8f59d14701023"
dependencies = [
- "lazy_static",
- "regex",
+ "tinyvec_macros",
]
[[package]]
-name = "tokio"
-version = "0.2.21"
+name = "tinyvec_macros"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d099fa27b9702bed751524694adbe393e18b36b204da91eb1cbbbbb4a5ee2d58"
-dependencies = [
- "bytes",
- "futures-core",
- "lazy_static",
- "libc",
- "memchr",
- "mio",
- "mio-named-pipes",
- "mio-uds",
- "pin-project-lite",
- "signal-hook-registry",
- "slab",
- "tokio-macros",
- "winapi 0.3.8",
-]
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
-name = "tokio-macros"
-version = "0.2.5"
+name = "titlecase"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0c3acc6aa564495a0f2e1d59fab677cd7f81a19994cfc7f3ad0e64301560389"
+checksum = "f565e410cfc24c2f2a89960b023ca192689d7f77d3f8d4f4af50c2d8affe1117"
dependencies = [
- "proc-macro2 1.0.13",
- "quote 1.0.7",
- "syn 1.0.39",
+ "lazy_static",
+ "regex",
]
[[package]]
-name = "tokio-util"
-version = "0.3.1"
+name = "triomphe"
+version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499"
+checksum = "6e9d872053cf9e5a833d8c1dd772cdc38ab66a908129d6f73c049c986161d07c"
dependencies = [
- "bytes",
- "futures-core",
- "futures-sink",
- "log",
- "pin-project-lite",
- "tokio",
+ "memoffset 0.5.6",
+ "serde",
+ "stable_deref_trait",
]
[[package]]
-name = "typenum"
-version = "1.11.2"
+name = "typed-builder"
+version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9"
+checksum = "345426c7406aa355b60c5007c79a2d1f5b605540072795222f17f6443e6a9c6f"
+dependencies = [
+ "proc-macro2 1.0.24",
+ "quote 1.0.9",
+ "syn 1.0.65",
+]
[[package]]
name = "ucd-trie"
@@ -2816,24 +2403,24 @@ dependencies = [
[[package]]
name = "unicode-normalization"
-version = "0.1.12"
+version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4"
+checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef"
dependencies = [
- "smallvec 1.4.0",
+ "tinyvec",
]
[[package]]
name = "unicode-segmentation"
-version = "1.6.0"
+version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
+checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
[[package]]
name = "unicode-width"
-version = "0.1.7"
+version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479"
+checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
[[package]]
name = "unicode-xid"
@@ -2843,9 +2430,9 @@ checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
[[package]]
name = "unicode-xid"
-version = "0.2.0"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
+checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "unindent"
@@ -2854,20 +2441,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7"
[[package]]
-name = "unreachable"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"
-dependencies = [
- "void",
-]
-
-[[package]]
name = "url"
-version = "2.1.1"
+version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb"
+checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
dependencies = [
+ "form_urlencoded",
"idna",
"matches",
"percent-encoding",
@@ -2876,18 +2455,21 @@ dependencies = [
[[package]]
name = "utf-8"
-version = "0.7.5"
+version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "utf8-ranges"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ae116fef2b7fea257ed6440d3cfcff7f190865f170cdad00bb6465bf18ecba"
[[package]]
name = "uuid"
-version = "0.8.1"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11"
-dependencies = [
- "rand 0.7.3",
-]
+checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
[[package]]
name = "v_escape"
@@ -2905,9 +2487,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2ca2a14bc3fc5b64d188b087a7d3a927df87b152e941ccfbc66672e20c467ae"
dependencies = [
"nom 4.2.3",
- "proc-macro2 1.0.13",
- "quote 1.0.7",
- "syn 1.0.39",
+ "proc-macro2 1.0.24",
+ "quote 1.0.9",
+ "syn 1.0.65",
]
[[package]]
@@ -2922,9 +2504,9 @@ dependencies = [
[[package]]
name = "vec_map"
-version = "0.8.1"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version_check"
@@ -2934,24 +2516,18 @@ checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
[[package]]
name = "version_check"
-version = "0.9.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce"
-
-[[package]]
-name = "void"
-version = "1.0.2"
+version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
+checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "walkdir"
-version = "2.3.1"
+version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d"
+checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
dependencies = [
"same-file",
- "winapi 0.3.8",
+ "winapi",
"winapi-util",
]
@@ -2962,92 +2538,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
-name = "wasm-bindgen"
-version = "0.2.59"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3557c397ab5a8e347d434782bcd31fc1483d927a6826804cec05cc792ee2519d"
-dependencies = [
- "cfg-if 0.1.10",
- "wasm-bindgen-macro",
-]
-
-[[package]]
-name = "wasm-bindgen-backend"
-version = "0.2.59"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0da9c9a19850d3af6df1cb9574970b566d617ecfaf36eb0b706b6f3ef9bd2f8"
-dependencies = [
- "bumpalo",
- "lazy_static",
- "log",
- "proc-macro2 1.0.13",
- "quote 1.0.7",
- "syn 1.0.39",
- "wasm-bindgen-shared",
-]
-
-[[package]]
-name = "wasm-bindgen-macro"
-version = "0.2.59"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f6fde1d36e75a714b5fe0cffbb78978f222ea6baebb726af13c78869fdb4205"
-dependencies = [
- "quote 1.0.7",
- "wasm-bindgen-macro-support",
-]
-
-[[package]]
-name = "wasm-bindgen-macro-support"
-version = "0.2.59"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25bda4168030a6412ea8a047e27238cadf56f0e53516e1e83fec0a8b7c786f6d"
-dependencies = [
- "proc-macro2 1.0.13",
- "quote 1.0.7",
- "syn 1.0.39",
- "wasm-bindgen-backend",
- "wasm-bindgen-shared",
-]
-
-[[package]]
-name = "wasm-bindgen-shared"
-version = "0.2.59"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc9f36ad51f25b0219a3d4d13b90eb44cd075dff8b6280cca015775d7acaddd8"
-
-[[package]]
-name = "web-sys"
-version = "0.3.36"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "721c6263e2c66fd44501cc5efbfa2b7dfa775d13e4ea38c46299646ed1f9c70a"
-dependencies = [
- "js-sys",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "winapi"
-version = "0.2.8"
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "winapi"
-version = "0.3.8"
+version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
-name = "winapi-build"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
-
-[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3055,11 +2561,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
-version = "0.1.3"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ccfbf554c6ad11084fb7517daca16cfdcaccbdadba4fc336f032a8b12c2ad80"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
- "winapi 0.3.8",
+ "winapi",
]
[[package]]
@@ -3069,22 +2575,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
-name = "ws2_32-sys"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
-dependencies = [
- "winapi 0.2.8",
- "winapi-build",
-]
-
-[[package]]
-name = "wyz"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
-
-[[package]]
name = "xml5ever"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3092,7 +2582,7 @@ checksum = "0b1b52e6e8614d4a58b8e70cf51ec0cc21b256ad8206708bcff8139b5bbd6a59"
dependencies = [
"log",
"mac",
- "markup5ever 0.10.0",
+ "markup5ever 0.10.1",
"time",
]
@@ -3101,3 +2591,12 @@ name = "xmlparser"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8110496c5bcc0d966b0b2da38d5a791aa139eeb0b80e7840a7463c2b806921eb"
+
+[[package]]
+name = "yaml-rust"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
+dependencies = [
+ "linked-hash-map",
+]
diff --git a/support/texlab/Cargo.toml b/support/texlab/Cargo.toml
index 3855dbf32d..56317c7f9c 100644
--- a/support/texlab/Cargo.toml
+++ b/support/texlab/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "texlab"
description = "LaTeX Language Server"
-version = "2.2.1"
+version = "3.0.1"
license = "GPL-3.0"
readme = "README.md"
authors = [
@@ -11,101 +11,66 @@ authors = [
edition = "2018"
homepage = "https://texlab.netlify.app"
-[workspace]
-members = ["crates/*"]
-
[lib]
doctest = false
[features]
-default = [
- "citation",
- "server",
- "test",
-]
-citation = [
- "bibutils-sys",
- "citeproc",
- "citeproc-db",
- "citeproc-io",
- "csl",
- "fnv",
- "html2md",
- "lalrpop/lexer",
- "lalrpop-util",
-]
-server = [
- "bytes",
- "jsonrpc",
- "jsonrpc-derive",
- "nom",
- "tokio-util/codec",
-]
-test = [
- "server",
- "aovec",
-]
-
-[[bin]]
-name = "texlab"
-required-features = ["server"]
-
-[[test]]
-name = "integration"
-required-features = ["test"]
+default = ["citation", "completion"]
+completion = ["fuzzy-matcher"]
+citation = ["bibutils-sys", "citeproc", "citeproc-db", "citeproc-io", "csl", "fnv", "html2md", "lalrpop-util", "lalrpop"]
+semantic = []
[dependencies]
-async-trait = "0.1"
-aovec = { version = "1.1", optional = true }
-base64 = "0.13"
-bibutils-sys = { version = "0.1", optional = true }
-byteorder = "1.3"
-bytes = { version = "0.5", optional = true }
-chashmap = "2.2"
-cfg-if = "0.1"
+anyhow = "1.0.40"
+bibutils-sys = { version = "0.1.1", optional = true }
+byteorder = "1.4.3"
+cancellation = "0.1"
+cfg-if = "1.0.0"
+chashmap = "2.2.2"
citeproc = { git = "https://github.com/cormacrelf/citeproc-rs", rev = "68c7a7c16088b5d7751641ab37299ad064cdb221", optional = true }
citeproc-db = { git = "https://github.com/cormacrelf/citeproc-rs", rev = "68c7a7c16088b5d7751641ab37299ad064cdb221", optional = true }
citeproc-io = { git = "https://github.com/cormacrelf/citeproc-rs", rev = "68c7a7c16088b5d7751641ab37299ad064cdb221", optional = true }
csl = { git = "https://github.com/cormacrelf/citeproc-rs", rev = "68c7a7c16088b5d7751641ab37299ad064cdb221", optional = true }
+crossbeam-channel = "0.5.1"
+cstree = "0.5.0"
+derive_more = "0.99.14"
+encoding_rs = "0.8.28"
+encoding_rs_io = "0.1.7"
fern = "0.6"
fnv = { version = "1.0", optional = true }
-futures = "0.3"
-fuzzy-matcher = "0.3"
-html2md = { version = "0.2", optional = true }
-image = "0.23"
-itertools = "0.9"
-jsonrpc = { path = "crates/jsonrpc", optional = true }
-jsonrpc-derive = { path = "crates/jsonrpc_derive", optional = true }
-lalrpop-util = { version = "0.18", optional = true }
-log = "0.4"
-lsp-types = { version = "0.74.1" }
-nom = { version = "6.0", optional = true }
-once_cell = "1.4"
-petgraph = { version = "0.5", features = ["serde-1"] }
-rayon = "1.5"
-regex = "1.3"
-serde = { version = "1.0", features = ["derive", "rc"] }
-serde_json = "1.0"
-serde_repr = "0.1"
+fuzzy-matcher = { version = "0.3.7", optional = true }
+html2md = { version = "0.2.11", optional = true }
+itertools = "0.10.0"
+lalrpop-util = { version = "0.19.5", optional = true }
+log = "0.4.14"
+logos = "0.12.0"
+lsp-server = "0.5"
+lsp-types = "0.89.0"
+multimap = "0.8.3"
+notify = "5.0.0-pre.8"
+once_cell = "1.7.2"
+petgraph = "0.5.1"
+rayon = "1.5.0"
+regex = "1.5.4"
+rustc-hash = "1.1.0"
+serde = "1.0.126"
+serde_json = "1.0.64"
+serde_repr = "0.1.7"
+smol_str = { version = "0.1.17", features = ["serde"] }
structopt = "0.3"
-tempfile = "3.1"
-thiserror = "1.0"
-titlecase = "1.1"
-tokio = { version = "0.2", features = ["fs", "io-std", "macros", "process", "rt-core", "time"] }
-tokio-util = { version = "0.3", optional = true }
-url = "2.1"
-uuid = { version = "0.8", features = ["v4"] }
+tempfile = "3.2.0"
+threadpool = "1.8.1"
+titlecase = "1.1.0"
+url = "2.2.2"
[dev-dependencies]
-criterion = "0.3"
-indoc = "1.0"
+indoc = "1.0.3"
+insta = "1.7.1"
+typed-builder = "0.9.0"
+unindent = "0.1.7"
[build-dependencies]
-lalrpop = { version = "0.18", optional = true }
+lalrpop = { version = "0.19.5", features = ["lexer"], optional = true }
[profile.release]
-lto = true
-
-[[bench]]
-name = "bench_main"
-harness = false
+lto = true \ No newline at end of file
diff --git a/support/texlab/README.md b/support/texlab/README.md
index f07fcd0bd9..b2edc3f2b4 100644
--- a/support/texlab/README.md
+++ b/support/texlab/README.md
@@ -15,19 +15,33 @@
A cross-platform implementation of the [Language Server Protocol](https://microsoft.github.io/language-server-protocol)
providing rich cross-editing support for the [LaTeX](https://www.latex-project.org/) typesetting system.
-We provide an [extension](https://github.com/latex-lsp/texlab-vscode) for [Visual Studio Code](https://code.visualstudio.com).
+The server may be used with [any editor that implements the Language Server Protocol](https://microsoft.github.io/language-server-protocol/implementors/tools/).
-Learn more about the project on our [website](https://texlab.netlify.app).
+![Demo](docs/demo.gif)
## Getting Started
-See the [installation chapter](https://texlab.netlify.app/docs) from our docs.
+If your editor extension like does not install the TexLab server automatically,
+you will need to install it manually.
+We provide [precompiled binaries](https://github.com/latex-lsp/texlab/releases)
+for Windows, Linux and macOS.
+Alternatively, you can build TexLab from source or install it using your package manager.
+For a list of supported package managers, you can take a look at [Repology](https://repology.org/project/texlab/versions).
-## Building from Source
+### Requirements
+
+A [TeX distribution](https://www.latex-project.org/get/#tex-distributions) is _not_ strictly required
+to use the server but TexLab cannot compile your documents without one.
+TexLab supports compiling using [Tectonic](https://tectonic-typesetting.github.io/).
+For an example configuration, please see [here](docs/tectonic.md).
+
+On Windows, you may need to install [Microsoft Visual C++ Redistributable for Visual Studio 2015](https://www.microsoft.com/en-US/download/details.aspx?id=48145).
+
+### Building from Source
You will need to install the following dependencies to compile the server:
-- [Rust (>= 1.39)](https://rustup.rs/)
+- A recent, stable version of [Rust](https://rustup.rs/)
Then run the following command in the project folder:
@@ -35,13 +49,22 @@ Then run the following command in the project folder:
cargo build --release
```
-Alternatively, Rust users can run the following command
-without having to clone this repository:
+Alternatively, you can run the following command
+without having to clone this repository and make TexLab available in your PATH:
```shell
-cargo install --git --locked https://github.com/latex-lsp/texlab.git
+cargo install --git https://github.com/latex-lsp/texlab.git --locked
```
+## Usage
+
+After installing an editor extension, you can simply start editing LaTeX files. All editing features work out-of-the-box over all files in the currently opened workspace.
+There is no need for magic comments like `%!TEX root`
+and TexLab should figure out the dependencies of a file on its own.
+Note that you may need to set the `texlab.rootDirectory` option for some multi-folder projects.
+
+TexLab features a variety of [options](docs/options.md) which can be used to configure features like building or [forward search](docs/previewing.md).
+
## Development
You can create a debug build by building the server without the `--release` flag.
@@ -56,6 +79,8 @@ cargo test
in the project folder.
+For a list of custom messages, please see [here](docs/custom_messages.md).
+
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us.
diff --git a/support/texlab/benches/bench_main.rs b/support/texlab/benches/bench_main.rs
deleted file mode 100644
index a73dbc1aa9..0000000000
--- a/support/texlab/benches/bench_main.rs
+++ /dev/null
@@ -1,7 +0,0 @@
-mod benchmarks;
-
-use criterion::criterion_main;
-
-criterion_main! {
- benchmarks::completion::benches
-}
diff --git a/support/texlab/benches/benchmarks/completion.rs b/support/texlab/benches/benchmarks/completion.rs
deleted file mode 100644
index 4653102460..0000000000
--- a/support/texlab/benches/benchmarks/completion.rs
+++ /dev/null
@@ -1,135 +0,0 @@
-use criterion::criterion_group;
-use criterion::{BenchmarkId, Criterion};
-use futures::executor::block_on;
-use indoc::indoc;
-use std::time::Duration;
-use texlab::{completion::CompletionProvider, feature::FeatureTester};
-
-fn criterion_benchmark(criterion: &mut Criterion) {
- criterion.bench_with_input(
- BenchmarkId::new("Completion", "LaTeX Word"),
- &LATEX_CODE,
- |b, code| {
- b.iter(|| {
- block_on(async {
- FeatureTester::new()
- .file("main.tex", *code)
- .main("main.tex")
- .position(0, 0)
- .test_completion(CompletionProvider::new())
- .await
- });
- });
- },
- );
-
- criterion.bench_with_input(
- BenchmarkId::new("Completion", "LaTeX Command (Filtered)"),
- &LATEX_CODE,
- |b, code| {
- b.iter(|| {
- block_on(async {
- FeatureTester::new()
- .file("main.tex", *code)
- .main("main.tex")
- .position(0, 1)
- .test_completion(CompletionProvider::new())
- .await
- });
- });
- },
- );
-
- criterion.bench_with_input(
- BenchmarkId::new("Completion", "LaTeX Command (Unfiltered)"),
- &LATEX_CODE,
- |b, code| {
- b.iter(|| {
- block_on(async {
- FeatureTester::new()
- .file("main.tex", *code)
- .main("main.tex")
- .position(30, 1)
- .test_completion(CompletionProvider::new())
- .await
- });
- });
- },
- );
-
- criterion.bench_with_input(
- BenchmarkId::new("Completion", "LaTeX Environment"),
- &LATEX_CODE,
- |b, code| {
- b.iter(|| {
- block_on(async {
- FeatureTester::new()
- .file("main.tex", *code)
- .main("main.tex")
- .position(9, 9)
- .test_completion(CompletionProvider::new())
- .await
- });
- })
- },
- );
-
- criterion.bench_with_input(
- BenchmarkId::new("Completion", "LaTeX Label"),
- &LATEX_CODE,
- |b, code| {
- b.iter(|| {
- block_on(async {
- FeatureTester::new()
- .file("main.tex", *code)
- .main("main.tex")
- .position(15, 7)
- .test_completion(CompletionProvider::new())
- .await
- });
- })
- },
- );
-}
-
-criterion_group! {
- name = benches;
- config = Criterion::default().measurement_time(Duration::from_secs(60));
- targets = criterion_benchmark
-}
-
-static LATEX_CODE: &str = indoc!(
- r#"
- \documentclass{article}
- \usepackage{amsmath}
- \usepackage{lipsum}
- \usepackage{geometry}
- \usepackage[utf8]{inputenc}
- \newcommand{\foo}{foo}
- \DeclareMathOperator{\bar}{bar}
- \include{child1}
- \input{child2.tex}
- \begin{document}
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec fermentum lectus placerat, suscipit ligula quis.
- \begin{equation*}\label{eq:foo}
- e^{i \pi} + 1 = 0
- \end{equation*}
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec fermentum lectus placerat, suscipit ligula quis.
- \ref{eq:foo}
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec fermentum lectus placerat, suscipit ligula quis.
- \section{Foo}\label{sec:foo}
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec fermentum lectus placerat, suscipit ligula quis.
- \subsection{Bar}\label{sec:bar}
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec fermentum lectus placerat, suscipit ligula quis.
- \include{foo}
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec fermentum lectus placerat, suscipit ligula quis.
- \input{bar.tex}
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec fermentum lectus placerat, suscipit ligula quis.
- \cite{foo, bar, baz}
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec fermentum lectus placerat, suscipit ligula quis.
- \nocite{*}
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec fermentum lectus placerat, suscipit ligula quis.
- \end{document}
- \
- "#
-);
diff --git a/support/texlab/benches/benchmarks/mod.rs b/support/texlab/benches/benchmarks/mod.rs
deleted file mode 100644
index c893d109ab..0000000000
--- a/support/texlab/benches/benchmarks/mod.rs
+++ /dev/null
@@ -1 +0,0 @@
-pub mod completion;
diff --git a/support/texlab/build.rs b/support/texlab/build.rs
index 5283240997..e14ac6747e 100644
--- a/support/texlab/build.rs
+++ b/support/texlab/build.rs
@@ -1,5 +1,5 @@
fn main() {
- println!("cargo:rerun-if-changed=src/citeproc/name/parser.lalrpop");
+ println!("cargo:rerun-if-changed=src/citation/name/parser.lalrpop");
#[cfg(feature = "citation")]
lalrpop::process_root().unwrap();
diff --git a/support/texlab/crates/jsonrpc/Cargo.toml b/support/texlab/crates/jsonrpc/Cargo.toml
deleted file mode 100644
index c5cec72074..0000000000
--- a/support/texlab/crates/jsonrpc/Cargo.toml
+++ /dev/null
@@ -1,20 +0,0 @@
-[package]
-name = "jsonrpc"
-version = "0.1.0"
-authors = [
- "Eric Förster <eric.foerster@outlook.com>",
- "Patrick Förster <patrick.foerster@outlook.de>"]
-edition = "2018"
-
-[lib]
-doctest = false
-
-[dependencies]
-async-trait = "0.1"
-chashmap = "2.2"
-futures = "0.3"
-log = "0.4"
-serde = { version = "1.0", features = ["derive", "rc"] }
-serde_json = "1.0"
-serde_repr = "0.1"
-tokio = { version = "0.2", features = ["rt-core"] }
diff --git a/support/texlab/crates/jsonrpc/src/client.rs b/support/texlab/crates/jsonrpc/src/client.rs
deleted file mode 100644
index 7d714283c6..0000000000
--- a/support/texlab/crates/jsonrpc/src/client.rs
+++ /dev/null
@@ -1,77 +0,0 @@
-use super::types::*;
-use async_trait::async_trait;
-use chashmap::CHashMap;
-use futures::{
- channel::{mpsc, oneshot},
- prelude::*,
-};
-use serde::Serialize;
-use serde_json::json;
-use std::sync::atomic::{AtomicU64, Ordering};
-
-pub type Result<T> = std::result::Result<T, Error>;
-
-#[async_trait]
-pub trait ResponseHandler {
- async fn handle(&self, response: Response);
-}
-
-#[derive(Debug)]
-pub struct Client {
- output: mpsc::Sender<String>,
- request_id: AtomicU64,
- senders_by_id: CHashMap<Id, oneshot::Sender<Result<serde_json::Value>>>,
-}
-
-impl Client {
- pub fn new(output: mpsc::Sender<String>) -> Self {
- Self {
- output,
- request_id: AtomicU64::new(0),
- senders_by_id: CHashMap::new(),
- }
- }
-
- pub async fn send_request<T: Serialize>(
- &self,
- method: String,
- params: T,
- ) -> Result<serde_json::Value> {
- let id = self.request_id.fetch_add(1, Ordering::SeqCst);
- let request = Request::new(method, json!(params), Id::Number(id));
-
- let (result_tx, result_rx) = oneshot::channel();
- self.senders_by_id.insert(request.id.clone(), result_tx);
- self.send(Message::Request(request)).await;
-
- result_rx.await.unwrap()
- }
-
- pub async fn send_notification<T: Serialize>(&self, method: String, params: T) {
- let notification = Notification::new(method, json!(params));
- self.send(Message::Notification(notification)).await;
- }
-
- async fn send(&self, message: Message) {
- let mut output = self.output.clone();
- let json = serde_json::to_string(&message).unwrap();
- output.send(json).await.unwrap();
- }
-}
-
-#[async_trait]
-impl ResponseHandler for Client {
- async fn handle(&self, response: Response) {
- let id = response.id.expect("Expected response with id");
- let result = match response.error {
- Some(why) => Err(why),
- None => Ok(response.result.unwrap_or(serde_json::Value::Null)),
- };
-
- let result_tx = self
- .senders_by_id
- .remove(&id)
- .expect("Unexpected response received");
- result_tx.send(result).unwrap();
- }
-}
diff --git a/support/texlab/crates/jsonrpc/src/lib.rs b/support/texlab/crates/jsonrpc/src/lib.rs
deleted file mode 100644
index 3a771e6ba5..0000000000
--- a/support/texlab/crates/jsonrpc/src/lib.rs
+++ /dev/null
@@ -1,67 +0,0 @@
-pub mod client;
-pub mod server;
-mod types;
-
-pub use self::{
- client::{Client, ResponseHandler},
- server::{handle_notification, handle_request, Middleware, RequestHandler},
- types::*,
-};
-
-use futures::{channel::mpsc, prelude::*};
-use log::error;
-use std::sync::Arc;
-
-#[derive(Debug)]
-pub struct MessageHandler<S, C> {
- pub server: Arc<S>,
- pub client: Arc<C>,
- pub output: mpsc::Sender<String>,
-}
-
-impl<S, C> MessageHandler<S, C>
-where
- S: RequestHandler + Middleware + Send + Sync + 'static,
- C: ResponseHandler + Send + Sync + 'static,
-{
- pub async fn handle(&mut self, json: &str) {
- self.server.before_message().await;
-
- match serde_json::from_str(json).map_err(|_| Error::parse_error()) {
- Ok(Message::Request(request)) => {
- let server = Arc::clone(&self.server);
- let mut output = self.output.clone();
- tokio::spawn(async move {
- let response = server.handle_request(request).await;
- if let Some(error) = response.error.as_ref() {
- error!("{:?}", error);
- }
- let json = serde_json::to_string(&response).unwrap();
- output.send(json).await.unwrap();
- server.after_message().await;
- });
- }
- Ok(Message::Notification(notification)) => {
- self.server.handle_notification(notification).await;
- self.after_message();
- }
- Ok(Message::Response(response)) => {
- self.client.handle(response).await;
- self.after_message();
- }
- Err(why) => {
- let response = Response::error(why, None);
- let json = serde_json::to_string(&response).unwrap();
- self.output.send(json).await.unwrap();
- self.after_message();
- }
- };
- }
-
- fn after_message(&self) {
- let server = Arc::clone(&self.server);
- tokio::spawn(async move {
- server.after_message().await;
- });
- }
-}
diff --git a/support/texlab/crates/jsonrpc/src/server.rs b/support/texlab/crates/jsonrpc/src/server.rs
deleted file mode 100644
index ce34dc8b60..0000000000
--- a/support/texlab/crates/jsonrpc/src/server.rs
+++ /dev/null
@@ -1,131 +0,0 @@
-use super::types::*;
-use async_trait::async_trait;
-use futures::prelude::*;
-use serde::{de::DeserializeOwned, Serialize};
-use serde_json::json;
-
-pub type Result<T> = std::result::Result<T, String>;
-
-#[async_trait]
-pub trait RequestHandler {
- async fn handle_request(&self, request: Request) -> Response;
-
- async fn handle_notification(&self, notification: Notification);
-}
-
-#[async_trait]
-pub trait Middleware {
- async fn before_message(&self);
-
- async fn after_message(&self);
-}
-
-pub async fn handle_request<'a, H, F, I, O>(request: Request, handler: H) -> Response
-where
- H: Fn(I) -> F + Send + Sync + 'a,
- F: Future<Output = Result<O>> + Send,
- I: DeserializeOwned + Send,
- O: Serialize,
-{
- let handle = |json| async move {
- let params: I = serde_json::from_value(json).map_err(|_| Error::deserialize_error())?;
- let result = handler(params).await.map_err(Error::internal_error)?;
- Ok(result)
- };
-
- match handle(request.params).await {
- Ok(result) => Response::result(json!(result), request.id),
- Err(error) => Response::error(error, Some(request.id)),
- }
-}
-
-pub async fn handle_notification<'a, H, F, I>(notification: Notification, handler: H)
-where
- H: Fn(I) -> F + Send + Sync + 'a,
- F: Future<Output = ()> + Send,
- I: DeserializeOwned + Send,
-{
- let error = Error::deserialize_error().message;
- let params = serde_json::from_value(notification.params).expect(&error);
- handler(params).await;
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- const METHOD_NAME: &str = "foo";
-
- async fn increment(i: i32) -> Result<i32> {
- Ok(i + 1)
- }
-
- async fn panic(_params: ()) {
- panic!("success");
- }
-
- fn setup_request<T: Serialize>(value: T) -> Request {
- Request {
- jsonrpc: PROTOCOL_VERSION.to_owned(),
- params: json!(value),
- method: METHOD_NAME.to_owned(),
- id: Id::Number(0),
- }
- }
-
- fn setup_notification() -> Notification {
- Notification {
- jsonrpc: PROTOCOL_VERSION.to_owned(),
- method: METHOD_NAME.to_owned(),
- params: json!(()),
- }
- }
-
- #[tokio::test]
- async fn request_valid() {
- let value = 42;
- let request = setup_request(value);
-
- let response = handle_request(request.clone(), increment).await;
- let expected = Response {
- jsonrpc: request.jsonrpc,
- result: Some(json!(increment(value).await.unwrap())),
- error: None,
- id: Some(request.id),
- };
-
- assert_eq!(response, expected);
- }
-
- #[tokio::test]
- async fn request_invalid_params() {
- let request = setup_request((0, 0));
-
- let response = handle_request(request.clone(), increment).await;
- let expected = Response {
- jsonrpc: request.jsonrpc.clone(),
- result: None,
- error: Some(Error::deserialize_error()),
- id: Some(request.id),
- };
-
- assert_eq!(response, expected);
- }
-
- #[tokio::test]
- #[should_panic(expected = "success")]
- async fn notification_valid() {
- let notification = setup_notification();
- handle_notification(notification, panic).await;
- }
-
- #[tokio::test]
- #[should_panic]
- async fn notification_invalid_params() {
- let notification = Notification {
- params: json!(0),
- ..setup_notification()
- };
- handle_notification(notification, panic).await;
- }
-}
diff --git a/support/texlab/crates/jsonrpc/src/types.rs b/support/texlab/crates/jsonrpc/src/types.rs
deleted file mode 100644
index 30036d1c7f..0000000000
--- a/support/texlab/crates/jsonrpc/src/types.rs
+++ /dev/null
@@ -1,144 +0,0 @@
-use serde::{Deserialize, Serialize};
-use serde_repr::*;
-
-pub const PROTOCOL_VERSION: &str = "2.0";
-
-#[derive(Debug, Eq, Hash, PartialEq, Clone, Deserialize, Serialize)]
-#[serde(untagged)]
-pub enum Id {
- Number(u64),
- String(String),
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize_repr, Serialize_repr)]
-#[repr(i32)]
-pub enum ErrorCode {
- ParseError = -32700,
- InvalidRequest = -32600,
- MethodNotFound = -32601,
- InvalidParams = -32602,
- InternalError = -32603,
- ServerNotInitialized = -32002,
- UnknownErrorCode = -32001,
- RequestCancelled = -32800,
-}
-
-#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
-pub struct Error {
- pub code: ErrorCode,
- pub message: String,
-
- #[serde(skip_serializing_if = "Option::is_none")]
- pub data: Option<serde_json::Value>,
-}
-
-impl Error {
- pub fn parse_error() -> Self {
- Self {
- code: ErrorCode::ParseError,
- message: "Could not parse the input".to_owned(),
- data: None,
- }
- }
-
- pub fn method_not_found_error() -> Self {
- Self {
- code: ErrorCode::MethodNotFound,
- message: "Method not found".to_owned(),
- data: None,
- }
- }
-
- pub fn deserialize_error() -> Self {
- Self {
- code: ErrorCode::InvalidParams,
- message: "Could not deserialize parameter object".to_owned(),
- data: None,
- }
- }
-
- pub fn internal_error(message: String) -> Self {
- Self {
- code: ErrorCode::InternalError,
- message,
- data: None,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
-pub struct Request {
- pub jsonrpc: String,
- pub method: String,
- pub params: serde_json::Value,
- pub id: Id,
-}
-
-impl Request {
- pub fn new(method: String, params: serde_json::Value, id: Id) -> Self {
- Self {
- jsonrpc: PROTOCOL_VERSION.to_owned(),
- method,
- params,
- id,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
-pub struct Response {
- pub jsonrpc: String,
-
- #[serde(skip_serializing_if = "Option::is_none")]
- pub result: Option<serde_json::Value>,
-
- #[serde(skip_serializing_if = "Option::is_none")]
- pub error: Option<Error>,
-
- pub id: Option<Id>,
-}
-
-impl Response {
- pub fn result(result: serde_json::Value, id: Id) -> Self {
- Self {
- jsonrpc: PROTOCOL_VERSION.to_owned(),
- result: Some(result),
- error: None,
- id: Some(id),
- }
- }
-
- pub fn error(error: Error, id: Option<Id>) -> Self {
- Self {
- jsonrpc: PROTOCOL_VERSION.to_owned(),
- result: None,
- error: Some(error),
- id,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
-pub struct Notification {
- pub jsonrpc: String,
- pub method: String,
- pub params: serde_json::Value,
-}
-
-impl Notification {
- pub fn new(method: String, params: serde_json::Value) -> Self {
- Self {
- jsonrpc: PROTOCOL_VERSION.to_owned(),
- method,
- params,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
-#[serde(untagged)]
-pub enum Message {
- Request(Request),
- Notification(Notification),
- Response(Response),
-}
diff --git a/support/texlab/crates/jsonrpc_derive/Cargo.toml b/support/texlab/crates/jsonrpc_derive/Cargo.toml
deleted file mode 100644
index fb99f16a26..0000000000
--- a/support/texlab/crates/jsonrpc_derive/Cargo.toml
+++ /dev/null
@@ -1,15 +0,0 @@
-[package]
-name = "jsonrpc-derive"
-version = "0.1.0"
-authors = [
- "Eric Förster <eric.foerster@outlook.com>",
- "Patrick Förster <patrick.foerster@outlook.de>"]
-edition = "2018"
-
-[lib]
-proc-macro = true
-doctest = false
-
-[dependencies]
-syn = "1.0"
-quote = "1.0"
diff --git a/support/texlab/crates/jsonrpc_derive/src/lib.rs b/support/texlab/crates/jsonrpc_derive/src/lib.rs
deleted file mode 100644
index 283be858db..0000000000
--- a/support/texlab/crates/jsonrpc_derive/src/lib.rs
+++ /dev/null
@@ -1,207 +0,0 @@
-#![recursion_limit = "128"]
-
-extern crate proc_macro;
-
-use proc_macro::TokenStream;
-use quote::quote;
-use syn::{export::TokenStream2, *};
-
-macro_rules! unwrap {
- ($input:expr, $arm:pat => $value:expr) => {{
- match $input {
- $arm => $value,
- _ => panic!(),
- }
- }};
-}
-
-enum MethodKind {
- Request,
- Notification,
-}
-
-struct MethodMeta {
- pub name: String,
- pub kind: MethodKind,
-}
-
-impl MethodMeta {
- pub fn parse(attr: &Attribute) -> Self {
- let meta = attr.parse_meta().unwrap();
- if meta.path().get_ident().unwrap() != "jsonrpc_method" {
- panic!("Expected jsonrpc_method attribute");
- }
-
- let nested = unwrap!(meta, Meta::List(x) => x.nested);
- let name = unwrap!(&nested[0], NestedMeta::Lit(Lit::Str(x)) => x.value());
- let kind = {
- let lit = unwrap!(&nested[1], NestedMeta::Meta(Meta::NameValue(x)) => &x.lit);
- let kind = unwrap!(lit, Lit::Str(x) => x.value());
- match kind.as_str() {
- "request" => MethodKind::Request,
- "notification" => MethodKind::Notification,
- _ => panic!(
- "Invalid method kind. Valid options are \"request\" and \"notification\""
- ),
- }
- };
-
- Self { name, kind }
- }
-}
-
-#[proc_macro_attribute]
-pub fn jsonrpc_method(_attr: TokenStream, item: TokenStream) -> TokenStream {
- item
-}
-
-#[proc_macro_attribute]
-pub fn jsonrpc_server(_attr: TokenStream, item: TokenStream) -> TokenStream {
- let impl_: ItemImpl = parse_macro_input!(item);
- let generics = &impl_.generics;
- let self_ty = &impl_.self_ty;
- let (requests, notifications) = generate_server_skeletons(&impl_.items);
-
- let tokens = quote! {
- #impl_
-
- #[async_trait::async_trait]
- impl #generics jsonrpc::RequestHandler for #self_ty {
- async fn handle_request(&self, request: jsonrpc::Request) -> jsonrpc::Response {
- use jsonrpc::*;
-
- match request.method.as_str() {
- #(#requests),*,
- _ => {
- Response::error(Error::method_not_found_error(), Some(request.id))
- }
- }
- }
-
- async fn handle_notification(&self, notification: jsonrpc::Notification) {
- match notification.method.as_str() {
- #(#notifications),*,
- _ => log::warn!("{}: {}", "Method not found", notification.method),
- }
- }
- }
- };
-
- tokens.into()
-}
-
-#[proc_macro_attribute]
-pub fn jsonrpc_client(attr: TokenStream, item: TokenStream) -> TokenStream {
- let trait_: ItemTrait = parse_macro_input!(item);
- let trait_ident = &trait_.ident;
- let stubs = generate_client_stubs(&trait_.items);
- let attr: AttributeArgs = parse_macro_input!(attr);
- let struct_ident = unwrap!(attr.first().unwrap(), NestedMeta::Meta(Meta::Path(x)) => x);
-
- let tokens = quote! {
- #[async_trait::async_trait]
- #trait_
-
- pub struct #struct_ident {
- client: jsonrpc::Client
- }
-
- impl #struct_ident
- {
- pub fn new(output: futures::channel::mpsc::Sender<String>) -> Self {
- Self {
- client: jsonrpc::Client::new(output),
- }
- }
- }
-
- #[async_trait::async_trait]
- impl #trait_ident for #struct_ident
- {
- #(#stubs)*
- }
-
- #[async_trait::async_trait]
- impl jsonrpc::ResponseHandler for #struct_ident
- {
- async fn handle(&self, response: jsonrpc::Response) -> () {
- self.client.handle(response).await
- }
- }
- };
-
- tokens.into()
-}
-
-fn generate_server_skeletons(items: &Vec<ImplItem>) -> (Vec<TokenStream2>, Vec<TokenStream2>) {
- let mut requests = Vec::new();
- let mut notifications = Vec::new();
- for item in items {
- let method = unwrap!(item, ImplItem::Method(x) => x);
- if method.attrs.is_empty() {
- continue;
- }
-
- let ident = &method.sig.ident;
- let param_ty = unwrap!(&method.sig.inputs[1], FnArg::Typed(x) => &x.ty);
- let meta = MethodMeta::parse(method.attrs.first().unwrap());
- let name = &meta.name.as_str();
-
- match meta.kind {
- MethodKind::Request => {
- requests.push(quote!(
- #name => {
- let handler = |param: #param_ty| async move {
- self.#ident(param).await
- };
-
- jsonrpc::handle_request(request, handler).await
- }
- ));
- }
- MethodKind::Notification => {
- notifications.push(quote!(
- #name => {
- let handler = |param: #param_ty| async move {
- self.#ident(param).await;
- };
-
- jsonrpc::handle_notification(notification, handler).await;
- }
- ));
- }
- }
- }
-
- (requests, notifications)
-}
-
-fn generate_client_stubs(items: &Vec<TraitItem>) -> Vec<TokenStream2> {
- let mut stubs = Vec::new();
- for item in items {
- let method = unwrap!(item, TraitItem::Method(x) => x);
- let attrs = &method.attrs;
- let sig = &method.sig;
- let param = unwrap!(&sig.inputs[1], FnArg::Typed(x) => &x.pat);
- let meta = MethodMeta::parse(attrs.first().unwrap());
- let name = &meta.name;
-
- let stub = match meta.kind {
- MethodKind::Request => quote!(
- #sig {
- let result = self.client.send_request(#name.to_owned(), #param).await?;
- serde_json::from_value(result).map_err(|_| jsonrpc::Error::deserialize_error())
- }
- ),
- MethodKind::Notification => quote!(
- #sig {
- self.client.send_notification(#name.to_owned(), #param).await
- }
- ),
- };
-
- stubs.push(stub);
- }
-
- stubs
-}
diff --git a/support/texlab/data/lang_data.json b/support/texlab/data/lang_data.json
index 6c156a2908..8cba6d297c 100644
--- a/support/texlab/data/lang_data.json
+++ b/support/texlab/data/lang_data.json
@@ -1,637 +1,4 @@
{
- "environmentCommands": [
- {
- "name": "\\begin",
- "index": 0
- },
- {
- "name": "\\end",
- "index": 0
- }
- ],
- "citationCommands": [
- {
- "name": "\\cite",
- "index": 0
- },
- {
- "name": "\\cite*",
- "index": 0
- },
- {
- "name": "\\Cite",
- "index": 0
- },
- {
- "name": "\\nocite",
- "index": 0
- },
- {
- "name": "\\citet",
- "index": 0
- },
- {
- "name": "\\citep",
- "index": 0
- },
- {
- "name": "\\citet*",
- "index": 0
- },
- {
- "name": "\\citep*",
- "index": 0
- },
- {
- "name": "\\citeauthor",
- "index": 0
- },
- {
- "name": "\\citeauthor*",
- "index": 0
- },
- {
- "name": "\\Citeauthor",
- "index": 0
- },
- {
- "name": "\\Citeauthor*",
- "index": 0
- },
- {
- "name": "\\citetitle",
- "index": 0
- },
- {
- "name": "\\citetitle*",
- "index": 0
- },
- {
- "name": "\\citeyear",
- "index": 0
- },
- {
- "name": "\\citeyear*",
- "index": 0
- },
- {
- "name": "\\citedate",
- "index": 0
- },
- {
- "name": "\\citedate*",
- "index": 0
- },
- {
- "name": "\\citeurl",
- "index": 0
- },
- {
- "name": "\\fullcite",
- "index": 0
- },
- {
- "name": "\\citeyearpar",
- "index": 0
- },
- {
- "name": "\\citealt",
- "index": 0
- },
- {
- "name": "\\citealp",
- "index": 0
- },
- {
- "name": "\\citetext",
- "index": 0
- },
- {
- "name": "\\parencite",
- "index": 0
- },
- {
- "name": "\\parencite*",
- "index": 0
- },
- {
- "name": "\\Parencite",
- "index": 0
- },
- {
- "name": "\\footcite",
- "index": 0
- },
- {
- "name": "\\footfullcite",
- "index": 0
- },
- {
- "name": "\\footcitetext",
- "index": 0
- },
- {
- "name": "\\textcite",
- "index": 0
- },
- {
- "name": "\\Textcite",
- "index": 0
- },
- {
- "name": "\\smartcite",
- "index": 0
- },
- {
- "name": "\\Smartcite",
- "index": 0
- },
- {
- "name": "\\supercite",
- "index": 0
- },
- {
- "name": "\\autocite",
- "index": 0
- },
- {
- "name": "\\Autocite",
- "index": 0
- },
- {
- "name": "\\autocite*",
- "index": 0
- },
- {
- "name": "\\Autocite*",
- "index": 0
- },
- {
- "name": "\\volcite",
- "index": 0
- },
- {
- "name": "\\Volcite",
- "index": 0
- },
- {
- "name": "\\pvolcite",
- "index": 0
- },
- {
- "name": "\\Pvolcite",
- "index": 0
- },
- {
- "name": "\\fvolcite",
- "index": 0
- },
- {
- "name": "\\ftvolcite",
- "index": 0
- },
- {
- "name": "\\svolcite",
- "index": 0
- },
- {
- "name": "\\Svolcite",
- "index": 0
- },
- {
- "name": "\\tvolcite",
- "index": 0
- },
- {
- "name": "\\Tvolcite",
- "index": 0
- },
- {
- "name": "\\avolcite",
- "index": 0
- },
- {
- "name": "\\Avolcite",
- "index": 0
- },
- {
- "name": "\\notecite",
- "index": 0
- },
- {
- "name": "\\notecite",
- "index": 0
- },
- {
- "name": "\\pnotecite",
- "index": 0
- },
- {
- "name": "\\Pnotecite",
- "index": 0
- },
- {
- "name": "\\fnotecite",
- "index": 0
- }
- ],
- "labelCommands": [
- {
- "name": "\\label",
- "index": 0,
- "kind": "definition"
- },
- {
- "name": "\\ref",
- "index": 0,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\vref",
- "index": 0,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\Vref",
- "index": 0,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\autoref",
- "index": 0,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\pageref",
- "index": 0,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\eqref",
- "index": 0,
- "kind": {
- "reference": "math"
- }
- },
- {
- "name": "\\cref",
- "index": 0,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\Cref",
- "index": 0,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\crefrange",
- "index": 0,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\crefrange",
- "index": 1,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\Crefrange",
- "index": 0,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\Crefrange",
- "index": 1,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\cref*",
- "index": 0,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\Cref*",
- "index": 0,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\crefrange*",
- "index": 0,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\crefrange*",
- "index": 1,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\Crefrange*",
- "index": 0,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\Crefrange*",
- "index": 1,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\namecref",
- "index": 0,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\nameCref",
- "index": 0,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\lcnamecref",
- "index": 0,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\namecrefs",
- "index": 0,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\nameCrefs",
- "index": 0,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\lcnamecrefs",
- "index": 0,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\labelcref",
- "index": 0,
- "kind": {
- "reference": "everything"
- }
- },
- {
- "name": "\\labelcpageref",
- "index": 0,
- "kind": {
- "reference": "everything"
- }
- }
- ],
- "sectionCommands": [
- {
- "name": "\\part",
- "index": 0,
- "level": 0,
- "prefix": "Part"
- },
- {
- "name": "\\part*",
- "index": 0,
- "level": 0,
- "prefix": "Part"
- },
- {
- "name": "\\chapter",
- "index": 0,
- "level": 1,
- "prefix": "Chapter"
- },
- {
- "name": "\\chapter*",
- "index": 0,
- "level": 1,
- "prefix": "Chapter"
- },
- {
- "name": "\\section",
- "index": 0,
- "level": 2,
- "prefix": "Section"
- },
- {
- "name": "\\section*",
- "index": 0,
- "level": 2,
- "prefix": "Section"
- },
- {
- "name": "\\subsection",
- "index": 0,
- "level": 3,
- "prefix": "Subsection"
- },
- {
- "name": "\\subsection*",
- "index": 0,
- "level": 3,
- "prefix": "Subsection"
- },
- {
- "name": "\\subsubsection",
- "index": 0,
- "level": 4,
- "prefix": "Subsection"
- },
- {
- "name": "\\subsubsection*",
- "index": 0,
- "level": 4,
- "prefix": "Subsection"
- },
- {
- "name": "\\paragraph",
- "index": 0,
- "level": 5,
- "prefix": "Paragraph"
- },
- {
- "name": "\\paragraph*",
- "index": 0,
- "level": 5,
- "prefix": "Paragraph"
- },
- {
- "name": "\\subparagraph",
- "index": 0,
- "level": 6,
- "prefix": "Subparagraph"
- },
- {
- "name": "\\subparagraph*",
- "index": 0,
- "level": 6,
- "prefix": "Subparagraph"
- }
- ],
- "includeCommands": [
- {
- "name": "\\documentclass",
- "index": 0,
- "kind": "class",
- "includeExtension": false
- },
- {
- "name": "\\usepackage",
- "index": 0,
- "kind": "package",
- "includeExtension": false
- },
- {
- "name": "\\RequirePackage",
- "index": 0,
- "kind": "package",
- "includeExtension": false
- },
- {
- "name": "\\include",
- "index": 0,
- "kind": "latex",
- "includeExtension": false
- },
- {
- "name": "\\input",
- "index": 0,
- "kind": "latex",
- "includeExtension": true
- },
- {
- "name": "\\bibliography",
- "index": 0,
- "kind": "bibliography",
- "includeExtension": true
- },
- {
- "name": "\\addbibresource",
- "index": 0,
- "kind": "bibliography",
- "includeExtension": true
- },
- {
- "name": "\\includegraphics",
- "index": 0,
- "kind": "image",
- "includeExtension": true
- },
- {
- "name": "\\includesvg",
- "index": 0,
- "kind": "svg",
- "includeExtension": false
- },
- {
- "name": "\\includepdf",
- "index": 0,
- "kind": "pdf",
- "includeExtension": true
- },
- {
- "name": "\\verbatiminput",
- "index": 0,
- "kind": "everything",
- "includeExtension": true
- },
- {
- "name": "\\VerbatimInput",
- "index": 0,
- "kind": "everything",
- "includeExtension": true
- },
- {
- "name": "\\subfile",
- "index": 0,
- "kind": "latex",
- "includeExtension": false
- }
- ],
- "commandDefinitionCommands": [
- {
- "name": "\\newcommand",
- "definitionIndex": 0,
- "argCountIndex": 0,
- "implementationIndex": 1
- },
- {
- "name": "\\renewcommand",
- "definitionIndex": 0,
- "argCountIndex": 0,
- "implementationIndex": 1
- },
- {
- "name": "\\DeclareRobustCommand",
- "definitionIndex": 0,
- "argCountIndex": 0,
- "implementationIndex": 1
- }
- ],
- "mathOperatorCommands": [
- {
- "name": "\\DeclareMathOperator",
- "definitionIndex": 0,
- "implementationIndex": 1
- },
- {
- "name": "\\DeclareMathOperator*",
- "definitionIndex": 0,
- "implementationIndex": 1
- }
- ],
- "theoremDefinitionCommands": [
- {
- "name": "\\newtheorem",
- "index": 0
- },
- {
- "name": "\\declaretheorem",
- "index": 0
- }
- ],
"colors": [
"black",
"blue",
@@ -721,493 +88,6 @@
"Yellow",
"YellowOrange"
],
- "colorCommands": [
- {
- "name": "\\color",
- "index": 0
- },
- {
- "name": "\\colorbox",
- "index": 0
- },
- {
- "name": "\\textcolor",
- "index": 0
- },
- {
- "name": "\\pagecolor",
- "index": 0
- }
- ],
- "colorModelCommands": [
- {
- "name": "\\definecolor",
- "index": 1
- },
- {
- "name": "\\definecolorset",
- "index": 0
- }
- ],
- "glossaryEntryDefinitionCommands": [
- {
- "name": "\\newglossaryentry",
- "labelIndex": 0,
- "kind": "general"
- },
- {
- "name": "\\newacronym",
- "labelIndex": 0,
- "kind": "acronym"
- }
- ],
- "glossaryEntryReferenceCommands": [
- {
- "name": "\\gls",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\Gls",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\GLS",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\glspl",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\Glspl",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\GLSpl",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\glsdisp",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\glslink",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\glstext",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\Glstext",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\GLStext",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\glsfirst",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\Glsfirst",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\GLSfirst",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\glsplural",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\Glsplural",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\GLSplural",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\glsfirstplural",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\Glsfirstplural",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\GLSfirstplural",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\glsname",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\Glsname",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\GLSname",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\glssymbol",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\Glssymbol",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\glsdesc",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\Glsdesc",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\GLSdesc",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\glsuseri",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\Glsuseri",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\GLSuseri",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\glsuserii",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\Glsuserii",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\GLSuserii",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\glsuseriii",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\Glsuseriii",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\GLSuseriii",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\glsuseriv",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\Glsuseriv",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\GLSuseriv",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\glsuserv",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\Glsuserv",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\GLSuserv",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\glsuservi",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\Glsuservi",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\GLSuservi",
- "index": 0,
- "kind": "general"
- },
- {
- "name": "\\acrshort",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\Acrshort",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\ACRshort",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\acrshortpl",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\Acrshortpl",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\ACRshortpl",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\acrlong",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\Acrlong",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\ACRlong",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\acrlongpl",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\Acrlongpl",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\ACRlongpl",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\acrfull",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\Acrfull",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\ACRfull",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\acrfullpl",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\Acrfullpl",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\ACRfullpl",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\acs",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\Acs",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\acsp",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\Acsp",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\acl",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\Acl",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\aclp",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\Aclp",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\acf",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\Acf",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\acfp",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\Acfp",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\ac",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\Ac",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\acp",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\glsentrylong",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\Glsentrylong",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\glsentrylongpl",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\Glsentrylongpl",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\glsentryshort",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\Glsentryshort",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\glsentryshortpl",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\Glsentryshortpl",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\glsentryfullpl",
- "index": 0,
- "kind": "acronym"
- },
- {
- "name": "\\Glsentryfullpl",
- "index": 0,
- "kind": "acronym"
- }
- ],
"entryTypes": [
{
"name": "preamble",
@@ -2198,6 +1078,8 @@
"eqnarray*",
"equation",
"equation*",
+ "subequations",
+ "subequations*",
"gather",
"gather*",
"gathered",
diff --git a/support/texlab/docs/custom_messages.md b/support/texlab/docs/custom_messages.md
new file mode 100644
index 0000000000..0142a76342
--- /dev/null
+++ b/support/texlab/docs/custom_messages.md
@@ -0,0 +1,102 @@
+# Custom Messages
+
+We extend the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/specification)
+with custom messages to provide better LaTeX integration.
+These messages are _optional_ and it is up to the client to support them.
+
+## Build Request
+
+The build request is sent from the client to the server to build a given LaTeX document.
+
+_Request_:
+
+- method: 'textDocument/build'
+- params: `BuildTextDocumentParams` defined as follows:
+
+```typescript
+interface BuildTextDocumentParams {
+ /**
+ * The text document to build.
+ */
+ textDocument: TextDocumentIdentifier;
+}
+```
+
+_Response_:
+
+- result: `BuildResult` defined as follows:
+
+```typescript
+interface BuildResult {
+ /**
+ * The status of the build process.
+ */
+ status: BuildStatus;
+}
+
+enum BuildStatus {
+ /**
+ * The build process terminated without any errors.
+ */
+ Success = 0,
+
+ /**
+ * The build process terminated with errors.
+ */
+ Error = 1,
+
+ /**
+ * The build process failed to start or crashed.
+ */
+ Failure = 2,
+
+ /**
+ * The build process was cancelled.
+ */
+ Cancelled = 3,
+}
+```
+
+## Forward Search Request
+
+The forward search request is sent from the client to the server when the user requests a forward search via SyncTeX.
+
+_Request_:
+
+- method: 'textDocument/forwardSearch'
+- params: [`TextDocumentPositionParams`](https://microsoft.github.io/language-server-protocol/specification#textdocumentpositionparams)
+
+_Response_:
+
+- result: `ForwardSearchResult` defined as follows:
+
+```typescript
+interface ForwardSearchResult {
+ /**
+ * The status of the previewer process.
+ */
+ status: ForwardSearchStatus;
+}
+
+enum ForwardSearchStatus {
+ /**
+ * The previewer process executed the command without any errors.
+ */
+ Success = 0,
+
+ /**
+ * The previewer process executed the command with errors.
+ */
+ Error = 1,
+
+ /**
+ * The previewer process failed to start or crashed.
+ */
+ Failure = 2,
+
+ /**
+ * The previewer command is not configured.
+ */
+ Unconfigured = 3,
+}
+```
diff --git a/support/texlab/docs/demo.gif b/support/texlab/docs/demo.gif
new file mode 100644
index 0000000000..6bb0817a59
--- /dev/null
+++ b/support/texlab/docs/demo.gif
Binary files differ
diff --git a/support/texlab/docs/lsp_kinds.md b/support/texlab/docs/lsp_kinds.md
new file mode 100644
index 0000000000..8d6ad6efc7
--- /dev/null
+++ b/support/texlab/docs/lsp_kinds.md
@@ -0,0 +1,31 @@
+The following table describes the mapping of LaTeX and BibTeX structures
+to their `CompletionItemKind` and `SymbolKind`.
+
+| LaTeX / BibTeX structure | CompletionItemKind | SymbolKind |
+| ------------------------- | -------------------- | -------------------- |
+| Command | `Function` (3) | `Function` (12) |
+| Command Argument | `Value` (12) | `Number` (16) |
+| Snippet | `Snippet` (15) | |
+| Environment | `Enum` (13) | `Enum` (10) |
+| Section | `Module` (9) | `Module` (2) |
+| Float | `Method` (2) | `Method` (6) |
+| Theorem | `Variable` (6) | `Variable` (13) |
+| Equation | `Constant` (21) | `Constant` (14) |
+| Enumeration Item | `EnumMember` (20) | `EnumMember` (22) |
+| Label | `Constructor` (4) | `Constructor` (9) |
+| Folder | `Folder` (19) | `Namespace` (3) |
+| File | `File` (17) | `File` (1) |
+| PGF Library | `Property` (10) | `Property` (7) |
+| TikZ Library | `Property` (10) | `Property` (7) |
+| Color | `Color` (16) | |
+| Color Model | `Color` (16) | |
+| Package | `Class` (7) | `Class` (5) |
+| Class | `Class` (7) | `Class` (5) |
+| BibTeX Entry (Misc) | `Interface` (8) | `Interface` (11) |
+| BibTeX Entry (Article) | `Event` (23) | `Event` (24) |
+| BibTeX Entry (Book) | `Struct` (22) | `Struct` (23) |
+| BibTeX Entry (Collection) | `TypeParameter` (25) | `TypeParameter` (26) |
+| BibTeX Entry (Part) | `Operator` (24) | `Operator` (25) |
+| BibTeX Entry (Thesis) | `Unit` (11) | `Object` (19) |
+| BibTeX String | `Text` (1) | `String` (15) |
+| BibTeX Field | `Field` (5) | `Field` (8) |
diff --git a/support/texlab/docs/options.md b/support/texlab/docs/options.md
new file mode 100644
index 0000000000..9cabd31062
--- /dev/null
+++ b/support/texlab/docs/options.md
@@ -0,0 +1,145 @@
+# Configuration
+
+This document describes the configuration settings
+that the server will query from the LSP client / extension.
+
+---
+
+## texlab.rootDirectory
+
+Defines the directory from which the source files get compiled.
+You may need to set this property for multi-folder projects
+where TexLab fails to detect the root document.
+
+**Type:** `string | null`
+
+**Default value**: `null`
+
+---
+
+## texlab.build.executable
+
+Defines the executable of the LaTeX build tool.
+
+**Type:** `string`
+
+**Default value:** `latexmk`
+
+---
+
+## texlab.build.args
+
+Defines additional arguments that are passed to the configured LaTeX build tool.
+Note that flags and their arguments need to be separate
+elements in this array.
+To pass the arguments `-foo bar` to a build tool,
+`latex.build.args` needs to be `["-foo", "bar"]`.
+The placeholder `%f` will be replaced by the server.
+
+**Placeholders:**
+
+- `%f`: The path of the TeX file to compile.
+
+**Type:** `string[]`
+
+**Default value:** `["-pdf", "-interaction=nonstopmode", "-synctex=1", "%f"]`
+
+---
+
+## texlab.build.isContinuous
+
+Set this property to true if the build arguments imply a continous build (like `latexmk -pvc`).
+
+**Type:** `boolean`
+
+**Default value:** `false`
+
+---
+
+## texlab.auxDirectory
+
+Defines the directory containing the build artifacts.
+Note that you need to set the output directory in `latex.build.args` too,
+if you want to change the build directory.
+In this case, use the `-outdir` flag for `latexmk`.
+
+**Type:** `string`
+
+**Default value:** `.` (the same directory as the TeX file)
+
+## texlab.forwardSearch.executable
+
+Defines the executable of the PDF previewer.
+The previewer needs to support [SyncTeX](http://www.tug.org/TUGboat/tb29-3/tb93laurens.pdf).
+
+**Type:** `string | null`
+
+**Default value:** `null`
+
+---
+
+## texlab.forwardSearch.args
+
+Defines additional arguments that are passed to the configured previewer to perform the forward search.
+The placeholders `%f, %p, %l` will be replaced by the server.
+
+**Placeholders:**
+
+- `%f`: The path of the current TeX file.
+- `%p`: The path of the current PDF file.
+- `%l`: The current line number.
+
+**Type:** `string[] | null`
+
+**Default value:** `null`
+
+---
+
+## texlab.chktex.onOpenAndSave
+
+Lint using [chktex](https://www.nongnu.org/chktex/) after opening and saving a file.
+
+**Type:** `boolean`
+
+**Default value:** `false`
+
+---
+
+## texlab.chktex.onEdit
+
+Lint using [chktex](https://www.nongnu.org/chktex/) after editing a file.
+
+**Type:** `boolean`
+
+**Default value:** `false`
+
+---
+
+## texlab.diagnosticsDelay
+
+Delay in milliseconds before reporting diagnostics.
+
+**Type:** `integer`
+
+**Default value:** `300`
+
+---
+
+## texlab.formatterLineLength
+
+Defines the maximum amount of characters per line (0 = disable) when formatting BibTeX files.
+
+**Type:** `integer`
+
+**Default value:** `80`
+
+---
+
+## texlab.bibtexFormatter
+
+Defines the formatter to use for BibTeX formatting.
+Possible values are either `texlab` or `latexindent`.
+
+**Type:** `string`
+
+**Default value:** `texlab`
diff --git a/support/texlab/docs/previewing.md b/support/texlab/docs/previewing.md
new file mode 100644
index 0000000000..16a047190b
--- /dev/null
+++ b/support/texlab/docs/previewing.md
@@ -0,0 +1,217 @@
+# Previewing
+
+We believe that previewing should be a concern of the build system.
+With [`latexmk`](https://ctan.org/pkg/latexmk?lang=en), you can enable the preview feature by adding the `-pv` flag
+to the [`texlab.build.args`](/docs/reference/configuration/#latexbuildargs) setting.
+After that, the configured previewer will start on each rebuild of the document.
+Alternatively, use the `-pvc` flag to tell the previewer to continuously check for updates,
+instead of opening a new window each time a build completes.
+
+If you want to use [SyncTeX](http://www.tug.org/TUGboat/tb29-3/tb93laurens.pdf),
+you need to configure the settings in the [`texlab.forwardSearch`](/docs/reference/configuration#latexforwardsearchexecutable) section
+and configure your previewer to call your editor correctly.
+A forward search can then be executed by invoking the [`texlab.forwardSearch`](/docs/reference/commands#latexforwardsearch) command.
+
+In the following sections, we will give configurations for popular viewers with `latexmk` and Visual Studio Code.
+For other build systems and editors, please refer to their respective manuals.
+
+---
+
+## SumatraPDF
+
+We highly recommend [SumatraPDF](https://www.sumatrapdfreader.org) on Windows
+because Adobe Reader locks the opened PDF file and will therefore prevent further builds.
+To use [SumatraPDF](https://www.sumatrapdfreader.org) as previewer, add the following line to your `%USERPROFILE%/.latexmkrc` file:
+
+```perl
+$pdf_previewer = 'start "C:\Program Files\SumatraPDF\SumatraPDF.exe" %O %S';
+```
+
+### Forward Search
+
+Add the following lines to your editor config:
+
+```json
+{
+ "texlab.forwardSearch.executable": "C:/Program Files/SumatraPDF/SumatraPDF.exe",
+ "texlab.forwardSearch.args": [
+ "-reuse-instance",
+ "%p",
+ "-forward-search",
+ "%f",
+ "%l"
+ ]
+}
+```
+
+### Inverse Search
+
+Add the following line to your SumatraPDF settings file (Menu -> Settings -> Advanced Options):
+
+```ini
+InverseSearchCmdLine = "C:\Users\{User}\AppData\Local\Programs\Microsoft VS Code\Code.exe" -g "%f":%l
+```
+
+> **Note**: Please make sure to replace `{User}` with your Windows username.
+
+You can execute the search by pressing `Alt+DoubleClick` in the PDF document.
+
+---
+
+## Evince
+
+The SyncTeX feature of [Evince](https://wiki.gnome.org/Apps/Evince) requires communication via D-Bus.
+In order to use it from the command line, install the [evince-synctex](https://github.com/latex-lsp/evince-synctex) script.
+Then add the following line to your `~/.latexmkrc` file:
+
+```perl
+$pdf_previewer = 'start evince-synctex %S "code -g %f:%l"';
+```
+
+### Forward Search
+
+Add the following lines to your editor config:
+
+```json
+{
+ "texlab.forwardSearch.executable": "evince-synctex",
+ "texlab.forwardSearch.args": ["-f", "%l", "%p", "\"code -g %f:%l\""]
+}
+```
+
+### Inverse Search
+
+The inverse search feature is already configured if you use `evince-synctex`.
+You can execute the search by pressing `Ctrl+Click` in the PDF document.
+
+---
+
+## Okular
+
+To use [Okular](https://okular.kde.org/) as previewer, add the following line to your `~/.latexmkrc` file:
+
+```perl
+$pdf_previewer = 'start okular';
+```
+
+### Forward Search
+
+Add the following lines to your editor config:
+
+```json
+{
+ "texlab.forwardSearch.executable": "okular",
+ "texlab.forwardSearch.args": ["--unique", "file:%p#src:%l%f"]
+}
+```
+
+### Inverse Search
+
+Change the editor of Okular (Settings -> Configure Okular... -> Editor) to "Custom Text Editor" and set the following command:
+
+```bash
+code -g "%f":%l
+```
+
+You can execute the search by pressing `Shift+Click` in the PDF document.
+
+---
+
+## Zathura
+
+To use [Zathura](https://pwmt.org/projects/zathura/) as previewer, add the following line to your `~/.latexmkrc` file:
+
+```perl
+$pdf_previewer = 'start zathura';
+```
+
+### Forward Search
+
+Add the following lines to your editor config:
+
+```json
+{
+ "texlab.forwardSearch.executable": "zathura",
+ "texlab.forwardSearch.args": ["--synctex-forward", "%l:1:%f", "%p"]
+}
+```
+
+### Inverse Search
+
+Add the following lines to your `~/.config/zathura/zathurarc` file:
+
+```bash
+set synctex true
+set synctex-editor-command "code -g %{input}:%{line}"
+```
+
+You can execute the search by pressing `Alt+Click` in the PDF document.
+
+---
+
+## qpdfview
+
+To use [qpdfview](https://launchpad.net/qpdfview) as previewer, add the following line to your `~/.latexmkrc` file:
+
+```perl
+$pdf_previewer = 'start qpdfview --unique %S';
+```
+
+### Forward Search
+
+Add the following lines to your editor config:
+
+```json
+{
+ "texlab.forwardSearch.executable": "qpdfview",
+ "texlab.forwardSearch.args": ["--unique", "%p#src:%f:%l:1"]
+}
+```
+
+### Inverse Search
+
+Change the source editor setting (Edit -> Settings... -> Behavior -> Source editor) to:
+
+```bash
+code -g "%1":%2
+```
+
+and select a mouse button modifier (Edit -> Settings... -> Behavior -> Modifiers -> Mouse button modifiers -> Open in Source Editor)
+of choice.
+You can execute the search by pressing `Modifier+Click` in the PDF document.
+
+---
+
+## Skim
+
+We recommend [Skim](https://skim-app.sourceforge.io/) on macOS since it is the only native viewer that supports SyncTeX.
+To use [Skim](https://skim-app.sourceforge.io/) as previewer, add the following line to your `~/.latexmkrc` file:
+
+```perl
+$pdf_previewer = 'open -a Skim';
+```
+
+If you want Skim to stay in the background after building,
+you can add the `-g` option to the `open` arguments.
+
+Additionally, enable the "Reload automatically" setting in the Skim preferences (Skim -> Preferences -> Sync -> Check for file changes).
+
+### Forward Search
+
+Add the following lines to your editor config:
+
+```json
+{
+ "texlab.forwardSearch.executable": "/Applications/Skim.app/Contents/SharedSupport/displayline",
+ "texlab.forwardSearch.args": ["%l", "%p", "%f"]
+}
+```
+
+If you want Skim to stay in the background after
+executing the forward search, you can add the `-g` option
+to `latex.forwardSearch.args`.
+
+### Inverse Search
+
+Select the Visual Studio Code preset in the Skim preferences (Skim -> Preferences -> Sync -> PDF-TeX Sync support).
+You can execute the search by pressing `Shift+⌘+Click` in the PDF document.
diff --git a/support/texlab/docs/tectonic.md b/support/texlab/docs/tectonic.md
new file mode 100644
index 0000000000..1e75c2d126
--- /dev/null
+++ b/support/texlab/docs/tectonic.md
@@ -0,0 +1,26 @@
+### Example: Using `tectonic`
+
+[`tectonic`](https://tectonic-typesetting.github.io/) is an alternative to `latexmk`, built in Rust.
+You can quickly get started by changing `.vscode/settings.json` in your workspace to include the following:
+
+```jsonc
+{
+ // See `tectonic --help` for the format
+ "texlab.build.executable": "tectonic",
+ "texlab.build.args": [
+ // Input
+ "%f",
+ // Flags
+ "--synctex",
+ "--keep-logs",
+ "--keep-intermediates"
+ // Options
+ // OPTIONAL: If you want a custom out directory,
+ // uncomment the following line.
+ //"--outdir out",
+ ]
+ // OPTIONAL: The extension needs to be configured
+ // to read the logs from the out directory as well.
+ // "latex.build.outputDirectory": "out",
+}
+```
diff --git a/support/texlab/src/build.rs b/support/texlab/src/build.rs
deleted file mode 100644
index fe4597c0d7..0000000000
--- a/support/texlab/src/build.rs
+++ /dev/null
@@ -1,203 +0,0 @@
-use crate::{
- feature::{FeatureProvider, FeatureRequest},
- protocol::{
- BuildParams, BuildResult, BuildStatus, ClientCapabilitiesExt, LatexOptions,
- LogMessageParams, LspClient, MessageType, ProgressParams, ProgressParamsValue,
- ProgressToken, Uri, WorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressCreateParams,
- WorkDoneProgressEnd,
- },
-};
-use async_trait::async_trait;
-use chashmap::CHashMap;
-use futures::{
- future::{AbortHandle, Abortable, Aborted},
- lock::Mutex,
- prelude::*,
- stream,
-};
-use log::error;
-use std::{collections::HashMap, io, path::Path, process::Stdio, sync::Arc};
-use tokio::{
- io::{AsyncBufReadExt, BufReader},
- process::Command,
-};
-use uuid::Uuid;
-
-pub struct BuildProvider<C> {
- client: Arc<C>,
- handles_by_token: Mutex<HashMap<ProgressToken, AbortHandle>>,
- current_docs: CHashMap<Uri, ()>,
-}
-
-impl<C> BuildProvider<C> {
- pub fn new(client: Arc<C>) -> Self {
- Self {
- client,
- handles_by_token: Mutex::new(HashMap::new()),
- current_docs: CHashMap::new(),
- }
- }
-
- pub fn is_building(&self) -> bool {
- self.current_docs.len() > 0
- }
-
- pub async fn cancel(&self, token: ProgressToken) {
- let handles_by_token = self.handles_by_token.lock().await;
- if let Some(handle) = handles_by_token.get(&token) {
- handle.abort();
- } else if let ProgressToken::String(id) = token {
- if id == "texlab-build-*" {
- handles_by_token.values().for_each(|handle| handle.abort());
- }
- }
- }
-}
-
-#[async_trait]
-impl<C> FeatureProvider for BuildProvider<C>
-where
- C: LspClient + Send + Sync + 'static,
-{
- type Params = BuildParams;
- type Output = BuildResult;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<BuildParams>) -> BuildResult {
- let token = ProgressToken::String(format!("texlab-build-{}", Uuid::new_v4()));
- let (handle, reg) = AbortHandle::new_pair();
- {
- let mut handles_by_token = self.handles_by_token.lock().await;
- handles_by_token.insert(token.clone(), handle);
- }
-
- let doc = req
- .snapshot()
- .parent(&req.current().uri, &req.options, &req.current_dir)
- .unwrap_or_else(|| Arc::clone(&req.view.current));
-
- if !doc.is_file() {
- error!("Unable to build the document {}: wrong URI scheme", doc.uri);
- return BuildResult {
- status: BuildStatus::Failure,
- };
- }
-
- if self.current_docs.get(&doc.uri).is_some() {
- return BuildResult {
- status: BuildStatus::Success,
- };
- }
- self.current_docs.insert(doc.uri.clone(), ());
-
- let status = match doc.uri.to_file_path() {
- Ok(path) => {
- if req.client_capabilities.has_work_done_progress_support() {
- let params = WorkDoneProgressCreateParams {
- token: token.clone(),
- };
- self.client.work_done_progress_create(params).await.unwrap();
-
- let title = path.file_name().unwrap().to_string_lossy().into_owned();
- let params = ProgressParams {
- token: token.clone(),
- value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(
- WorkDoneProgressBegin {
- title,
- cancellable: Some(true),
- message: Some("Building".into()),
- percentage: None,
- },
- )),
- };
- self.client.progress(params).await;
- }
-
- let latex_options = req.options.latex.clone().unwrap_or_default();
- let client = Arc::clone(&self.client);
- match Abortable::new(build(&path, &latex_options, client), reg).await {
- Ok(Ok(true)) => BuildStatus::Success,
- Ok(Ok(false)) => BuildStatus::Error,
- Ok(Err(why)) => {
- error!("Unable to build the document {}: {}", doc.uri, why);
- BuildStatus::Failure
- }
- Err(Aborted) => BuildStatus::Cancelled,
- }
- }
- Err(()) => {
- error!("Unable to build the document {}: invalid URI", doc.uri);
- BuildStatus::Failure
- }
- };
-
- if req.client_capabilities.has_work_done_progress_support() {
- let params = ProgressParams {
- token: token.clone(),
- value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(WorkDoneProgressEnd {
- message: None,
- })),
- };
- self.client.progress(params).await;
- }
- {
- let mut handles_by_token = self.handles_by_token.lock().await;
- handles_by_token.remove(&token);
- }
-
- self.current_docs.remove(&doc.uri);
- BuildResult { status }
- }
-}
-
-async fn build<C>(path: &Path, options: &LatexOptions, client: Arc<C>) -> io::Result<bool>
-where
- C: LspClient + Send + Sync + 'static,
-{
- let build_options = options.build.as_ref().cloned().unwrap_or_default();
- let build_dir = options
- .root_directory
- .as_ref()
- .map(AsRef::as_ref)
- .or_else(|| path.parent())
- .unwrap();
-
- let args: Vec<_> = build_options
- .args()
- .into_iter()
- .map(|arg| replace_placeholder(arg, path))
- .collect();
-
- let mut process = Command::new(build_options.executable())
- .args(args)
- .stdin(Stdio::null())
- .stdout(Stdio::piped())
- .stderr(Stdio::piped())
- .current_dir(build_dir)
- .kill_on_drop(true)
- .spawn()?;
-
- let stdout = BufReader::new(process.stdout.take().unwrap()).lines();
- let stderr = BufReader::new(process.stderr.take().unwrap()).lines();
- let mut output = stream::select(stdout, stderr);
-
- tokio::spawn(async move {
- while let Some(Ok(line)) = output.next().await {
- let params = LogMessageParams {
- typ: MessageType::Log,
- message: line,
- };
-
- client.log_message(params).await;
- }
- });
-
- Ok(process.await?.success())
-}
-
-fn replace_placeholder(arg: String, file: &Path) -> String {
- if arg.starts_with('"') || arg.ends_with('"') {
- arg
- } else {
- arg.replace("%f", &file.to_string_lossy())
- }
-}
diff --git a/support/texlab/src/protocol/capabilities.rs b/support/texlab/src/capabilities.rs
index c83b205aa8..e20a66b1ba 100644
--- a/support/texlab/src/protocol/capabilities.rs
+++ b/support/texlab/src/capabilities.rs
@@ -60,10 +60,13 @@ impl ClientCapabilitiesExt for ClientCapabilities {
#[cfg(test)]
mod tests {
use super::*;
- use lsp_types::*;
+ use lsp_types::{
+ DocumentSymbolClientCapabilities, GotoCapability, HoverClientCapabilities,
+ TextDocumentClientCapabilities, WindowClientCapabilities,
+ };
#[test]
- fn has_definition_link_support_true() {
+ fn test_has_definition_link_support_true() {
let capabilities = ClientCapabilities {
text_document: Some(TextDocumentClientCapabilities {
definition: Some(GotoCapability {
@@ -78,18 +81,18 @@ mod tests {
}
#[test]
- fn has_definition_link_support_false() {
+ fn test_has_definition_link_support_false() {
let capabilities = ClientCapabilities::default();
assert!(!capabilities.has_definition_link_support());
}
#[test]
- fn has_hierarchical_document_symbol_support_true() {
+ fn test_has_hierarchical_document_symbol_support_true() {
let capabilities = ClientCapabilities {
text_document: Some(TextDocumentClientCapabilities {
- document_symbol: Some(DocumentSymbolCapability {
+ document_symbol: Some(DocumentSymbolClientCapabilities {
hierarchical_document_symbol_support: Some(true),
- ..DocumentSymbolCapability::default()
+ ..DocumentSymbolClientCapabilities::default()
}),
..TextDocumentClientCapabilities::default()
}),
@@ -99,13 +102,13 @@ mod tests {
}
#[test]
- fn has_hierarchical_document_symbol_support_false() {
+ fn test_has_hierarchical_document_symbol_support_false() {
let capabilities = ClientCapabilities::default();
assert!(!capabilities.has_hierarchical_document_symbol_support());
}
#[test]
- fn has_work_done_progress_support_true() {
+ fn test_has_work_done_progress_support_true() {
let capabilities = ClientCapabilities {
window: Some(WindowClientCapabilities {
work_done_progress: Some(true),
@@ -117,18 +120,18 @@ mod tests {
}
#[test]
- fn has_work_done_progress_support_false() {
+ fn test_has_work_done_progress_support_false() {
let capabilities = ClientCapabilities::default();
assert!(!capabilities.has_work_done_progress_support());
}
#[test]
- fn has_hover_markdown_support_true() {
+ fn test_has_hover_markdown_support_true() {
let capabilities = ClientCapabilities {
text_document: Some(TextDocumentClientCapabilities {
- hover: Some(HoverCapability {
+ hover: Some(HoverClientCapabilities {
content_format: Some(vec![MarkupKind::PlainText, MarkupKind::Markdown]),
- ..HoverCapability::default()
+ ..HoverClientCapabilities::default()
}),
..TextDocumentClientCapabilities::default()
}),
@@ -138,7 +141,7 @@ mod tests {
}
#[test]
- fn has_hover_markdown_support_false() {
+ fn test_has_hover_markdown_support_false() {
let capabilities = ClientCapabilities::default();
assert!(!capabilities.has_hover_markdown_support());
}
diff --git a/support/texlab/src/citeproc/mod.rs b/support/texlab/src/citation.rs
index 6def2ed8a4..e185c14b55 100644
--- a/support/texlab/src/citeproc/mod.rs
+++ b/support/texlab/src/citation.rs
@@ -2,28 +2,30 @@ mod bibutils;
mod name;
mod ris;
-use self::{
- bibutils::{InputFormat, OutputFormat},
- ris::{RisLibrary, RisReference},
-};
-use crate::{
- protocol::{BibtexFormattingOptions, MarkupContent, MarkupKind},
- syntax::bibtex,
-};
-use citeproc::prelude::*;
+use std::sync::Arc;
+
+use citeproc::{prelude::SupportedFormat, ClusterPosition, Processor};
use citeproc_db::PredefinedLocales;
+use citeproc_io::{Cite, Cluster, Reference};
+use lsp_types::{MarkupContent, MarkupKind};
use once_cell::sync::Lazy;
use regex::Regex;
-use std::sync::Arc;
-static APA_STYLE: &str = include_str!("apa.csl");
+use crate::syntax::{bibtex, CstNode};
+
+use self::{
+ bibutils::*,
+ ris::{RisLibrary, RisReference},
+};
+
+static APA_STYLE: &str = include_str!("citation/apa.csl");
static DOI_URL_PATTERN: &str = r#"https://doi.org/\[.*\]\(.*\)"#;
static DOI_URL_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(DOI_URL_PATTERN).unwrap());
-pub fn render_citation(tree: &bibtex::Tree, key: &str) -> Option<MarkupContent> {
- let ris_reference = convert_to_ris(tree, key)?;
+pub fn render_citation(root: &bibtex::SyntaxNode, key: &str) -> Option<MarkupContent> {
+ let ris_reference = convert_to_ris(root, key)?;
let doi_url = get_doi_url_markdown(&ris_reference);
let csl_reference: Reference = ris_reference.into();
let html = generate_bibliography(csl_reference)?;
@@ -52,34 +54,22 @@ pub fn render_citation(tree: &bibtex::Tree, key: &str) -> Option<MarkupContent>
Some(content)
}
-fn convert_to_ris(tree: &bibtex::Tree, key: &str) -> Option<RisReference> {
- let options = BibtexFormattingOptions {
- line_length: None,
- formatter: None,
- };
- let params = bibtex::FormattingParams {
- insert_spaces: true,
- tab_size: 4,
- options: &options,
- };
-
+fn convert_to_ris(root: &bibtex::SyntaxNode, key: &str) -> Option<RisReference> {
let mut bib_code = String::new();
- tree.children(tree.root)
- .filter(|node| tree.as_string(*node).is_some())
- .map(|node| bibtex::format(tree, node, params))
- .for_each(|string| {
- bib_code.push_str(&string);
- bib_code.push('\n');
- });
-
- let entry = tree.entry_by_key(key)?;
- if let Some(crossref) = tree.crossref(entry) {
- bib_code.push_str(&bibtex::format(tree, crossref, params));
- bib_code.push('\n');
+ for string in root
+ .children()
+ .filter_map(bibtex::String::cast)
+ .filter(|string| string.name().is_some())
+ {
+ bib_code.push_str(&string.syntax().to_string());
}
- bib_code.push_str(&bibtex::format(tree, entry, params));
- bib_code.push('\n');
+ let entry = root
+ .children()
+ .filter_map(bibtex::Entry::cast)
+ .find(|entry| entry.key().map(|key| key.text()) == Some(key))?;
+ bib_code.push_str(&entry.syntax().to_string());
+
bib_code = bib_code.replace("\\hypen", "-");
let ris_code = bibutils::convert(&bib_code, InputFormat::Biblatex, OutputFormat::Ris)?;
@@ -119,21 +109,20 @@ fn generate_bibliography(reference: Reference) -> Option<String> {
#[cfg(test)]
mod tests {
use super::*;
- use indoc::indoc;
#[test]
- fn simple() {
- let tree = bibtex::open(indoc!(
+ fn test_simple() {
+ let document = bibtex::parse(
r#"
- @article{foo,
- author = {Foo Bar},
+ @article{foo,
+ author = {Foo Bar},
title = {Baz Qux},
year = {2020}
}
- "#
- ));
+ "#,
+ );
- let actual_md = render_citation(&tree, "foo").unwrap();
+ let actual_md = render_citation(&document.root, "foo").unwrap();
let expected_md = MarkupContent {
kind: MarkupKind::Markdown,
@@ -144,55 +133,18 @@ mod tests {
}
#[test]
- fn crossref() {
- let tree = bibtex::open(indoc!(
- r#"
- https://tex.stackexchange.com/questions/401138/what-is-the-bibtex-crossref-field-used-for
-
- @inproceedings{duck2015,
- author = {Duck, D.},
- title = {Duck tales},
- crossref = {ICRC2015},
- }
-
- @inproceedings{mouse2015,
- author = {Mouse, M.},
- title = {Mouse stories},
- crossref = {ICRC2015},
- }
-
- @proceedings{ICRC2015,
- title = "{Proceedings of the 34\textsuperscript{th} International Cosmic Ray Conference}",
- year = "2015",
- month = aug,
- }
- "#
- ));
-
- let actual_md = render_citation(&tree, "mouse2015").unwrap();
-
- let expected_md = MarkupContent {
- kind: MarkupKind::Markdown,
- value: "Mouse, M. (2015). Mouse stories. In *Proceedings of the 34th International Cosmic Ray Conference*.".into(),
- };
-
- assert_eq!(actual_md, expected_md);
- }
-
- #[test]
- fn string() {
- let tree = bibtex::open(indoc!(
+ fn test_string() {
+ let document = bibtex::parse(
r#"
@string{author = "Foo Bar"}
- @article{foo,
- author = author,
+ @article{foo,
+ author = author,
title = {Baz Qux},
year = {2020}
}
- "#
- ));
-
- let actual_md = render_citation(&tree, "foo").unwrap();
+ "#,
+ );
+ let actual_md = render_citation(&document.root, "foo").unwrap();
let expected_md = MarkupContent {
kind: MarkupKind::Markdown,
@@ -203,10 +155,10 @@ mod tests {
}
#[test]
- fn unknown_key() {
- let tree = bibtex::open("");
+ fn test_unknown_key() {
+ let document = bibtex::parse("");
- let actual_md = render_citation(&tree, "foo");
+ let actual_md = render_citation(&document.root, "foo");
assert_eq!(actual_md, None);
}
diff --git a/support/texlab/src/citeproc/apa.csl b/support/texlab/src/citation/apa.csl
index 8454246a98..8454246a98 100644
--- a/support/texlab/src/citeproc/apa.csl
+++ b/support/texlab/src/citation/apa.csl
diff --git a/support/texlab/src/citeproc/bibutils.rs b/support/texlab/src/citation/bibutils.rs
index b69ed66ed1..9931ba3f9f 100644
--- a/support/texlab/src/citeproc/bibutils.rs
+++ b/support/texlab/src/citation/bibutils.rs
@@ -1,3 +1,5 @@
+use std::{ffi::CString, fs, mem::MaybeUninit, path::Path};
+
use bibutils_sys::{
bibl, bibl_free, bibl_freeparams, bibl_init, bibl_initparams, bibl_read, bibl_write, fclose,
fopen, param, BIBL_ADSABSOUT, BIBL_BIBLATEXIN, BIBL_BIBTEXIN, BIBL_BIBTEXOUT, BIBL_COPACIN,
@@ -5,10 +7,10 @@ use bibutils_sys::{
BIBL_MODSIN, BIBL_MODSOUT, BIBL_NBIBIN, BIBL_NBIBOUT, BIBL_OK, BIBL_RISIN, BIBL_RISOUT,
BIBL_WORD2007OUT, BIBL_WORDIN, FILE,
};
-use std::{ffi::CString, fs, mem::MaybeUninit, path::Path};
use tempfile::tempdir;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+#[allow(unused)]
pub enum InputFormat {
Bibtex,
Biblatex,
@@ -42,6 +44,7 @@ impl InputFormat {
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+#[allow(unused)]
pub enum OutputFormat {
Adsabs,
Bibtex,
@@ -183,6 +186,10 @@ pub fn convert(input: &str, from: InputFormat, to: OutputFormat) -> Option<Strin
// Remove BOM
let data = fs::read(&output_path).ok()?;
+ if data.is_empty() {
+ return None;
+ }
+
let text = String::from_utf8_lossy(&data[3..]).into_owned();
Some(text)
}
diff --git a/support/texlab/src/citeproc/name/mod.rs b/support/texlab/src/citation/name.rs
index ef97d30130..9e22383a7d 100644
--- a/support/texlab/src/citeproc/name/mod.rs
+++ b/support/texlab/src/citation/name.rs
@@ -1,14 +1,15 @@
// Ported from: https://github.com/michel-kraemer/citeproc-java/blob/master/citeproc-java/grammars/InternalName.g4
// Michel Kraemer
// Apache License 2.0
+use citeproc_io::Name;
+
+use parser::NamesParser;
+
mod parser {
#![allow(warnings)]
- include!(concat!(env!("OUT_DIR"), "/citeproc/name/parser.rs"));
+ include!(concat!(env!("OUT_DIR"), "/citation/name/parser.rs"));
}
-use self::parser::NamesParser;
-use citeproc_io::Name;
-
pub fn parse(input: &str) -> Vec<Name> {
let parser = NamesParser::new();
parser.parse(input).unwrap_or_else(|_| {
@@ -24,7 +25,7 @@ mod tests {
use citeproc_io::PersonName;
#[test]
- fn family_only() {
+ fn test_family_only() {
let name = Name::Person(PersonName {
family: Some("Thompson".into()),
given: None,
@@ -36,7 +37,7 @@ mod tests {
}
#[test]
- fn simple() {
+ fn test_simple() {
let name = Name::Person(PersonName {
family: Some("Thompson".into()),
given: Some("Ken".into()),
@@ -48,7 +49,7 @@ mod tests {
}
#[test]
- fn middle_name() {
+ fn test_middle_name() {
let name = Name::Person(PersonName {
family: Some("Ritchie".into()),
given: Some("Dennis M.".into()),
@@ -60,7 +61,7 @@ mod tests {
}
#[test]
- fn initials() {
+ fn test_initials() {
let name = Name::Person(PersonName {
family: Some("Johnson".into()),
given: Some("S. C.".into()),
@@ -72,7 +73,7 @@ mod tests {
}
#[test]
- fn non_dropping_particle() {
+ fn test_non_dropping_particle() {
let name = Name::Person(PersonName {
family: Some("Gerwen".into()),
given: Some("Michael".into()),
@@ -84,7 +85,7 @@ mod tests {
}
#[test]
- fn non_dropping_particle_family_only() {
+ fn test_non_dropping_particle_family_only() {
let name = Name::Person(PersonName {
family: Some("Gerwen".into()),
given: None,
@@ -96,7 +97,7 @@ mod tests {
}
#[test]
- fn comma() {
+ fn test_comma() {
let name = Name::Person(PersonName {
family: Some("Thompson".into()),
given: Some("Ken".into()),
@@ -108,7 +109,7 @@ mod tests {
}
#[test]
- fn comma_junior() {
+ fn test_comma_junior() {
let name = Name::Person(PersonName {
family: Some("Friedman".into()),
given: Some("George".into()),
@@ -120,7 +121,7 @@ mod tests {
}
#[test]
- fn comma_no_junior() {
+ fn test_comma_no_junior() {
let name = Name::Person(PersonName {
family: Some("Familya Familyb".into()),
given: Some("Given".into()),
@@ -132,7 +133,7 @@ mod tests {
}
#[test]
- fn comma_initials() {
+ fn test_comma_initials() {
let name = Name::Person(PersonName {
family: Some("Ritchie".into()),
given: Some("Dennis M.".into()),
@@ -144,7 +145,7 @@ mod tests {
}
#[test]
- fn comma_non_dropping_particle() {
+ fn test_comma_non_dropping_particle() {
let name = Name::Person(PersonName {
family: Some("Gerwen".into()),
given: Some("Michael".into()),
@@ -156,7 +157,7 @@ mod tests {
}
#[test]
- fn comma_non_dropping_particles() {
+ fn test_comma_non_dropping_particles() {
let name = Name::Person(PersonName {
family: Some("Voort".into()),
given: Some("Vincent".into()),
@@ -168,7 +169,7 @@ mod tests {
}
#[test]
- fn and() {
+ fn test_and() {
let name1 = Name::Person(PersonName {
family: Some("Gerwen".into()),
given: Some("Michael".into()),
@@ -190,7 +191,7 @@ mod tests {
}
#[test]
- fn and_comma1() {
+ fn test_and_comma1() {
let name1 = Name::Person(PersonName {
family: Some("Gerwen".into()),
given: Some("Michael".into()),
@@ -212,7 +213,7 @@ mod tests {
}
#[test]
- fn and_comma2() {
+ fn test_and_comma2() {
let name1 = Name::Person(PersonName {
family: Some("Gerwen".into()),
given: Some("Michael".into()),
@@ -234,7 +235,7 @@ mod tests {
}
#[test]
- fn and_comma_mix() {
+ fn test_and_comma_mix() {
let name1 = Name::Person(PersonName {
family: Some("Gerwen".into()),
given: Some("Michael".into()),
@@ -256,7 +257,7 @@ mod tests {
}
#[test]
- fn junior() {
+ fn test_junior() {
let name = Name::Person(PersonName {
family: Some("Friedman".into()),
given: Some("George".into()),
@@ -268,7 +269,7 @@ mod tests {
}
#[test]
- fn non_parseable() {
+ fn test_non_parseable() {
let literal = "Jerry Peek and Tim O'Reilly and Mike Loukides and other authors of the Nutshell handbooks";
let name = Name::Literal {
literal: literal.into(),
diff --git a/support/texlab/src/citeproc/name/parser.lalrpop b/support/texlab/src/citation/name/parser.lalrpop
index e7bce8a302..e7bce8a302 100644
--- a/support/texlab/src/citeproc/name/parser.lalrpop
+++ b/support/texlab/src/citation/name/parser.lalrpop
diff --git a/support/texlab/src/citeproc/ris.rs b/support/texlab/src/citation/ris.rs
index 1cb5754aec..5b666160a3 100644
--- a/support/texlab/src/citeproc/ris.rs
+++ b/support/texlab/src/citation/ris.rs
@@ -1,12 +1,13 @@
// Ported from: https://github.com/michel-kraemer/citeproc-java/tree/master/citeproc-java/templates
// Michel Kraemer
// Apache License 2.0
-use super::name;
use citeproc_io::{Date, DateOrRange, Name, NumericValue, Reference};
use csl::*;
use fnv::FnvHashMap;
use serde::{Deserialize, Serialize};
+use super::name;
+
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum RisType {
diff --git a/support/texlab/src/client.rs b/support/texlab/src/client.rs
new file mode 100644
index 0000000000..72f2ac9eba
--- /dev/null
+++ b/support/texlab/src/client.rs
@@ -0,0 +1,53 @@
+use std::sync::Mutex;
+
+use anyhow::{anyhow, Result};
+use crossbeam_channel::{Receiver, Sender};
+use lsp_server::{Message, ResponseError};
+use serde::{de::DeserializeOwned, Serialize};
+
+use crate::req_queue::{OutgoingData, ReqQueue};
+
+pub fn send_notification<N>(lsp_sender: &Sender<Message>, params: N::Params) -> Result<()>
+where
+ N: lsp_types::notification::Notification,
+ N::Params: Serialize,
+{
+ lsp_sender.send(lsp_server::Notification::new(N::METHOD.to_string(), params).into())?;
+ Ok(())
+}
+
+pub fn send_request<R>(
+ req_queue: &Mutex<ReqQueue>,
+ lsp_sender: &Sender<Message>,
+ params: R::Params,
+) -> Result<R::Result>
+where
+ R: lsp_types::request::Request,
+ R::Params: Serialize,
+ R::Result: DeserializeOwned,
+{
+ let receiver = register_outgoing_request::<R>(req_queue, lsp_sender, params)?;
+ let params = receiver.recv()?.map_err(|err| anyhow!(err.message))?;
+ let result = serde_json::from_value(params)?;
+ Ok(result)
+}
+
+fn register_outgoing_request<R>(
+ req_queue: &Mutex<ReqQueue>,
+ lsp_sender: &Sender<Message>,
+ params: R::Params,
+) -> Result<Receiver<Result<serde_json::Value, ResponseError>>>
+where
+ R: lsp_types::request::Request,
+ R::Params: Serialize,
+ R::Result: DeserializeOwned,
+{
+ let mut req_queue = req_queue.lock().unwrap();
+ let (sender, receiver) = crossbeam_channel::bounded(1);
+ let method = R::METHOD.to_string();
+ let data = OutgoingData { sender };
+ let req = req_queue.outgoing.register(method, params, data);
+ drop(req_queue);
+ lsp_sender.send(req.into())?;
+ Ok(receiver)
+}
diff --git a/support/texlab/src/completion/bibtex/cmd.rs b/support/texlab/src/completion/bibtex/cmd.rs
deleted file mode 100644
index eab905386a..0000000000
--- a/support/texlab/src/completion/bibtex/cmd.rs
+++ /dev/null
@@ -1,177 +0,0 @@
-use crate::{
- completion::types::{Item, ItemData},
- components::COMPONENT_DATABASE,
- feature::FeatureRequest,
- protocol::{CompletionParams, RangeExt},
- syntax::SyntaxNode,
- workspace::DocumentContent,
-};
-
-pub async fn complete_bibtex_commands<'a>(
- req: &'a FeatureRequest<CompletionParams>,
- items: &mut Vec<Item<'a>>,
-) {
- if let DocumentContent::Bibtex(tree) = &req.current().content {
- let pos = req.params.text_document_position.position;
- if let Some(cmd) = tree
- .find(pos)
- .into_iter()
- .last()
- .and_then(|node| tree.as_command(node))
- {
- if cmd.token.range().contains(pos) && cmd.token.start().character != pos.character {
- let mut range = cmd.range();
- range.start.character += 1;
- for cmd in &COMPONENT_DATABASE.kernel().commands {
- let item = Item::new(
- range,
- ItemData::ComponentCommand {
- name: &cmd.name,
- image: cmd.image.as_deref(),
- glyph: cmd.glyph.as_deref(),
- file_names: &[],
- },
- );
- items.push(item);
- }
- }
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{feature::FeatureTester, protocol::Range};
- use indoc::indoc;
-
- #[tokio::test]
- async fn empty_latex_document() {
- let req = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_bibtex_commands(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let req = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_bibtex_commands(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn inside_comment() {
- let req = FeatureTester::new()
- .file("main.bib", r#"\"#)
- .main("main.bib")
- .position(0, 1)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_bibtex_commands(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn inside_command() {
- let req = FeatureTester::new()
- .file(
- "main.bib",
- indoc!(
- r#"
- @article{foo, bar=
- \}
- "#
- ),
- )
- .main("main.bib")
- .position(1, 1)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_bibtex_commands(&req, &mut actual_items).await;
-
- assert!(!actual_items.is_empty());
- assert_eq!(actual_items[0].range, Range::new_simple(1, 1, 1, 2));
- }
-
- #[tokio::test]
- async fn start_of_command() {
- let req = FeatureTester::new()
- .file(
- "main.bib",
- indoc!(
- r#"
- @article{foo, bar=
- \}
- "#
- ),
- )
- .main("main.bib")
- .position(1, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_bibtex_commands(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn inside_text() {
- let req = FeatureTester::new()
- .file(
- "main.bib",
- indoc!(
- r#"
- @article{foo, bar=
- }
- "#
- ),
- )
- .main("main.bib")
- .position(1, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_bibtex_commands(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn inside_latex_command() {
- let req = FeatureTester::new()
- .file("main.tex", r#"\"#)
- .main("main.tex")
- .position(0, 1)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_bibtex_commands(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-}
diff --git a/support/texlab/src/completion/bibtex/entry_type.rs b/support/texlab/src/completion/bibtex/entry_type.rs
deleted file mode 100644
index e3b12ef00d..0000000000
--- a/support/texlab/src/completion/bibtex/entry_type.rs
+++ /dev/null
@@ -1,164 +0,0 @@
-use crate::{
- completion::types::{Item, ItemData},
- feature::FeatureRequest,
- protocol::{CompletionParams, Position, Range, RangeExt},
- syntax::{bibtex, SyntaxNode, LANGUAGE_DATA},
- workspace::DocumentContent,
-};
-
-pub async fn complete_bibtex_entry_types<'a>(
- req: &'a FeatureRequest<CompletionParams>,
- items: &mut Vec<Item<'a>>,
-) {
- if let DocumentContent::Bibtex(tree) = &req.current().content {
- let pos = req.params.text_document_position.position;
- for decl in tree.children(tree.root) {
- match &tree.graph[decl] {
- bibtex::Node::Preamble(preamble) => {
- if contains(&preamble.ty, pos) {
- make_items(items, preamble.ty.range());
- return;
- }
- }
- bibtex::Node::String(string) => {
- if contains(&string.ty, pos) {
- make_items(items, string.ty.range());
- return;
- }
- }
- bibtex::Node::Entry(entry) => {
- if contains(&entry.ty, pos) {
- make_items(items, entry.ty.range());
- return;
- }
- }
- _ => {}
- }
- }
- }
-}
-
-fn contains(ty: &bibtex::Token, pos: Position) -> bool {
- ty.range().contains(pos) && ty.start().character != pos.character
-}
-
-fn make_items(items: &mut Vec<Item>, mut range: Range) {
- range.start.character += 1;
- for ty in &LANGUAGE_DATA.entry_types {
- let item = Item::new(range, ItemData::EntryType { ty });
- items.push(item);
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::feature::FeatureTester;
-
- #[tokio::test]
- async fn empty_latex_document() {
- let req = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_bibtex_entry_types(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let req = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_bibtex_entry_types(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn before_at_sign() {
- let req = FeatureTester::new()
- .file("main.bib", "@")
- .main("main.bib")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_bibtex_entry_types(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn after_at_sign() {
- let req = FeatureTester::new()
- .file("main.bib", "@")
- .main("main.bib")
- .position(0, 1)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_bibtex_entry_types(&req, &mut actual_items).await;
-
- assert!(!actual_items.is_empty());
- assert_eq!(actual_items[0].range, Range::new_simple(0, 1, 0, 1));
- }
-
- #[tokio::test]
- async fn inside_entry_type() {
- let req = FeatureTester::new()
- .file("main.bib", "@foo")
- .main("main.bib")
- .position(0, 2)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_bibtex_entry_types(&req, &mut actual_items).await;
-
- assert!(!actual_items.is_empty());
- assert_eq!(actual_items[0].range, Range::new_simple(0, 1, 0, 4));
- }
-
- #[tokio::test]
- async fn inside_entry_key() {
- let req = FeatureTester::new()
- .file("main.bib", "@article{foo,}")
- .main("main.bib")
- .position(0, 11)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_bibtex_entry_types(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn inside_comments() {
- let req = FeatureTester::new()
- .file("main.bib", "foo")
- .main("main.bib")
- .position(0, 2)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_bibtex_entry_types(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-}
diff --git a/support/texlab/src/completion/bibtex/field_name.rs b/support/texlab/src/completion/bibtex/field_name.rs
deleted file mode 100644
index 039cc5d6b3..0000000000
--- a/support/texlab/src/completion/bibtex/field_name.rs
+++ /dev/null
@@ -1,190 +0,0 @@
-use crate::{
- completion::types::{Item, ItemData},
- feature::FeatureRequest,
- protocol::{CompletionParams, Range, RangeExt},
- syntax::{bibtex, SyntaxNode, LANGUAGE_DATA},
- workspace::DocumentContent,
-};
-
-pub async fn complete_bibtex_fields<'a>(
- req: &'a FeatureRequest<CompletionParams>,
- items: &mut Vec<Item<'a>>,
-) {
- if let DocumentContent::Bibtex(tree) = &req.current().content {
- let pos = req.params.text_document_position.position;
- match tree
- .find(pos)
- .into_iter()
- .last()
- .map(|node| &tree.graph[node])
- {
- Some(bibtex::Node::Field(field)) => {
- if field.name.range().contains(pos) {
- make_items(items, field.name.range());
- return;
- }
- }
- Some(bibtex::Node::Entry(entry)) => {
- if !entry.is_comment() && !entry.ty.range().contains(pos) {
- let range = Range::new(pos, pos);
- if let Some(key) = &entry.key {
- if !key.range().contains(pos) {
- make_items(items, range);
- return;
- }
- } else {
- make_items(items, range);
- return;
- }
- }
- }
- _ => (),
- }
- }
-}
-
-fn make_items(items: &mut Vec<Item>, range: Range) {
- for field in &LANGUAGE_DATA.fields {
- let item = Item::new(range, ItemData::Field { field });
- items.push(item);
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{feature::FeatureTester, protocol::Range};
- use indoc::indoc;
-
- #[tokio::test]
- async fn empty_latex_document() {
- let req = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_bibtex_fields(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let req = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_bibtex_fields(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn inside_first_field() {
- let req = FeatureTester::new()
- .file(
- "main.bib",
- indoc!(
- r#"
- @article{foo,
- bar}
- "#
- ),
- )
- .main("main.bib")
- .position(1, 1)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_bibtex_fields(&req, &mut actual_items).await;
-
- assert!(!actual_items.is_empty());
- assert_eq!(actual_items[0].range, Range::new_simple(1, 0, 1, 3));
- }
-
- #[tokio::test]
- async fn inside_second_field() {
- let req = FeatureTester::new()
- .file("main.bib", "@article{foo, bar = {baz}, qux}")
- .main("main.bib")
- .position(0, 27)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_bibtex_fields(&req, &mut actual_items).await;
-
- assert!(!actual_items.is_empty());
- assert_eq!(actual_items[0].range, Range::new_simple(0, 27, 0, 30));
- }
-
- #[tokio::test]
- async fn inside_entry() {
- let req = FeatureTester::new()
- .file(
- "main.bib",
- indoc!(
- r#"
- @article{foo,
- }
- "#
- ),
- )
- .main("main.bib")
- .position(1, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_bibtex_fields(&req, &mut actual_items).await;
-
- assert!(!actual_items.is_empty());
- assert_eq!(actual_items[0].range, Range::new_simple(1, 0, 1, 0));
- }
-
- #[tokio::test]
- async fn inside_content() {
- let req = FeatureTester::new()
- .file(
- "main.bib",
- indoc!(
- r#"
- @article{foo,
- bar = {baz}}
- "#
- ),
- )
- .main("main.bib")
- .position(1, 7)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_bibtex_fields(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn inside_entry_type() {
- let req = FeatureTester::new()
- .file("main.bib", "@article{foo,}")
- .main("main.bib")
- .position(0, 3)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_bibtex_fields(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-}
diff --git a/support/texlab/src/completion/bibtex/mod.rs b/support/texlab/src/completion/bibtex/mod.rs
deleted file mode 100644
index 96f3a94932..0000000000
--- a/support/texlab/src/completion/bibtex/mod.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-pub mod cmd;
-pub mod entry_type;
-pub mod field_name;
diff --git a/support/texlab/src/completion/latex/argument.rs b/support/texlab/src/completion/latex/argument.rs
deleted file mode 100644
index 5736099ca6..0000000000
--- a/support/texlab/src/completion/latex/argument.rs
+++ /dev/null
@@ -1,215 +0,0 @@
-use super::combinators::{self, Parameter};
-use crate::{
- completion::types::{Item, ItemData},
- feature::FeatureRequest,
- protocol::CompletionParams,
-};
-use std::iter;
-
-pub async fn complete_latex_arguments<'a>(
- req: &'a FeatureRequest<CompletionParams>,
- items: &mut Vec<Item<'a>>,
-) {
- for comp in req.view.components() {
- for cmd in &comp.commands {
- for (i, param) in cmd.parameters.iter().enumerate() {
- complete_internal(req, items, &cmd.name, i, param).await;
- }
- }
- }
-}
-
-async fn complete_internal<'a>(
- req: &'a FeatureRequest<CompletionParams>,
- items: &mut Vec<Item<'a>>,
- name: &'a str,
- index: usize,
- param: &'a crate::components::Parameter,
-) {
- combinators::argument(
- req,
- iter::once(Parameter { name, index }),
- |ctx| async move {
- for arg in &param.0 {
- let item = Item::new(
- ctx.range,
- ItemData::Argument {
- name: &arg.name,
- image: arg.image.as_deref(),
- },
- );
- items.push(item);
- }
- },
- )
- .await;
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- feature::FeatureTester,
- protocol::{Range, RangeExt},
- };
- use indoc::indoc;
-
- #[tokio::test]
- async fn empty_latex_document() {
- let req = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_arguments(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let req = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_arguments(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn inside_mathbb_empty() {
- let req = FeatureTester::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \usepackage{amsfonts}
- \mathbb{}
- "#
- ),
- )
- .main("main.tex")
- .position(1, 8)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_arguments(&req, &mut actual_items).await;
-
- assert!(!actual_items.is_empty());
- assert_eq!(actual_items[0].range, Range::new_simple(1, 8, 1, 8));
- }
-
- #[tokio::test]
- async fn inside_mathbb_non_empty() {
- let req = FeatureTester::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \usepackage{amsfonts}
- \mathbb{foo}
- "#
- ),
- )
- .main("main.tex")
- .position(1, 8)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_arguments(&req, &mut actual_items).await;
-
- assert!(!actual_items.is_empty());
- assert_eq!(actual_items[0].range, Range::new_simple(1, 8, 1, 11));
- }
-
- #[tokio::test]
- async fn outside_mathbb_empty() {
- let req = FeatureTester::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \usepackage{amsfonts}
- \mathbb{}
- "#
- ),
- )
- .main("main.tex")
- .position(1, 9)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_arguments(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-}
-
-/*
-use super::combinators::{self, Parameter};
-use crate::{
- completion::factory,
- feature::{FeatureProvider, FeatureRequest},
- protocol::{CompletionItem, CompletionParams, TextEdit},
-};
-use async_trait::async_trait;
-use std::iter;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct LatexArgumentCompletionProvider;
-
-#[async_trait]
-impl FeatureProvider for LatexArgumentCompletionProvider {
- type Params = CompletionParams;
- type Output = Vec<CompletionItem>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let mut all_items = Vec::new();
- for comp in req.view.components() {
- for cmd in &comp.commands {
- let name = format!("\\{}", cmd.name);
- for (i, param) in cmd.parameters.iter().enumerate() {
- let mut items = combinators::argument(
- req,
- iter::once(Parameter {
- name: &name,
- index: i,
- }),
- |ctx| async move {
- let mut items = Vec::new();
- for arg in &param.0 {
- let text_edit = TextEdit::new(ctx.range, (&arg.name).into());
- let item = factory::argument(
- req,
- &arg.name,
- text_edit,
- arg.image.as_ref().map(AsRef::as_ref),
- );
- items.push(item);
- }
- items
- },
- )
- .await;
- all_items.append(&mut items);
- }
- }
- }
- all_items
- }
-}
-
-
-
-*/
diff --git a/support/texlab/src/completion/latex/begin_cmd.rs b/support/texlab/src/completion/latex/begin_cmd.rs
deleted file mode 100644
index 44e7a4f60d..0000000000
--- a/support/texlab/src/completion/latex/begin_cmd.rs
+++ /dev/null
@@ -1,70 +0,0 @@
-use super::combinators;
-use crate::{
- completion::types::{Item, ItemData},
- feature::FeatureRequest,
- protocol::CompletionParams,
-};
-
-pub async fn complete_latex_begin_command<'a>(
- req: &'a FeatureRequest<CompletionParams>,
- items: &mut Vec<Item<'a>>,
-) {
- combinators::command(req, |cmd_node| async move {
- let table = req.current().content.as_latex().unwrap();
- let cmd = table.as_command(cmd_node).unwrap();
- let range = cmd.short_name_range();
- items.push(Item::new(range, ItemData::BeginCommand));
- })
- .await;
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::feature::FeatureTester;
-
- #[tokio::test]
- async fn empty_latex_document() {
- let req = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_begin_command(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let req = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_begin_command(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn after_backslash() {
- let req = FeatureTester::new()
- .file("main.tex", r#"\"#)
- .main("main.tex")
- .position(0, 1)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_begin_command(&req, &mut actual_items).await;
-
- assert_eq!(actual_items.len(), 1);
- }
-}
diff --git a/support/texlab/src/completion/latex/citation.rs b/support/texlab/src/completion/latex/citation.rs
deleted file mode 100644
index 3e7b1851d1..0000000000
--- a/support/texlab/src/completion/latex/citation.rs
+++ /dev/null
@@ -1,261 +0,0 @@
-use super::combinators::{self, ArgumentContext, Parameter};
-use crate::{
- completion::types::{Item, ItemData},
- feature::FeatureRequest,
- protocol::{BibtexFormattingOptions, CompletionParams},
- syntax::{bibtex, BibtexEntryTypeCategory, Structure, LANGUAGE_DATA},
- workspace::{Document, DocumentContent},
-};
-use once_cell::sync::Lazy;
-use petgraph::graph::NodeIndex;
-use regex::Regex;
-
-pub async fn complete_latex_citations<'a>(
- req: &'a FeatureRequest<CompletionParams>,
- items: &mut Vec<Item<'a>>,
-) {
- let parameters = LANGUAGE_DATA.citation_commands.iter().map(|cmd| Parameter {
- name: &cmd.name[1..],
- index: cmd.index,
- });
-
- combinators::argument(req, parameters, |ctx| async move {
- for doc in req.related() {
- if let DocumentContent::Bibtex(tree) = &doc.content {
- for entry_node in tree.children(tree.root) {
- if let Some(item) = make_item(ctx, doc, tree, entry_node) {
- items.push(item);
- }
- }
- }
- }
- })
- .await;
-}
-
-fn make_item<'a>(
- ctx: ArgumentContext,
- doc: &'a Document,
- tree: &'a bibtex::Tree,
- entry_node: NodeIndex,
-) -> Option<Item<'a>> {
- let entry = tree.as_entry(entry_node)?;
- if entry.is_comment() {
- return None;
- }
-
- let key = entry.key.as_ref()?.text();
- let options = BibtexFormattingOptions::default();
- let params = bibtex::FormattingParams {
- insert_spaces: true,
- tab_size: 4,
- options: &options,
- };
- let entry_code = bibtex::format(tree, entry_node, params);
- let text = format!(
- "{} {}",
- &key,
- WHITESPACE_REGEX
- .replace_all(
- &entry_code
- .replace('{', "")
- .replace('}', "")
- .replace(',', " ")
- .replace('=', " "),
- " ",
- )
- .trim()
- );
-
- let ty = LANGUAGE_DATA
- .find_entry_type(&entry.ty.text()[1..])
- .map(|ty| Structure::Entry(ty.category))
- .unwrap_or_else(|| Structure::Entry(BibtexEntryTypeCategory::Misc));
-
- let item = Item::new(
- ctx.range,
- ItemData::Citation {
- uri: &doc.uri,
- key,
- text,
- ty,
- },
- );
- Some(item)
-}
-
-static WHITESPACE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("\\s+").unwrap());
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- feature::FeatureTester,
- protocol::{Range, RangeExt},
- };
- use indoc::indoc;
-
- #[tokio::test]
- async fn empty_latex_document() {
- let req = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_citations(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let req = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_citations(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn incomplete() {
- let req = FeatureTester::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \addbibresource{main.bib}
- \cite{
- \begin{foo}
- \end{bar}
- "#
- ),
- )
- .file("main.bib", "@article{foo,}")
- .main("main.tex")
- .position(1, 6)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_citations(&req, &mut actual_items).await;
-
- assert_eq!(actual_items.len(), 1);
- assert_eq!(actual_items[0].data.label(), "foo");
- assert_eq!(actual_items[0].range, Range::new_simple(1, 6, 1, 6));
- }
-
- #[tokio::test]
- async fn empty_key() {
- let req = FeatureTester::new()
- .file(
- "foo.tex",
- indoc!(
- r#"
- \addbibresource{bar.bib}
- \cite{}
- "#
- ),
- )
- .file("bar.bib", "@article{foo,}")
- .file("baz.bib", "@article{bar,}")
- .main("foo.tex")
- .position(1, 6)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_citations(&req, &mut actual_items).await;
-
- assert_eq!(actual_items.len(), 1);
- assert_eq!(actual_items[0].data.label(), "foo");
- assert_eq!(actual_items[0].range, Range::new_simple(1, 6, 1, 6));
- }
-
- #[tokio::test]
- async fn single_key() {
- let req = FeatureTester::new()
- .file(
- "foo.tex",
- indoc!(
- r#"
- \addbibresource{bar.bib}
- \cite{foo}
- "#
- ),
- )
- .file("bar.bib", "@article{foo,}")
- .file("baz.bib", "@article{bar,}")
- .main("foo.tex")
- .position(1, 6)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_citations(&req, &mut actual_items).await;
-
- assert_eq!(actual_items.len(), 1);
- assert_eq!(actual_items[0].data.label(), "foo");
- assert_eq!(actual_items[0].range, Range::new_simple(1, 6, 1, 9));
- }
-
- #[tokio::test]
- async fn second_key() {
- let req = FeatureTester::new()
- .file(
- "foo.tex",
- indoc!(
- r#"
- \addbibresource{bar.bib}
- \cite{foo,}
- "#
- ),
- )
- .file("bar.bib", "@article{foo,}")
- .file("baz.bib", "@article{bar,}")
- .main("foo.tex")
- .position(1, 10)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_citations(&req, &mut actual_items).await;
-
- assert_eq!(actual_items.len(), 1);
- assert_eq!(actual_items[0].data.label(), "foo");
- assert_eq!(actual_items[0].range, Range::new_simple(1, 10, 1, 10));
- }
-
- #[tokio::test]
- async fn outside_cite() {
- let req = FeatureTester::new()
- .file(
- "foo.tex",
- indoc!(
- r#"
- \addbibresource{bar.bib}
- \cite{}
- "#
- ),
- )
- .file("bar.bib", "@article{foo,}")
- .file("baz.bib", "@article{bar,}")
- .main("foo.tex")
- .position(1, 7)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_citations(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-}
diff --git a/support/texlab/src/completion/latex/color.rs b/support/texlab/src/completion/latex/color.rs
deleted file mode 100644
index 93b06b178e..0000000000
--- a/support/texlab/src/completion/latex/color.rs
+++ /dev/null
@@ -1,91 +0,0 @@
-use super::combinators::{self, Parameter};
-use crate::{
- completion::types::{Item, ItemData},
- feature::FeatureRequest,
- protocol::CompletionParams,
- syntax::LANGUAGE_DATA,
-};
-
-pub async fn complete_latex_colors<'a>(
- req: &'a FeatureRequest<CompletionParams>,
- items: &mut Vec<Item<'a>>,
-) {
- let parameters = LANGUAGE_DATA.color_commands.iter().map(|cmd| Parameter {
- name: &cmd.name[1..],
- index: cmd.index,
- });
-
- combinators::argument(req, parameters, |ctx| async move {
- for name in &LANGUAGE_DATA.colors {
- let item = Item::new(ctx.range, ItemData::Color { name });
- items.push(item);
- }
- })
- .await;
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- feature::FeatureTester,
- protocol::{Range, RangeExt},
- };
-
- #[tokio::test]
- async fn empty_latex_document() {
- let req = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
- complete_latex_colors(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let req = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
- complete_latex_colors(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn inside_color() {
- let req = FeatureTester::new()
- .file("main.tex", r#"\color{}"#)
- .main("main.tex")
- .position(0, 7)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
- complete_latex_colors(&req, &mut actual_items).await;
-
- assert!(!actual_items.is_empty());
- assert_eq!(actual_items[0].range, Range::new_simple(0, 7, 0, 7));
- }
-
- #[tokio::test]
- async fn inside_define_color_set() {
- let req = FeatureTester::new()
- .file("main.tex", r#"\color{}"#)
- .main("main.tex")
- .position(0, 8)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
- complete_latex_colors(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-}
diff --git a/support/texlab/src/completion/latex/color_model.rs b/support/texlab/src/completion/latex/color_model.rs
deleted file mode 100644
index e0151da2f0..0000000000
--- a/support/texlab/src/completion/latex/color_model.rs
+++ /dev/null
@@ -1,101 +0,0 @@
-use super::combinators::{self, Parameter};
-use crate::{
- completion::types::{Item, ItemData},
- feature::FeatureRequest,
- protocol::CompletionParams,
- syntax::LANGUAGE_DATA,
-};
-
-const MODEL_NAMES: &[&str] = &["gray", "rgb", "RGB", "HTML", "cmyk"];
-
-pub async fn complete_latex_color_models<'a>(
- req: &'a FeatureRequest<CompletionParams>,
- items: &mut Vec<Item<'a>>,
-) {
- let parameters = LANGUAGE_DATA
- .color_model_commands
- .iter()
- .map(|cmd| Parameter {
- name: &cmd.name[1..],
- index: cmd.index,
- });
-
- combinators::argument(req, parameters, |ctx| async move {
- for name in MODEL_NAMES {
- let item = Item::new(ctx.range, ItemData::ColorModel { name });
- items.push(item);
- }
- })
- .await;
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- feature::FeatureTester,
- protocol::{Range, RangeExt},
- };
-
- #[tokio::test]
- async fn empty_latex_document() {
- let req = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_color_models(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let req = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_color_models(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn inside_define_color() {
- let req = FeatureTester::new()
- .file("main.tex", r#"\definecolor{name}{}"#)
- .main("main.tex")
- .position(0, 19)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_color_models(&req, &mut actual_items).await;
-
- assert!(!actual_items.is_empty());
- assert_eq!(actual_items[0].range, Range::new_simple(0, 19, 0, 19));
- }
-
- #[tokio::test]
- async fn inside_define_color_set() {
- let req = FeatureTester::new()
- .file("main.tex", r#"\definecolorset{}"#)
- .main("main.tex")
- .position(0, 16)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_color_models(&req, &mut actual_items).await;
-
- assert!(!actual_items.is_empty());
- assert_eq!(actual_items[0].range, Range::new_simple(0, 16, 0, 16));
- }
-}
diff --git a/support/texlab/src/completion/latex/combinators.rs b/support/texlab/src/completion/latex/combinators.rs
deleted file mode 100644
index d8ef336421..0000000000
--- a/support/texlab/src/completion/latex/combinators.rs
+++ /dev/null
@@ -1,143 +0,0 @@
-use crate::{
- feature::FeatureRequest,
- protocol::{CompletionParams, Position, Range, RangeExt},
- syntax::{latex, AstNodeIndex, SyntaxNode, LANGUAGE_DATA},
- workspace::DocumentContent,
-};
-use std::future::Future;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub struct Parameter<'a> {
- pub name: &'a str,
- pub index: usize,
-}
-
-pub async fn command<E, F>(req: &FeatureRequest<CompletionParams>, execute: E)
-where
- E: FnOnce(AstNodeIndex) -> F,
- F: Future<Output = ()>,
-{
- if let DocumentContent::Latex(table) = &req.current().content {
- let pos = req.params.text_document_position.position;
- if let Some(cmd) = table.find_command_by_short_name_range(pos) {
- execute(cmd).await;
- }
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub struct ArgumentContext<'a> {
- pub parameter: Parameter<'a>,
- pub node: AstNodeIndex,
- pub range: Range,
-}
-
-pub async fn argument<'a, I, E, F>(
- req: &'a FeatureRequest<CompletionParams>,
- mut parameters: I,
- execute: E,
-) where
- I: Iterator<Item = Parameter<'a>>,
- E: FnOnce(ArgumentContext<'a>) -> F,
- F: Future<Output = ()>,
-{
- if let DocumentContent::Latex(table) = &req.current().content {
- let pos = req.params.text_document_position.position;
- if let Some(node) = find_command(&table, pos) {
- let cmd = table.as_command(node).unwrap();
- for parameter in parameters
- .by_ref()
- .filter(|param| param.name == &cmd.name.text()[1..])
- {
- if let Some(args_node) =
- table.extract_group(node, latex::GroupKind::Group, parameter.index)
- {
- let args = table.as_group(args_node).unwrap();
- if args.right.is_some() && !args.range().contains_exclusive(pos) {
- continue;
- }
-
- let range = table
- .children(args_node)
- .filter_map(|child| table.as_text(child))
- .flat_map(|text| text.words.iter())
- .map(|word| word.range())
- .find(|range| range.contains(pos))
- .unwrap_or_else(|| Range::new(pos, pos));
-
- let context = ArgumentContext {
- parameter,
- node,
- range,
- };
- execute(context).await;
- return;
- }
- }
- }
- }
-}
-
-pub async fn argument_word<'a, I, E, F>(
- req: &'a FeatureRequest<CompletionParams>,
- mut parameters: I,
- execute: E,
-) where
- I: Iterator<Item = Parameter<'a>>,
- E: FnOnce(AstNodeIndex, usize) -> F,
- F: Future<Output = ()>,
-{
- if let DocumentContent::Latex(table) = &req.current().content {
- let pos = req.params.text_document_position.position;
- if let Some(node) = find_command(&table, pos) {
- let cmd = table.as_command(node).unwrap();
- for parameter in parameters
- .by_ref()
- .filter(|param| param.name == &cmd.name.text()[1..])
- {
- if let Some(args_node) =
- table.extract_group(node, latex::GroupKind::Group, parameter.index)
- {
- let args = table.as_group(args_node).unwrap();
- if args.right.is_some() && !args.range().contains_exclusive(pos) {
- continue;
- }
-
- if table.children(args_node).next().is_some()
- && table
- .extract_word(node, latex::GroupKind::Group, parameter.index)
- .is_none()
- {
- continue;
- }
-
- execute(node, parameter.index).await;
- return;
- }
- }
- }
- }
-}
-
-pub async fn environment<'a, E, F>(req: &'a FeatureRequest<CompletionParams>, execute: E)
-where
- E: FnOnce(ArgumentContext<'a>) -> F,
- F: Future<Output = ()>,
-{
- let parameters = LANGUAGE_DATA
- .environment_commands
- .iter()
- .map(|cmd| Parameter {
- name: &cmd.name[1..],
- index: cmd.index,
- });
- argument(req, parameters, execute).await;
-}
-
-fn find_command(table: &latex::SymbolTable, pos: Position) -> Option<AstNodeIndex> {
- table
- .find(pos)
- .into_iter()
- .rev()
- .find(|node| table.as_command(*node).is_some())
-}
diff --git a/support/texlab/src/completion/latex/component.rs b/support/texlab/src/completion/latex/component.rs
deleted file mode 100644
index 8cf9f494a4..0000000000
--- a/support/texlab/src/completion/latex/component.rs
+++ /dev/null
@@ -1,351 +0,0 @@
-use super::combinators;
-use crate::{
- completion::types::{Item, ItemData},
- feature::FeatureRequest,
- protocol::CompletionParams,
-};
-
-pub async fn complete_latex_component_commands<'a>(
- req: &'a FeatureRequest<CompletionParams>,
- items: &mut Vec<Item<'a>>,
-) {
- combinators::command(req, |cmd_node| async move {
- let table = req.current().content.as_latex().unwrap();
- let cmd = table.as_command(cmd_node).unwrap();
- let range = cmd.short_name_range();
-
- for comp in req.view.components() {
- for cmd in &comp.commands {
- items.push(Item::new(
- range,
- ItemData::ComponentCommand {
- name: &cmd.name,
- image: cmd.image.as_deref(),
- glyph: cmd.glyph.as_deref(),
- file_names: &comp.file_names,
- },
- ));
- }
- }
- })
- .await;
-}
-
-pub async fn complete_latex_component_environments<'a>(
- req: &'a FeatureRequest<CompletionParams>,
- items: &mut Vec<Item<'a>>,
-) {
- combinators::environment(req, |ctx| async move {
- for comp in req.view.components() {
- for env in &comp.environments {
- items.push(Item::new(
- ctx.range,
- ItemData::ComponentEnvironment {
- name: env,
- file_names: &comp.file_names,
- },
- ));
- }
- }
- })
- .await;
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- feature::FeatureTester,
- protocol::{Range, RangeExt},
- };
- use indoc::indoc;
-
- #[tokio::test]
- async fn empty_latex_document_command() {
- let req = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_component_commands(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document_command() {
- let req = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_component_commands(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn empty_latex_document_environment() {
- let req = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_component_environments(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document_environment() {
- let req = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_component_environments(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn command_start() {
- let req = FeatureTester::new()
- .file("main.tex", r#"\use"#)
- .main("main.tex")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_component_commands(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn command_end() {
- let req = FeatureTester::new()
- .file("main.tex", r#"\use"#)
- .main("main.tex")
- .position(0, 4)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_component_commands(&req, &mut actual_items).await;
-
- assert!(!actual_items.is_empty());
- assert_eq!(actual_items[0].range, Range::new_simple(0, 1, 0, 4));
- }
-
- #[tokio::test]
- async fn command_word() {
- let req = FeatureTester::new()
- .file("main.tex", r#"use"#)
- .main("main.tex")
- .position(0, 2)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_component_commands(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn command_package() {
- let req = FeatureTester::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \usepackage{lipsum}
- \lips
- "#
- ),
- )
- .main("main.tex")
- .position(1, 2)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_component_commands(&req, &mut actual_items).await;
-
- assert!(actual_items
- .iter()
- .any(|item| item.data.label() == "lipsum"));
- }
-
- #[tokio::test]
- async fn command_package_comma_separated() {
- let req = FeatureTester::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \usepackage{geometry, lipsum}
- \lips
- "#
- ),
- )
- .main("main.tex")
- .position(1, 2)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_component_commands(&req, &mut actual_items).await;
-
- assert!(actual_items
- .iter()
- .any(|item| item.data.label() == "lipsum"));
- }
-
- #[tokio::test]
- async fn command_class() {
- let req = FeatureTester::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \documentclass{book}
- \chap
- "#
- ),
- )
- .main("main.tex")
- .position(1, 2)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_component_commands(&req, &mut actual_items).await;
-
- assert!(actual_items
- .iter()
- .any(|item| item.data.label() == "chapter"));
- }
-
- #[tokio::test]
- async fn environment_inside_of_empty_begin() {
- let req = FeatureTester::new()
- .file("main.tex", r#"\begin{}"#)
- .main("main.tex")
- .position(0, 7)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_component_environments(&req, &mut actual_items).await;
-
- assert!(!actual_items.is_empty());
- assert_eq!(actual_items[0].range, Range::new_simple(0, 7, 0, 7));
- }
-
- #[tokio::test]
- async fn environment_inside_of_non_empty_end() {
- let req = FeatureTester::new()
- .file("main.tex", r#"\end{foo}"#)
- .main("main.tex")
- .position(0, 6)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_component_environments(&req, &mut actual_items).await;
-
- assert!(!actual_items.is_empty());
- assert_eq!(actual_items[0].range, Range::new_simple(0, 5, 0, 8));
- }
-
- #[tokio::test]
- async fn environment_outside_of_empty_begin() {
- let req = FeatureTester::new()
- .file("main.tex", r#"\begin{}"#)
- .main("main.tex")
- .position(0, 6)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_component_environments(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn environment_outside_of_empty_end() {
- let req = FeatureTester::new()
- .file("main.tex", r#"\end{}"#)
- .main("main.tex")
- .position(0, 6)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_component_environments(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn environment_inside_of_other_command() {
- let req = FeatureTester::new()
- .file("main.tex", r#"\foo{bar}"#)
- .main("main.tex")
- .position(0, 6)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_component_environments(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn environment_inside_second_argument() {
- let req = FeatureTester::new()
- .file("main.tex", r#"\begin{foo}{bar}"#)
- .main("main.tex")
- .position(0, 14)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_component_environments(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn environment_unterminated() {
- let req = FeatureTester::new()
- .file("main.tex", r#"\begin{foo"#)
- .main("main.tex")
- .position(0, 7)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_component_environments(&req, &mut actual_items).await;
-
- assert!(!actual_items.is_empty());
- assert_eq!(actual_items[0].range, Range::new_simple(0, 7, 0, 10));
- }
-}
diff --git a/support/texlab/src/completion/latex/glossary.rs b/support/texlab/src/completion/latex/glossary.rs
deleted file mode 100644
index 417152dd21..0000000000
--- a/support/texlab/src/completion/latex/glossary.rs
+++ /dev/null
@@ -1,114 +0,0 @@
-use super::combinators::{self, Parameter};
-use crate::{
- completion::types::{Item, ItemData},
- feature::FeatureRequest,
- protocol::CompletionParams,
- syntax::{
- LatexGlossaryEntryKind::{Acronym, General},
- LANGUAGE_DATA,
- },
- workspace::DocumentContent,
-};
-
-pub async fn complete_latex_glossary_entries<'a>(
- req: &'a FeatureRequest<CompletionParams>,
- items: &mut Vec<Item<'a>>,
-) {
- let parameters = LANGUAGE_DATA
- .glossary_entry_reference_commands
- .iter()
- .map(|cmd| Parameter {
- name: &cmd.name[1..],
- index: cmd.index,
- });
-
- combinators::argument(req, parameters, |ctx| async move {
- let cmd_kind = LANGUAGE_DATA
- .glossary_entry_reference_commands
- .iter()
- .find(|cmd| &cmd.name[1..] == ctx.parameter.name)
- .unwrap()
- .kind;
-
- for doc in req.related() {
- if let DocumentContent::Latex(table) = &doc.content {
- for entry in &table.glossary_entries {
- match (cmd_kind, entry.kind) {
- (Acronym, Acronym) | (General, General) | (General, Acronym) => {
- let name = entry.label(&table).text();
- let item = Item::new(ctx.range, ItemData::GlossaryEntry { name });
- items.push(item);
- }
- (Acronym, General) => {}
- }
- }
- }
- }
- })
- .await
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- feature::FeatureTester,
- protocol::{Range, RangeExt},
- };
- use indoc::indoc;
-
- #[tokio::test]
- async fn empty_latex_document() {
- let req = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_glossary_entries(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let req = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_glossary_entries(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn acronym() {
- let req = FeatureTester::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \newacronym{lvm}{LVM}{Logical Volume Manager}
- \acrfull{foo}
- "#
- ),
- )
- .main("main.tex")
- .position(1, 9)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_glossary_entries(&req, &mut actual_items).await;
-
- assert_eq!(actual_items.len(), 1);
- assert_eq!(actual_items[0].data.label(), "lvm");
- assert_eq!(actual_items[0].range, Range::new_simple(1, 9, 1, 12));
- }
-}
diff --git a/support/texlab/src/completion/latex/import.rs b/support/texlab/src/completion/latex/import.rs
deleted file mode 100644
index 06c28e189b..0000000000
--- a/support/texlab/src/completion/latex/import.rs
+++ /dev/null
@@ -1,188 +0,0 @@
-use super::combinators::{self, Parameter};
-use crate::{
- completion::types::{Item, ItemData},
- components::COMPONENT_DATABASE,
- feature::FeatureRequest,
- protocol::CompletionParams,
- syntax::{LatexIncludeKind, LANGUAGE_DATA},
-};
-use std::{borrow::Cow, collections::HashSet};
-
-pub async fn complete_latex_classes<'a>(
- req: &'a FeatureRequest<CompletionParams>,
- items: &mut Vec<Item<'a>>,
-) {
- complete_latex_imports(req, items, LatexIncludeKind::Class, |name| {
- ItemData::Class { name }
- })
- .await;
-}
-
-pub async fn complete_latex_packages<'a>(
- req: &'a FeatureRequest<CompletionParams>,
- items: &mut Vec<Item<'a>>,
-) {
- complete_latex_imports(req, items, LatexIncludeKind::Package, |name| {
- ItemData::Package { name }
- })
- .await;
-}
-
-async fn complete_latex_imports<'a, F>(
- req: &'a FeatureRequest<CompletionParams>,
- items: &mut Vec<Item<'a>>,
- kind: LatexIncludeKind,
- mut factory: F,
-) where
- F: FnMut(Cow<'a, str>) -> ItemData<'a>,
-{
- let extension = if kind == LatexIncludeKind::Package {
- "sty"
- } else {
- "cls"
- };
-
- let parameters = LANGUAGE_DATA
- .include_commands
- .iter()
- .filter(|cmd| cmd.kind == kind)
- .map(|cmd| Parameter {
- name: &cmd.name[1..],
- index: cmd.index,
- });
-
- combinators::argument(req, parameters, |ctx| async move {
- let resolver = req.distro.resolver().await;
- let mut file_names = HashSet::new();
- COMPONENT_DATABASE
- .components
- .iter()
- .flat_map(|comp| comp.file_names.iter())
- .filter(|file_name| file_name.ends_with(extension))
- .for_each(|file_name| {
- file_names.insert(file_name);
- let stem = &file_name[0..file_name.len() - 4];
- let data = factory(stem.into());
- let item = Item::new(ctx.range, data);
- items.push(item);
- });
-
- resolver
- .files_by_name
- .keys()
- .filter(|file_name| file_name.ends_with(extension) && !file_names.contains(file_name))
- .for_each(|file_name| {
- let stem = &file_name[0..file_name.len() - 4];
- let data = factory(stem.to_owned().into());
- let item = Item::new(ctx.range, data);
- items.push(item);
- });
- })
- .await;
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::feature::FeatureTester;
-
- #[tokio::test]
- async fn empty_latex_document_class() {
- let req = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_classes(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document_class() {
- let req = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_classes(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn empty_latex_document_package() {
- let req = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_packages(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document_package() {
- let req = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_packages(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn class() {
- let req = FeatureTester::new()
- .file("main.tex", r#"\documentclass{}"#)
- .main("main.tex")
- .position(0, 15)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_classes(&req, &mut actual_items).await;
-
- assert!(actual_items
- .iter()
- .any(|item| item.data.label() == "beamer"));
- assert!(actual_items
- .iter()
- .all(|item| item.data.label() != "amsmath"));
- }
-
- #[tokio::test]
- async fn package() {
- let req = FeatureTester::new()
- .file("main.tex", r#"\usepackage{}"#)
- .main("main.tex")
- .position(0, 12)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_packages(&req, &mut actual_items).await;
-
- assert!(actual_items
- .iter()
- .all(|item| item.data.label() != "beamer"));
- assert!(actual_items
- .iter()
- .any(|item| item.data.label() == "amsmath"));
- }
-}
diff --git a/support/texlab/src/completion/latex/include.rs b/support/texlab/src/completion/latex/include.rs
deleted file mode 100644
index 1a8555d07c..0000000000
--- a/support/texlab/src/completion/latex/include.rs
+++ /dev/null
@@ -1,133 +0,0 @@
-use super::combinators::{self, Parameter};
-use crate::{
- completion::types::{Item, ItemData},
- feature::FeatureRequest,
- protocol::{CompletionParams, Range, RangeExt},
- syntax::{latex, AstNodeIndex, SyntaxNode, LANGUAGE_DATA},
-};
-use std::path::{Path, PathBuf};
-use tokio::fs;
-
-pub async fn complete_latex_includes<'a>(
- req: &'a FeatureRequest<CompletionParams>,
- items: &mut Vec<Item<'a>>,
-) {
- let parameters = LANGUAGE_DATA.include_commands.iter().map(|cmd| Parameter {
- name: &cmd.name[1..],
- index: cmd.index,
- });
-
- combinators::argument_word(req, parameters, |cmd_node, index| async move {
- if !req.current().is_file() {
- return;
- }
-
- make_items(req, items, cmd_node, index).await;
- })
- .await;
-}
-
-async fn make_items<'a>(
- req: &'a FeatureRequest<CompletionParams>,
- items: &mut Vec<Item<'a>>,
- cmd_node: AstNodeIndex,
- index: usize,
-) -> Option<()> {
- let table = req.current().content.as_latex()?;
- let pos = req.params.text_document_position.position;
- let path_word = table.extract_word(cmd_node, latex::GroupKind::Group, index);
- let name_range = match path_word {
- Some(path_word) => Range::new_simple(
- path_word.start().line,
- path_word.end().character - path_word.text().split('/').last()?.chars().count() as u64,
- path_word.end().line,
- path_word.end().character,
- ),
- None => Range::new(pos, pos),
- };
-
- let cmd = table.as_command(cmd_node)?;
- let current_dir = current_dir(req, table, cmd_node)?;
- let mut entries = fs::read_dir(current_dir).await.ok()?;
- while let Some(entry) = entries.next_entry().await.ok()? {
- let mut path = entry.path();
-
- let file_type = entry.file_type().await.ok()?;
- if file_type.is_file() && is_included(&cmd, &path) {
- let include_extension = LANGUAGE_DATA
- .include_commands
- .iter()
- .find(|c| cmd.name.text() == c.name)?
- .include_extension;
-
- if !include_extension {
- remove_extension(&mut path);
- }
- let name = path.file_name().unwrap().to_string_lossy().into_owned();
- let item = Item::new(name_range, ItemData::File { name });
- items.push(item);
- } else if file_type.is_dir() {
- let name = path.file_name().unwrap().to_string_lossy().into_owned();
- let item = Item::new(name_range, ItemData::Directory { name });
- items.push(item);
- }
- }
- Some(())
-}
-
-fn current_dir(
- req: &FeatureRequest<CompletionParams>,
- table: &latex::SymbolTable,
- cmd_node: AstNodeIndex,
-) -> Option<PathBuf> {
- let mut path = req
- .options
- .latex
- .as_ref()
- .and_then(|latex| latex.root_directory.as_ref())
- .map_or_else(
- || {
- let mut path = req.current().uri.to_file_path().unwrap();
- path.pop();
- path
- },
- Clone::clone,
- );
-
- path = PathBuf::from(path.to_str()?.replace('\\', "/"));
- if let Some(include) = table.extract_word(cmd_node, latex::GroupKind::Group, 0) {
- path.push(include.text());
- if !include.text().ends_with('/') {
- path.pop();
- }
- }
- Some(path)
-}
-
-fn is_included(cmd: &latex::Command, file: &Path) -> bool {
- if let Some(allowed_extensions) = LANGUAGE_DATA
- .include_commands
- .iter()
- .find(|c| c.name == cmd.name.text())
- .and_then(|c| c.kind.extensions())
- {
- file.extension()
- .and_then(|ext| ext.to_str())
- .map(|ext| ext.to_lowercase())
- .map(|ext| allowed_extensions.contains(&ext.as_str()))
- .unwrap_or_default()
- } else {
- true
- }
-}
-
-fn remove_extension(path: &mut PathBuf) {
- if let Some(stem) = path
- .file_stem()
- .and_then(|stem| stem.to_str())
- .map(ToOwned::to_owned)
- {
- path.pop();
- path.push(stem);
- }
-}
diff --git a/support/texlab/src/completion/latex/label.rs b/support/texlab/src/completion/latex/label.rs
deleted file mode 100644
index 91aa80e532..0000000000
--- a/support/texlab/src/completion/latex/label.rs
+++ /dev/null
@@ -1,230 +0,0 @@
-use super::combinators::{self, ArgumentContext, Parameter};
-use crate::{
- completion::types::{Item, ItemData},
- feature::{DocumentView, FeatureRequest},
- outline::{Outline, OutlineContext, OutlineContextItem},
- protocol::{CompletionParams, RangeExt},
- syntax::{
- latex, LatexLabelKind, LatexLabelReferenceSource, Structure, SyntaxNode, LANGUAGE_DATA,
- },
- workspace::DocumentContent,
-};
-use std::sync::Arc;
-
-pub async fn complete_latex_labels<'a>(
- req: &'a FeatureRequest<CompletionParams>,
- items: &mut Vec<Item<'a>>,
-) {
- let parameters = LANGUAGE_DATA
- .label_commands
- .iter()
- .filter(|cmd| cmd.kind.is_reference())
- .map(|cmd| Parameter {
- name: &cmd.name[1..],
- index: cmd.index,
- });
-
- combinators::argument(req, parameters, |ctx| async move {
- let source = find_source(ctx);
- for doc in req.related() {
- let snapshot = Arc::clone(&req.view.snapshot);
- let view =
- DocumentView::analyze(snapshot, Arc::clone(&doc), &req.options, &req.current_dir);
- let outline = Outline::analyze(&view, &req.options, &req.current_dir);
-
- if let DocumentContent::Latex(table) = &doc.content {
- for label in table
- .labels
- .iter()
- .filter(|label| label.kind == LatexLabelKind::Definition)
- .filter(|label| is_included(&table, label, source))
- {
- let outline_ctx = OutlineContext::parse(&view, &outline, *label);
-
- let kind = match outline_ctx.as_ref().map(|ctx| &ctx.item) {
- Some(OutlineContextItem::Section { .. }) => Structure::Section,
- Some(OutlineContextItem::Caption { .. }) => Structure::Float,
- Some(OutlineContextItem::Theorem { .. }) => Structure::Theorem,
- Some(OutlineContextItem::Equation) => Structure::Equation,
- Some(OutlineContextItem::Item) => Structure::Item,
- None => Structure::Label,
- };
-
- for name in label.names(&table) {
- let header = outline_ctx.as_ref().and_then(|ctx| ctx.detail());
- let footer = outline_ctx.as_ref().and_then(|ctx| match &ctx.item {
- OutlineContextItem::Caption { text, .. } => Some(text.clone()),
- _ => None,
- });
-
- let text = outline_ctx
- .as_ref()
- .map(|ctx| format!("{} {}", name.text(), ctx.reference()))
- .unwrap_or_else(|| name.text().into());
-
- let item = Item::new(
- ctx.range,
- ItemData::Label {
- name: name.text(),
- kind,
- header,
- footer,
- text,
- },
- );
- items.push(item);
- }
- }
- }
- }
- })
- .await;
-}
-
-fn find_source(ctx: ArgumentContext) -> LatexLabelReferenceSource {
- match LANGUAGE_DATA
- .label_commands
- .iter()
- .find(|cmd| &cmd.name[1..] == ctx.parameter.name && cmd.index == ctx.parameter.index)
- .map(|cmd| cmd.kind)
- .unwrap()
- {
- LatexLabelKind::Definition => unreachable!(),
- LatexLabelKind::Reference(source) => source,
- }
-}
-
-fn is_included(
- table: &latex::SymbolTable,
- label: &latex::Label,
- source: LatexLabelReferenceSource,
-) -> bool {
- let label_range = table[label.parent].range();
- match source {
- LatexLabelReferenceSource::Everything => true,
- LatexLabelReferenceSource::Math => table
- .environments
- .iter()
- .filter(|env| env.left.is_math(&table))
- .any(|env| env.range(&table).contains_exclusive(label_range.start)),
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::feature::FeatureTester;
- use indoc::indoc;
-
- #[tokio::test]
- async fn empty_latex_document() {
- let req = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_labels(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let req = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_labels(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn inside_of_ref() {
- let req = FeatureTester::new()
- .file(
- "foo.tex",
- indoc!(
- r#"
- \addbibresource{bar.bib}
- \include{baz}
- \ref{}
- "#
- ),
- )
- .file("bar.bib", "")
- .file("baz.tex", r#"\label{foo}\label{bar}\ref{baz}"#)
- .main("foo.tex")
- .position(2, 5)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_labels(&req, &mut actual_items).await;
-
- let actual_labels: Vec<_> = actual_items
- .into_iter()
- .map(|item| item.data.label().to_owned())
- .collect();
- assert_eq!(actual_labels, vec!["foo", "bar"]);
- }
-
- #[tokio::test]
- async fn outside_of_ref() {
- let req = FeatureTester::new()
- .file(
- "foo.tex",
- indoc!(
- r#"
- \include{bar}
- \ref{}
- "#
- ),
- )
- .file("bar.tex", r#"\label{foo}\label{bar}"#)
- .main("foo.tex")
- .position(1, 6)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_labels(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn eqref() {
- let req = FeatureTester::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \begin{align}\label{foo}\end{align}\label{bar}
- \eqref{}
- "#
- ),
- )
- .main("main.tex")
- .position(1, 7)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_labels(&req, &mut actual_items).await;
-
- let actual_labels: Vec<_> = actual_items
- .into_iter()
- .map(|item| item.data.label().to_owned())
- .collect();
-
- assert_eq!(actual_labels, vec!["foo"]);
- }
-}
diff --git a/support/texlab/src/completion/latex/mod.rs b/support/texlab/src/completion/latex/mod.rs
deleted file mode 100644
index 519d00e5f7..0000000000
--- a/support/texlab/src/completion/latex/mod.rs
+++ /dev/null
@@ -1,14 +0,0 @@
-pub mod argument;
-pub mod begin_cmd;
-pub mod citation;
-pub mod color;
-pub mod color_model;
-mod combinators;
-pub mod component;
-pub mod glossary;
-pub mod import;
-pub mod include;
-pub mod label;
-pub mod theorem;
-pub mod tikz_lib;
-pub mod user;
diff --git a/support/texlab/src/completion/latex/theorem.rs b/support/texlab/src/completion/latex/theorem.rs
deleted file mode 100644
index a8706bed0b..0000000000
--- a/support/texlab/src/completion/latex/theorem.rs
+++ /dev/null
@@ -1,115 +0,0 @@
-use super::combinators;
-use crate::{
- completion::types::{Item, ItemData},
- feature::FeatureRequest,
- protocol::CompletionParams,
-};
-
-pub async fn complete_latex_theorem_environments<'a>(
- req: &'a FeatureRequest<CompletionParams>,
- items: &mut Vec<Item<'a>>,
-) {
- combinators::environment(req, |ctx| async move {
- for table in req
- .related()
- .into_iter()
- .filter_map(|doc| doc.content.as_latex())
- {
- for theorem in &table.theorem_definitions {
- let name = theorem.name(&table).text();
- let data = ItemData::UserEnvironment { name };
- let item = Item::new(ctx.range, data);
- items.push(item);
- }
- }
- })
- .await;
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- feature::FeatureTester,
- protocol::{Range, RangeExt},
- };
- use indoc::indoc;
-
- #[tokio::test]
- async fn empty_latex_document() {
- let req = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_theorem_environments(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let req = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_theorem_environments(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn inside_begin() {
- let req = FeatureTester::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \newtheorem{theorem}{Theorem}
- \begin{th}
- "#
- ),
- )
- .main("main.tex")
- .position(1, 8)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_theorem_environments(&req, &mut actual_items).await;
-
- assert_eq!(actual_items.len(), 1);
- assert_eq!(actual_items[0].data.label(), "theorem");
- assert_eq!(actual_items[0].range, Range::new_simple(1, 7, 1, 9));
- }
-
- #[tokio::test]
- async fn outside_begin() {
- let req = FeatureTester::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \newtheorem{theorem}{Theorem}
- \begin{th}
- "#
- ),
- )
- .main("main.tex")
- .position(1, 10)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_theorem_environments(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-}
diff --git a/support/texlab/src/completion/latex/tikz_lib.rs b/support/texlab/src/completion/latex/tikz_lib.rs
deleted file mode 100644
index fb51fd7229..0000000000
--- a/support/texlab/src/completion/latex/tikz_lib.rs
+++ /dev/null
@@ -1,138 +0,0 @@
-use super::combinators::{self, Parameter};
-use crate::{
- completion::types::{Item, ItemData},
- feature::FeatureRequest,
- protocol::CompletionParams,
- syntax::LANGUAGE_DATA,
-};
-use std::iter;
-
-pub async fn complete_latex_pgf_libraries<'a>(
- req: &'a FeatureRequest<CompletionParams>,
- items: &mut Vec<Item<'a>>,
-) {
- let param = Parameter {
- name: "usepgflibrary",
- index: 0,
- };
- combinators::argument(req, iter::once(param), |ctx| async move {
- for name in &LANGUAGE_DATA.pgf_libraries {
- let item = Item::new(ctx.range, ItemData::PgfLibrary { name });
- items.push(item);
- }
- })
- .await;
-}
-
-pub async fn complete_latex_tikz_libraries<'a>(
- req: &'a FeatureRequest<CompletionParams>,
- items: &mut Vec<Item<'a>>,
-) {
- let param = Parameter {
- name: "usetikzlibrary",
- index: 0,
- };
- combinators::argument(req, iter::once(param), |ctx| async move {
- for name in &LANGUAGE_DATA.tikz_libraries {
- let item = Item::new(ctx.range, ItemData::TikzLibrary { name });
- items.push(item);
- }
- })
- .await;
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::feature::FeatureTester;
-
- #[tokio::test]
- async fn empty_latex_document_pgf() {
- let req = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_pgf_libraries(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document_pgf() {
- let req = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_pgf_libraries(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn empty_latex_document_tikz() {
- let req = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_tikz_libraries(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document_tikz() {
- let req = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_tikz_libraries(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn pgf_library() {
- let req = FeatureTester::new()
- .file("main.tex", r#"\usepgflibrary{}"#)
- .main("main.tex")
- .position(0, 15)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_pgf_libraries(&req, &mut actual_items).await;
-
- assert!(!actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn tikz_library() {
- let req = FeatureTester::new()
- .file("main.tex", r#"\usetikzlibrary{}"#)
- .main("main.tex")
- .position(0, 16)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_tikz_libraries(&req, &mut actual_items).await;
-
- assert!(!actual_items.is_empty());
- }
-}
diff --git a/support/texlab/src/completion/latex/user.rs b/support/texlab/src/completion/latex/user.rs
deleted file mode 100644
index 0a5706958a..0000000000
--- a/support/texlab/src/completion/latex/user.rs
+++ /dev/null
@@ -1,204 +0,0 @@
-use super::combinators;
-use crate::{
- completion::{Item, ItemData},
- feature::FeatureRequest,
- protocol::{CompletionParams, Range},
- syntax::latex,
- workspace::DocumentContent,
-};
-
-pub async fn complete_latex_user_commands<'a>(
- req: &'a FeatureRequest<CompletionParams>,
- items: &mut Vec<Item<'a>>,
-) {
- combinators::command(req, |current_cmd_node| async move {
- let current_cmd = req
- .current()
- .content
- .as_latex()
- .unwrap()
- .as_command(current_cmd_node)
- .unwrap();
-
- for table in req
- .related()
- .into_iter()
- .flat_map(|doc| doc.content.as_latex())
- {
- table
- .commands
- .iter()
- .filter(|cmd_node| **cmd_node != current_cmd_node)
- .map(|cmd_node| {
- let name = &table.as_command(*cmd_node).unwrap().name.text()[1..];
- Item::new(
- current_cmd.short_name_range(),
- ItemData::UserCommand { name },
- )
- })
- .for_each(|item| items.push(item));
- }
- })
- .await;
-}
-
-pub async fn complete_latex_user_environments<'a>(
- req: &'a FeatureRequest<CompletionParams>,
- items: &mut Vec<Item<'a>>,
-) {
- fn make_item(
- table: &latex::SymbolTable,
- delim: latex::EnvironmentDelimiter,
- name_range: Range,
- ) -> Option<Item> {
- delim
- .name(&table)
- .map(|name| Item::new(name_range, ItemData::UserEnvironment { name: &name.text() }))
- }
-
- combinators::environment(req, |ctx| async move {
- for doc in req.related() {
- if let DocumentContent::Latex(table) = &doc.content {
- for env in &table.environments {
- if (env.left.parent == ctx.node || env.right.parent == ctx.node)
- && doc.uri == req.current().uri
- {
- continue;
- }
-
- if let Some(item) = make_item(&table, env.left, ctx.range) {
- items.push(item);
- }
-
- if let Some(item) = make_item(&table, env.right, ctx.range) {
- items.push(item);
- }
- }
- }
- }
- })
- .await;
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::feature::FeatureTester;
- use indoc::indoc;
- use itertools::Itertools;
-
- #[tokio::test]
- async fn empty_latex_document_command() {
- let req = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
- complete_latex_user_commands(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document_command() {
- let req = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
- complete_latex_user_commands(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn empty_latex_document_environment() {
- let req = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
- complete_latex_user_environments(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document_environment() {
- let req = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
- complete_latex_user_environments(&req, &mut actual_items).await;
-
- assert!(actual_items.is_empty());
- }
-
- #[tokio::test]
- async fn command() {
- let req = FeatureTester::new()
- .file(
- "foo.tex",
- indoc!(
- r#"
- \include{bar}
- \foo
- "#
- ),
- )
- .file("bar.tex", r#"\bar"#)
- .file("baz.tex", r#"\baz"#)
- .main("foo.tex")
- .position(1, 2)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_user_commands(&req, &mut actual_items).await;
-
- let actual_labels: Vec<_> = actual_items
- .into_iter()
- .map(|item| item.data.label().to_owned())
- .collect();
- assert_eq!(actual_labels, vec!["include", "bar"]);
- }
-
- #[tokio::test]
- async fn environment() {
- let req = FeatureTester::new()
- .file(
- "foo.tex",
- indoc!(
- r#"
- \include{bar}
- \begin{foo}
- "#
- ),
- )
- .file("bar.tex", r#"\begin{bar}\end{bar}"#)
- .file("baz.tex", r#"\begin{baz}\end{baz}"#)
- .main("foo.tex")
- .position(1, 9)
- .test_completion_request()
- .await;
- let mut actual_items = Vec::new();
-
- complete_latex_user_environments(&req, &mut actual_items).await;
-
- let actual_labels: Vec<_> = actual_items
- .into_iter()
- .map(|item| item.data.label().to_owned())
- .unique()
- .collect();
- assert_eq!(actual_labels, vec!["bar"]);
- }
-}
diff --git a/support/texlab/src/completion/mod.rs b/support/texlab/src/completion/mod.rs
deleted file mode 100644
index d354ed1d98..0000000000
--- a/support/texlab/src/completion/mod.rs
+++ /dev/null
@@ -1,419 +0,0 @@
-mod bibtex;
-mod latex;
-mod types;
-mod util;
-
-pub use self::types::{CompletionItemData, Item, ItemData};
-
-use self::{
- bibtex::{
- cmd::complete_bibtex_commands, entry_type::complete_bibtex_entry_types,
- field_name::complete_bibtex_fields,
- },
- latex::{
- argument::complete_latex_arguments,
- begin_cmd::complete_latex_begin_command,
- citation::complete_latex_citations,
- color::complete_latex_colors,
- color_model::complete_latex_color_models,
- component::{complete_latex_component_commands, complete_latex_component_environments},
- glossary::complete_latex_glossary_entries,
- import::{complete_latex_classes, complete_latex_packages},
- include::complete_latex_includes,
- label::complete_latex_labels,
- theorem::complete_latex_theorem_environments,
- tikz_lib::{complete_latex_pgf_libraries, complete_latex_tikz_libraries},
- user::{complete_latex_user_commands, complete_latex_user_environments},
- },
- util::{adjust_kind, component_detail, current_word, image_documentation},
-};
-use crate::{
- feature::{FeatureProvider, FeatureRequest},
- protocol::{
- CompletionItem, CompletionParams, CompletionTextEdit, Documentation, InsertTextFormat,
- MarkupContent, MarkupKind, RangeExt, TextEdit,
- },
- syntax::{self, Structure, SyntaxNode},
- workspace::DocumentContent,
-};
-use async_trait::async_trait;
-use fuzzy_matcher::FuzzyMatcher;
-use fuzzy_matcher::skim::SkimMatcherV2;
-use std::collections::HashSet;
-
-pub const COMPLETION_LIMIT: usize = 50;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct CompletionProvider;
-
-impl CompletionProvider {
- pub fn new() -> Self {
- Self
- }
-}
-
-#[async_trait]
-impl FeatureProvider for CompletionProvider {
- type Params = CompletionParams;
- type Output = Vec<CompletionItem>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let items = complete_all(req).await;
- let mut items = dedup(items);
- preselect(req, &mut items);
- score(req, &mut items);
-
- items.sort_by_key(|item| (!item.preselect, -item.score.unwrap_or(std::i64::MIN + 1)));
- items
- .into_iter()
- .take(COMPLETION_LIMIT)
- .filter(|item| item.score.is_some())
- .map(|item| convert(req, item))
- .enumerate()
- .map(|(i, item)| append_sort_text(item, i))
- .collect()
- }
-}
-
-async fn complete_all<'a>(req: &'a FeatureRequest<CompletionParams>) -> Vec<Item<'a>> {
- let mut items = Vec::new();
- complete_bibtex_commands(req, &mut items).await;
- complete_bibtex_entry_types(req, &mut items).await;
- complete_bibtex_fields(req, &mut items).await;
- complete_latex_arguments(req, &mut items).await;
- complete_latex_begin_command(req, &mut items).await;
- complete_latex_colors(req, &mut items).await;
- complete_latex_color_models(req, &mut items).await;
- complete_latex_glossary_entries(req, &mut items).await;
- complete_latex_citations(req, &mut items).await;
- complete_latex_classes(req, &mut items).await;
- complete_latex_packages(req, &mut items).await;
- complete_latex_includes(req, &mut items).await;
- complete_latex_labels(req, &mut items).await;
- complete_latex_pgf_libraries(req, &mut items).await;
- complete_latex_tikz_libraries(req, &mut items).await;
- complete_latex_component_environments(req, &mut items).await;
- complete_latex_theorem_environments(req, &mut items).await;
- complete_latex_user_environments(req, &mut items).await;
- complete_latex_component_commands(req, &mut items).await;
- complete_latex_user_commands(req, &mut items).await;
- items
-}
-
-fn dedup<'a>(items: Vec<Item<'a>>) -> Vec<Item<'a>> {
- let mut labels = HashSet::new();
- let mut insert = vec![false; items.len()];
- for (i, item) in items.iter().enumerate() {
- insert[i] = labels.insert(item.data.label());
- }
- items
- .into_iter()
- .enumerate()
- .filter(|(i, _)| insert[*i])
- .map(|(_, item)| item)
- .collect()
-}
-
-fn preselect(req: &FeatureRequest<CompletionParams>, items: &mut [Item]) {
- let pos = req.params.text_document_position.position;
- if let DocumentContent::Latex(table) = &req.current().content {
- for env in &table.environments {
- if let Some(name) = env.left.name(&table) {
- let right_args = table
- .extract_group(env.right.parent, syntax::latex::GroupKind::Group, 0)
- .unwrap();
- let right_args_range = table[right_args].range();
- let cond1 = right_args_range.contains_exclusive(pos);
- let cond2 = table
- .as_group(right_args)
- .and_then(|group| group.right.as_ref())
- .is_none()
- && right_args_range.contains(pos);
-
- if cond1 || cond2 {
- for symbol in items.iter_mut() {
- if symbol.data.label() == name.text() {
- symbol.preselect = true;
- break;
- }
- }
- }
- }
- }
- }
-}
-
-fn score(req: &FeatureRequest<CompletionParams>, items: &mut Vec<Item>) {
- let current_word = current_word(req);
- let pattern = current_word.as_deref().unwrap_or_default();
- let matcher = SkimMatcherV2::default();
- for item in items {
- item.score = match &item.data {
- ItemData::ComponentCommand { name, .. } => matcher.fuzzy_match(name, pattern),
- ItemData::ComponentEnvironment { name, .. } => matcher.fuzzy_match(name, pattern),
- ItemData::UserCommand { name } => matcher.fuzzy_match(name, pattern),
- ItemData::UserEnvironment { name } => matcher.fuzzy_match(name, pattern),
- ItemData::Label { text, .. } => matcher.fuzzy_match(&text, pattern),
- ItemData::Class { name } => matcher.fuzzy_match(&name, pattern),
- ItemData::Package { name } => matcher.fuzzy_match(&name, pattern),
- ItemData::PgfLibrary { name } => matcher.fuzzy_match(name, pattern),
- ItemData::TikzLibrary { name } => matcher.fuzzy_match(name, pattern),
- ItemData::File { name } => matcher.fuzzy_match(name, pattern),
- ItemData::Directory { name } => matcher.fuzzy_match(name, pattern),
- ItemData::Citation { text, .. } => matcher.fuzzy_match(&text, pattern),
- ItemData::Argument { name, .. } => matcher.fuzzy_match(&name, pattern),
- ItemData::BeginCommand => matcher.fuzzy_match("begin", pattern),
- ItemData::Color { name } => matcher.fuzzy_match(name, pattern),
- ItemData::ColorModel { name } => matcher.fuzzy_match(name, pattern),
- ItemData::GlossaryEntry { name } => matcher.fuzzy_match(name, pattern),
- ItemData::EntryType { ty } => matcher.fuzzy_match(&ty.name, pattern),
- ItemData::Field { field } => matcher.fuzzy_match(&field.name, pattern),
- };
- }
-}
-
-fn convert(req: &FeatureRequest<CompletionParams>, item: Item) -> CompletionItem {
- let mut new_item = match item.data {
- ItemData::ComponentCommand {
- name,
- image,
- glyph,
- file_names,
- } => {
- let detail = glyph.map_or_else(
- || component_detail(file_names),
- |glyph| format!("{}, {}", glyph, component_detail(file_names)),
- );
- let documentation = image.and_then(|img| image_documentation(&req, &name, img));
- let text_edit = TextEdit::new(item.range, name.into());
- CompletionItem {
- kind: Some(adjust_kind(req, Structure::Command.completion_kind())),
- data: Some(CompletionItemData::Command.into()),
- documentation,
- text_edit: Some(CompletionTextEdit::Edit(text_edit)),
- ..CompletionItem::new_simple(name.into(), detail)
- }
- }
- ItemData::ComponentEnvironment { name, file_names } => {
- let text_edit = TextEdit::new(item.range, name.into());
- CompletionItem {
- kind: Some(adjust_kind(req, Structure::Environment.completion_kind())),
- data: Some(CompletionItemData::Environment.into()),
- text_edit: Some(CompletionTextEdit::Edit(text_edit)),
- ..CompletionItem::new_simple(name.into(), component_detail(file_names))
- }
- }
- ItemData::UserCommand { name } => {
- let detail = "user-defined".into();
- let text_edit = TextEdit::new(item.range, name.into());
- CompletionItem {
- kind: Some(adjust_kind(req, Structure::Command.completion_kind())),
- data: Some(CompletionItemData::Command.into()),
- text_edit: Some(CompletionTextEdit::Edit(text_edit)),
- ..CompletionItem::new_simple(name.into(), detail)
- }
- }
- ItemData::UserEnvironment { name } => {
- let detail = "user-defined".into();
- let text_edit = TextEdit::new(item.range, name.into());
- CompletionItem {
- kind: Some(adjust_kind(req, Structure::Environment.completion_kind())),
- data: Some(CompletionItemData::Environment.into()),
- text_edit: Some(CompletionTextEdit::Edit(text_edit)),
- ..CompletionItem::new_simple(name.into(), detail)
- }
- }
- ItemData::Label {
- name,
- kind,
- header,
- footer,
- text,
- } => {
- let text_edit = TextEdit::new(item.range, name.into());
- CompletionItem {
- label: name.into(),
- kind: Some(adjust_kind(req, kind.completion_kind())),
- data: Some(CompletionItemData::Label.into()),
- text_edit: Some(CompletionTextEdit::Edit(text_edit)),
- filter_text: Some(text.clone()),
- sort_text: Some(text),
- detail: header,
- documentation: footer.map(Documentation::String),
- ..CompletionItem::default()
- }
- }
- ItemData::Class { name } => {
- let text_edit = TextEdit::new(item.range, name.as_ref().into());
- CompletionItem {
- label: name.into_owned(),
- kind: Some(adjust_kind(req, Structure::Class.completion_kind())),
- data: Some(CompletionItemData::Class.into()),
- text_edit: Some(CompletionTextEdit::Edit(text_edit)),
- ..CompletionItem::default()
- }
- }
- ItemData::Package { name } => {
- let text_edit = TextEdit::new(item.range, name.as_ref().into());
- CompletionItem {
- label: name.into_owned(),
- kind: Some(adjust_kind(req, Structure::Package.completion_kind())),
- data: Some(CompletionItemData::Package.into()),
- text_edit: Some(CompletionTextEdit::Edit(text_edit)),
- ..CompletionItem::default()
- }
- }
- ItemData::PgfLibrary { name } => {
- let text_edit = TextEdit::new(item.range, name.into());
- CompletionItem {
- label: name.into(),
- kind: Some(adjust_kind(req, Structure::PgfLibrary.completion_kind())),
- data: Some(CompletionItemData::PgfLibrary.into()),
- text_edit: Some(CompletionTextEdit::Edit(text_edit)),
- ..CompletionItem::default()
- }
- }
- ItemData::TikzLibrary { name } => {
- let text_edit = TextEdit::new(item.range, name.into());
- CompletionItem {
- label: name.into(),
- kind: Some(adjust_kind(req, Structure::TikzLibrary.completion_kind())),
- data: Some(CompletionItemData::TikzLibrary.into()),
- text_edit: Some(CompletionTextEdit::Edit(text_edit)),
- ..CompletionItem::default()
- }
- }
- ItemData::File { name } => {
- let text_edit = TextEdit::new(item.range, name.clone());
- CompletionItem {
- label: name,
- kind: Some(adjust_kind(req, Structure::File.completion_kind())),
- data: Some(CompletionItemData::File.into()),
- text_edit: Some(CompletionTextEdit::Edit(text_edit)),
- ..CompletionItem::default()
- }
- }
- ItemData::Directory { name } => {
- let text_edit = TextEdit::new(item.range, name.clone());
- CompletionItem {
- label: name,
- kind: Some(adjust_kind(req, Structure::Folder.completion_kind())),
- data: Some(CompletionItemData::Folder.into()),
- text_edit: Some(CompletionTextEdit::Edit(text_edit)),
- ..CompletionItem::default()
- }
- }
- ItemData::Citation { uri, key, text, ty } => {
- let text_edit = TextEdit::new(item.range, key.into());
- CompletionItem {
- label: key.into(),
- kind: Some(adjust_kind(req, ty.completion_kind())),
- filter_text: Some(text.clone()),
- sort_text: Some(text),
- data: Some(
- CompletionItemData::Citation {
- uri: uri.clone(),
- key: key.into(),
- }
- .into(),
- ),
- text_edit: Some(CompletionTextEdit::Edit(text_edit)),
- ..CompletionItem::default()
- }
- }
- ItemData::Argument { name, image } => {
- let text_edit = TextEdit::new(item.range, name.into());
- CompletionItem {
- label: name.into(),
- kind: Some(adjust_kind(req, Structure::Argument.completion_kind())),
- data: Some(CompletionItemData::Argument.into()),
- text_edit: Some(CompletionTextEdit::Edit(text_edit)),
- documentation: image.and_then(|image| image_documentation(&req, &name, image)),
- ..CompletionItem::default()
- }
- }
- ItemData::BeginCommand => CompletionItem {
- kind: Some(adjust_kind(req, Structure::Snippet.completion_kind())),
- data: Some(CompletionItemData::CommandSnippet.into()),
- insert_text: Some("begin{$1}\n\t$0\n\\end{$1}".into()),
- insert_text_format: Some(InsertTextFormat::Snippet),
- ..CompletionItem::new_simple("begin".into(), component_detail(&[]))
- },
- ItemData::Color { name } => {
- let text_edit = TextEdit::new(item.range, name.into());
- CompletionItem {
- label: name.into(),
- kind: Some(adjust_kind(req, Structure::Color.completion_kind())),
- data: Some(CompletionItemData::Color.into()),
- text_edit: Some(CompletionTextEdit::Edit(text_edit)),
- ..CompletionItem::default()
- }
- }
- ItemData::ColorModel { name } => {
- let text_edit = TextEdit::new(item.range, name.into());
- CompletionItem {
- label: name.into(),
- kind: Some(adjust_kind(req, Structure::ColorModel.completion_kind())),
- data: Some(CompletionItemData::ColorModel.into()),
- text_edit: Some(CompletionTextEdit::Edit(text_edit)),
- ..CompletionItem::default()
- }
- }
- ItemData::GlossaryEntry { name } => {
- let text_edit = TextEdit::new(item.range, name.into());
- CompletionItem {
- label: name.into(),
- kind: Some(adjust_kind(req, Structure::GlossaryEntry.completion_kind())),
- data: Some(CompletionItemData::GlossaryEntry.into()),
- text_edit: Some(CompletionTextEdit::Edit(text_edit)),
- ..CompletionItem::default()
- }
- }
- ItemData::EntryType { ty } => {
- let text_edit = TextEdit::new(item.range, (&ty.name).into());
- let kind = Structure::Entry(ty.category).completion_kind();
- CompletionItem {
- label: (&ty.name).into(),
- kind: Some(adjust_kind(req, kind)),
- data: Some(CompletionItemData::EntryType.into()),
- text_edit: Some(CompletionTextEdit::Edit(text_edit)),
- documentation: ty.documentation.as_ref().map(|doc| {
- Documentation::MarkupContent(MarkupContent {
- kind: MarkupKind::Markdown,
- value: doc.into(),
- })
- }),
- ..CompletionItem::default()
- }
- }
- ItemData::Field { field } => {
- let text_edit = TextEdit::new(item.range, (&field.name).into());
- CompletionItem {
- label: (&field.name).into(),
- kind: Some(adjust_kind(req, Structure::Field.completion_kind())),
- data: Some(CompletionItemData::FieldName.into()),
- text_edit: Some(CompletionTextEdit::Edit(text_edit)),
- documentation: Some(Documentation::MarkupContent(MarkupContent {
- kind: MarkupKind::Markdown,
- value: (&field.documentation).into(),
- })),
- ..CompletionItem::default()
- }
- }
- };
- new_item.preselect = Some(item.preselect);
- new_item
-}
-
-fn append_sort_text(mut item: CompletionItem, index: usize) -> CompletionItem {
- let sort_prefix = format!("{:0>2}", index);
- match &item.sort_text {
- Some(sort_text) => {
- item.sort_text = Some(format!("{} {}", sort_prefix, sort_text));
- }
- None => {
- item.sort_text = Some(sort_prefix);
- }
- };
- item
-}
diff --git a/support/texlab/src/completion/util.rs b/support/texlab/src/completion/util.rs
deleted file mode 100644
index ec3c8eb406..0000000000
--- a/support/texlab/src/completion/util.rs
+++ /dev/null
@@ -1,121 +0,0 @@
-use crate::{
- feature::FeatureRequest,
- protocol::{
- CompletionItemKind, CompletionParams, Documentation, MarkupContent, MarkupKind, Position,
- RangeExt,
- },
- syntax::{bibtex, latex, SyntaxNode},
- workspace::DocumentContent,
-};
-use std::borrow::Cow;
-
-pub fn current_word(req: &FeatureRequest<CompletionParams>) -> Option<Cow<str>> {
- let pos = req.params.text_document_position.position;
- match &req.current().content {
- DocumentContent::Latex(table) => {
- if let Some(node) = table.find_command_by_short_name_range(pos) {
- return Some(command_word(table.as_command(node).unwrap()));
- }
-
- match &table[table.find(pos).into_iter().last()?] {
- latex::Node::Root(_) | latex::Node::Group(_) => Some("".into()),
- latex::Node::Command(cmd) => Some(command_word(cmd)),
- latex::Node::Text(text) => text
- .words
- .iter()
- .find(|word| word.range().contains(pos))
- .map(|word| word.text().split('/').last().unwrap().to_owned().into()),
- latex::Node::Comma(_) => Some(",".into()),
- latex::Node::Math(math) => Some(math.token.text().to_owned().into()),
- }
- }
- DocumentContent::Bibtex(tree) => {
- fn type_query(ty: &bibtex::Token, pos: Position) -> Option<Cow<str>> {
- if ty.range().contains(pos) {
- Some((&ty.text()[1..]).into())
- } else {
- Some("".into())
- }
- }
-
- match &tree.graph[tree.find(pos).pop()?] {
- bibtex::Node::Root(_) => Some("".into()),
- bibtex::Node::Preamble(preamble) => type_query(&preamble.ty, pos),
- bibtex::Node::String(string) => type_query(&string.ty, pos),
- bibtex::Node::Entry(entry) => type_query(&entry.ty, pos),
- bibtex::Node::Comment(comment) => Some(comment.token.text().into()),
- bibtex::Node::Field(field) => {
- if field.name.range().contains(pos) {
- Some(field.name.text().into())
- } else {
- Some("".into())
- }
- }
- bibtex::Node::Word(word) => Some(word.token.text().into()),
- bibtex::Node::Command(cmd) => Some((&cmd.token.text()[1..]).into()),
- bibtex::Node::QuotedContent(_)
- | bibtex::Node::BracedContent(_)
- | bibtex::Node::Concat(_) => Some("".into()),
- }
- }
- }
-}
-
-fn command_word(cmd: &latex::Command) -> Cow<str> {
- cmd.name.text()[1..].into()
-}
-
-pub fn component_detail(file_names: &[String]) -> String {
- if file_names.is_empty() {
- "built-in".to_owned()
- } else {
- file_names.join(", ")
- }
-}
-
-pub fn image_documentation(
- req: &FeatureRequest<CompletionParams>,
- name: &str,
- image: &str,
-) -> Option<Documentation> {
- if supports_images(req) {
- Some(Documentation::MarkupContent(MarkupContent {
- kind: MarkupKind::Markdown,
- value: format!(
- "![{}](data:image/png;base64,{}|width=48,height=48)",
- name, image
- ),
- }))
- } else {
- None
- }
-}
-
-fn supports_images(req: &FeatureRequest<CompletionParams>) -> bool {
- req.client_capabilities
- .text_document
- .as_ref()
- .and_then(|cap| cap.completion.as_ref())
- .and_then(|cap| cap.completion_item.as_ref())
- .and_then(|cap| cap.documentation_format.as_ref())
- .map_or(true, |formats| formats.contains(&MarkupKind::Markdown))
-}
-
-pub fn adjust_kind(
- req: &FeatureRequest<CompletionParams>,
- kind: CompletionItemKind,
-) -> CompletionItemKind {
- if let Some(value_set) = req
- .client_capabilities
- .text_document
- .as_ref()
- .and_then(|cap| cap.completion.as_ref())
- .and_then(|cap| cap.completion_item_kind.as_ref())
- .and_then(|cap| cap.value_set.as_ref())
- {
- if value_set.contains(&kind) {
- return kind;
- }
- }
- CompletionItemKind::Text
-}
diff --git a/support/texlab/src/components.rs b/support/texlab/src/component_db.rs
index 76890c8b15..a0c728a7ae 100644
--- a/support/texlab/src/components.rs
+++ b/support/texlab/src/component_db.rs
@@ -1,15 +1,19 @@
-use crate::protocol::{MarkupContent, MarkupKind};
+use itertools::Itertools;
+use lsp_types::{MarkupContent, MarkupKind};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
+use smol_str::SmolStr;
+
+use crate::WorkspaceSubset;
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
-pub struct Database {
+pub struct ComponentDatabase {
pub components: Vec<Component>,
- pub metadata: Vec<Metadata>,
+ pub metadata: Vec<ComponentMetadata>,
}
-impl Database {
+impl ComponentDatabase {
pub fn find(&self, name: &str) -> Option<&Component> {
self.components.iter().find(|component| {
component
@@ -19,6 +23,44 @@ impl Database {
})
}
+ pub fn find_no_ext(&self, name: &str) -> Option<&Component> {
+ self.components.iter().find(|component| {
+ component
+ .file_names
+ .iter()
+ .any(|file_name| &file_name[0..file_name.len() - 4] == name)
+ })
+ }
+
+ pub fn linked_components<'a>(&self, subset: &WorkspaceSubset) -> Vec<&Component> {
+ let mut start_components = vec![self.kernel()];
+ for document in &subset.documents {
+ if let Some(data) = document.data.as_latex() {
+ data.extras
+ .explicit_links
+ .iter()
+ .filter_map(|link| link.as_component_name())
+ .filter_map(|name| self.find(&name))
+ .for_each(|component| start_components.push(component));
+ }
+ }
+
+ let mut all_components = Vec::new();
+ for component in start_components {
+ all_components.push(component);
+ component
+ .references
+ .iter()
+ .flat_map(|file| self.find(&file))
+ .for_each(|component| all_components.push(component))
+ }
+
+ all_components
+ .into_iter()
+ .unique_by(|component| &component.file_names)
+ .collect()
+ }
+
pub fn contains(&self, short_name: &str) -> bool {
let sty = format!("{}.sty", short_name);
let cls = format!("{}.cls", short_name);
@@ -55,35 +97,35 @@ impl Database {
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Component {
- pub file_names: Vec<String>,
- pub references: Vec<String>,
- pub commands: Vec<Command>,
- pub environments: Vec<String>,
+ pub file_names: Vec<SmolStr>,
+ pub references: Vec<SmolStr>,
+ pub commands: Vec<ComponentCommand>,
+ pub environments: Vec<SmolStr>,
}
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
-pub struct Command {
- pub name: String,
+pub struct ComponentCommand {
+ pub name: SmolStr,
pub image: Option<String>,
- pub glyph: Option<String>,
- pub parameters: Vec<Parameter>,
+ pub glyph: Option<SmolStr>,
+ pub parameters: Vec<ComponentParameter>,
}
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
-pub struct Parameter(pub Vec<Argument>);
+pub struct ComponentParameter(pub Vec<ComponentArgument>);
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
-pub struct Argument {
- pub name: String,
+pub struct ComponentArgument {
+ pub name: SmolStr,
pub image: Option<String>,
}
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
-pub struct Metadata {
+pub struct ComponentMetadata {
pub name: String,
pub caption: Option<String>,
pub description: Option<String>,
@@ -91,4 +133,5 @@ pub struct Metadata {
const JSON: &str = include_str!("../data/components.json");
-pub static COMPONENT_DATABASE: Lazy<Database> = Lazy::new(|| serde_json::from_str(JSON).unwrap());
+pub static COMPONENT_DATABASE: Lazy<ComponentDatabase> =
+ Lazy::new(|| serde_json::from_str(JSON).unwrap());
diff --git a/support/texlab/src/config.rs b/support/texlab/src/config.rs
index 84de2268fa..5690bee091 100644
--- a/support/texlab/src/config.rs
+++ b/support/texlab/src/config.rs
@@ -1,103 +1,90 @@
-use crate::protocol::*;
-use futures::lock::Mutex;
-use log::{error, warn};
-use serde::de::DeserializeOwned;
-use std::sync::Arc;
+use std::sync::{Mutex, RwLock};
-#[derive(Debug)]
-pub struct ConfigManager<C> {
- client: Arc<C>,
- client_capabilities: Arc<ClientCapabilities>,
- options: Mutex<Options>,
-}
+use crossbeam_channel::Sender;
+use log::{error, warn};
+use lsp_server::Message;
+use lsp_types::{
+ notification::{DidChangeConfiguration, Notification},
+ request::{RegisterCapability, WorkspaceConfiguration},
+ ClientCapabilities, ConfigurationItem, ConfigurationParams, Registration, RegistrationParams,
+};
-impl<C: LspClient + Send + Sync + 'static> ConfigManager<C> {
- pub fn new(client: Arc<C>, client_capabilities: Arc<ClientCapabilities>) -> Self {
- Self {
- client,
- client_capabilities,
- options: Mutex::default(),
- }
- }
+use crate::{client::send_request, req_queue::ReqQueue, ClientCapabilitiesExt, Options};
- pub async fn get(&self) -> Options {
- self.options.lock().await.clone()
- }
+pub fn register_config_capability(
+ req_queue: &Mutex<ReqQueue>,
+ sender: &Sender<Message>,
+ client_capabilities: &Mutex<ClientCapabilities>,
+) {
+ let client_capabilities = client_capabilities.lock().unwrap();
+ if !client_capabilities.has_pull_configuration_support()
+ && client_capabilities.has_push_configuration_support()
+ {
+ drop(client_capabilities);
+ let reg = Registration {
+ id: "pull-config".to_string(),
+ method: DidChangeConfiguration::METHOD.to_string(),
+ register_options: None,
+ };
- pub async fn register(&self) {
- if !self.client_capabilities.has_pull_configuration_support()
- && self.client_capabilities.has_push_configuration_support()
- {
- let registration = Registration {
- id: "pull-config".into(),
- method: "workspace/didChangeConfiguration".into(),
- register_options: None,
- };
- let params = RegistrationParams {
- registrations: vec![registration],
- };
+ let params = RegistrationParams {
+ registrations: vec![reg],
+ };
- if let Err(why) = self.client.register_capability(params).await {
- error!(
- "Failed to register \"workspace/didChangeConfiguration\": {}",
- why.message
- );
- }
+ if let Err(why) = send_request::<RegisterCapability>(&req_queue, &sender, params) {
+ error!(
+ "Failed to register \"{}\" notification: {}",
+ DidChangeConfiguration::METHOD,
+ why
+ );
}
}
+}
- pub async fn push(&self, options: serde_json::Value) {
- match serde_json::from_value(options) {
- Ok(options) => {
- *self.options.lock().await = options;
- }
- Err(why) => {
- error!("Invalid configuration: {}", why);
- }
- }
+pub fn pull_config(
+ req_queue: &Mutex<ReqQueue>,
+ sender: &Sender<Message>,
+ options: &RwLock<Options>,
+ client_capabilities: &ClientCapabilities,
+) {
+ if !client_capabilities.has_pull_configuration_support() {
+ return;
}
- pub async fn pull(&self) -> bool {
- if self.client_capabilities.has_pull_configuration_support() {
- let latex = self.pull_section("latex").await;
- let bibtex = self.pull_section("bibtex").await;
+ let params = ConfigurationParams {
+ items: vec![ConfigurationItem {
+ section: Some("texlab".to_string()),
+ scope_uri: None,
+ }],
+ };
- let new_options = Options {
- latex: Some(latex),
- bibtex: Some(bibtex),
+ match send_request::<WorkspaceConfiguration>(req_queue, sender, params) {
+ Ok(mut json) => {
+ let value = json.pop().expect("invalid configuration request");
+ let new_options = match serde_json::from_value(value) {
+ Ok(new_options) => new_options,
+ Err(why) => {
+ warn!("Invalid configuration section \"texlab\": {}", why);
+ Options::default()
+ }
};
- let mut old_options = self.options.lock().await;
- let has_changed = *old_options != new_options;
- *old_options = new_options;
- has_changed
- } else {
- false
- }
- }
- async fn pull_section<T: DeserializeOwned + Default>(&self, section: &str) -> T {
- let params = ConfigurationParams {
- items: vec![ConfigurationItem {
- section: Some(section.into()),
- scope_uri: None,
- }],
- };
+ let mut options = options.write().unwrap();
+ *options = new_options;
+ }
+ Err(why) => {
+ error!("Retrieving configuration failed: {}", why);
+ }
+ };
+}
- match self.client.configuration(params).await {
- Ok(json) => match serde_json::from_value::<Vec<T>>(json) {
- Ok(config) => config.into_iter().next().unwrap(),
- Err(_) => {
- warn!("Invalid configuration: {}", section);
- T::default()
- }
- },
- Err(why) => {
- error!(
- "Retrieving configuration for {} failed: {}",
- section, why.message
- );
- T::default()
- }
+pub fn push_config(options: &RwLock<Options>, config: serde_json::Value) {
+ match serde_json::from_value(config) {
+ Ok(new_options) => {
+ *options.write().unwrap() = new_options;
}
- }
+ Err(why) => {
+ error!("Invalid configuration: {}", why);
+ }
+ };
}
diff --git a/support/texlab/src/context.rs b/support/texlab/src/context.rs
new file mode 100644
index 0000000000..a9d6f530a6
--- /dev/null
+++ b/support/texlab/src/context.rs
@@ -0,0 +1,34 @@
+use std::{
+ path::PathBuf,
+ sync::{Mutex, RwLock},
+};
+
+use lsp_types::{ClientCapabilities, ClientInfo};
+
+use crate::{
+ distro::{DistributionKind, Resolver},
+ Options,
+};
+
+#[derive(Debug)]
+pub struct ServerContext {
+ pub current_directory: PathBuf,
+ pub distro_kind: Mutex<DistributionKind>,
+ pub resolver: Mutex<Resolver>,
+ pub client_capabilities: Mutex<ClientCapabilities>,
+ pub client_info: Mutex<Option<ClientInfo>>,
+ pub options: RwLock<Options>,
+}
+
+impl ServerContext {
+ pub fn new(current_dir: PathBuf) -> Self {
+ Self {
+ current_directory: current_dir,
+ distro_kind: Mutex::new(DistributionKind::Unknown),
+ resolver: Mutex::new(Resolver::default()),
+ client_capabilities: Mutex::default(),
+ client_info: Mutex::default(),
+ options: RwLock::default(),
+ }
+ }
+}
diff --git a/support/texlab/src/definition/bibtex_string.rs b/support/texlab/src/definition/bibtex_string.rs
deleted file mode 100644
index a1fe5132c0..0000000000
--- a/support/texlab/src/definition/bibtex_string.rs
+++ /dev/null
@@ -1,171 +0,0 @@
-use crate::{
- feature::{FeatureProvider, FeatureRequest},
- protocol::{LocationLink, Position, TextDocumentPositionParams, Uri},
- syntax::{bibtex, SyntaxNode},
- workspace::DocumentContent,
-};
-use async_trait::async_trait;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct BibtexStringDefinitionProvider;
-
-#[async_trait]
-impl FeatureProvider for BibtexStringDefinitionProvider {
- type Params = TextDocumentPositionParams;
- type Output = Vec<LocationLink>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- if let DocumentContent::Bibtex(tree) = &req.current().content {
- if let Some(reference) = Self::find_reference(tree, req.params.position) {
- return Self::find_definitions(&req.current().uri, tree, reference);
- }
- }
- Vec::new()
- }
-}
-
-impl BibtexStringDefinitionProvider {
- fn find_reference(tree: &bibtex::Tree, pos: Position) -> Option<&bibtex::Token> {
- let mut nodes = tree.find(pos);
- nodes.reverse();
- match (
- &tree.graph[nodes[0]],
- nodes.get(1).map(|node| &tree.graph[*node]),
- ) {
- (bibtex::Node::Word(word), Some(bibtex::Node::Field(_)))
- | (bibtex::Node::Word(word), Some(bibtex::Node::Concat(_))) => Some(&word.token),
- _ => None,
- }
- }
-
- fn find_definitions(
- uri: &Uri,
- tree: &bibtex::Tree,
- reference: &bibtex::Token,
- ) -> Vec<LocationLink> {
- let mut links = Vec::new();
- for node in tree.children(tree.root) {
- if let Some(string) = tree.as_string(node) {
- if let Some(name) = &string.name {
- if name.text() == reference.text() {
- links.push(LocationLink {
- origin_selection_range: Some(reference.range()),
- target_uri: uri.clone().into(),
- target_range: string.range(),
- target_selection_range: name.range(),
- });
- }
- }
- }
- }
- links
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- feature::FeatureTester,
- protocol::{Range, RangeExt},
- };
- use indoc::indoc;
-
- #[tokio::test]
- async fn empty_latex_document() {
- let actual_links = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_position(BibtexStringDefinitionProvider)
- .await;
-
- assert!(actual_links.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let actual_links = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_position(BibtexStringDefinitionProvider)
- .await;
-
- assert!(actual_links.is_empty());
- }
-
- #[tokio::test]
- async fn simple() {
- let actual_links = FeatureTester::new()
- .file(
- "main.bib",
- indoc!(
- r#"
- @string{foo = {bar}}
- @article{bar, author = foo}
- "#
- ),
- )
- .main("main.bib")
- .position(1, 24)
- .test_position(BibtexStringDefinitionProvider)
- .await;
-
- let expected_links = vec![LocationLink {
- origin_selection_range: Some(Range::new_simple(1, 23, 1, 26)),
- target_uri: FeatureTester::uri("main.bib").into(),
- target_range: Range::new_simple(0, 0, 0, 20),
- target_selection_range: Range::new_simple(0, 8, 0, 11),
- }];
-
- assert_eq!(actual_links, expected_links);
- }
-
- #[tokio::test]
- async fn concat() {
- let actual_links = FeatureTester::new()
- .file(
- "main.bib",
- indoc!(
- r#"
- @string{foo = {bar}}
- @article{bar, author = foo # "bar"}
- "#
- ),
- )
- .main("main.bib")
- .position(1, 24)
- .test_position(BibtexStringDefinitionProvider)
- .await;
-
- let expected_links = vec![LocationLink {
- origin_selection_range: Some(Range::new_simple(1, 23, 1, 26)),
- target_uri: FeatureTester::uri("main.bib").into(),
- target_range: Range::new_simple(0, 0, 0, 20),
- target_selection_range: Range::new_simple(0, 8, 0, 11),
- }];
-
- assert_eq!(actual_links, expected_links);
- }
-
- #[tokio::test]
- async fn field() {
- let actual_links = FeatureTester::new()
- .file(
- "main.bib",
- indoc!(
- r#"
- @string{foo = {bar}}
- @article{bar, author = foo}
- "#
- ),
- )
- .main("main.bib")
- .position(1, 18)
- .test_position(BibtexStringDefinitionProvider)
- .await;
-
- assert!(actual_links.is_empty());
- }
-}
diff --git a/support/texlab/src/definition/latex_citation.rs b/support/texlab/src/definition/latex_citation.rs
deleted file mode 100644
index aa93e25fc0..0000000000
--- a/support/texlab/src/definition/latex_citation.rs
+++ /dev/null
@@ -1,118 +0,0 @@
-use crate::{
- feature::{FeatureProvider, FeatureRequest},
- protocol::{LocationLink, RangeExt, TextDocumentPositionParams},
- syntax::{latex, SyntaxNode},
- workspace::{Document, DocumentContent},
-};
-use async_trait::async_trait;
-
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub struct LatexCitationDefinitionProvider;
-
-#[async_trait]
-impl FeatureProvider for LatexCitationDefinitionProvider {
- type Params = TextDocumentPositionParams;
- type Output = Vec<LocationLink>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let mut links = Vec::new();
- if let Some(reference) = Self::find_reference(req) {
- for doc in req.related() {
- Self::find_definitions(&doc, &reference, &mut links);
- }
- }
- links
- }
-}
-
-impl LatexCitationDefinitionProvider {
- fn find_reference(req: &FeatureRequest<TextDocumentPositionParams>) -> Option<&latex::Token> {
- req.current().content.as_latex().and_then(|table| {
- table
- .citations
- .iter()
- .flat_map(|citation| citation.keys(&table))
- .find(|key| key.range().contains(req.params.position))
- })
- }
-
- fn find_definitions(doc: &Document, reference: &latex::Token, links: &mut Vec<LocationLink>) {
- if let DocumentContent::Bibtex(tree) = &doc.content {
- for entry in tree
- .children(tree.root)
- .filter_map(|node| tree.as_entry(node))
- {
- if let Some(key) = &entry.key {
- if key.text() == reference.text() {
- links.push(LocationLink {
- origin_selection_range: Some(reference.range()),
- target_uri: doc.uri.clone().into(),
- target_range: entry.range(),
- target_selection_range: key.range(),
- });
- }
- }
- }
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{feature::FeatureTester, protocol::Range};
- use indoc::indoc;
-
- #[tokio::test]
- async fn empty_latex_document() {
- let actual_links = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_position(LatexCitationDefinitionProvider)
- .await;
-
- assert!(actual_links.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let actual_links = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_position(LatexCitationDefinitionProvider)
- .await;
-
- assert!(actual_links.is_empty());
- }
-
- #[tokio::test]
- async fn has_definition() {
- let actual_links = FeatureTester::new()
- .file(
- "foo.tex",
- indoc!(
- r#"
- \addbibresource{baz.bib}
- \cite{foo}
- "#
- ),
- )
- .file("bar.bib", r#"@article{foo, bar = {baz}}"#)
- .file("baz.bib", r#"@article{foo, bar = {baz}}"#)
- .main("foo.tex")
- .position(1, 6)
- .test_position(LatexCitationDefinitionProvider)
- .await;
-
- let exepcted_links = vec![LocationLink {
- origin_selection_range: Some(Range::new_simple(1, 6, 1, 9)),
- target_uri: FeatureTester::uri("baz.bib").into(),
- target_range: Range::new_simple(0, 0, 0, 26),
- target_selection_range: Range::new_simple(0, 9, 0, 12),
- }];
-
- assert_eq!(actual_links, exepcted_links);
- }
-}
diff --git a/support/texlab/src/definition/latex_cmd.rs b/support/texlab/src/definition/latex_cmd.rs
deleted file mode 100644
index 35fc42be4f..0000000000
--- a/support/texlab/src/definition/latex_cmd.rs
+++ /dev/null
@@ -1,154 +0,0 @@
-use crate::{
- feature::{FeatureProvider, FeatureRequest},
- protocol::{LocationLink, TextDocumentPositionParams},
- syntax::SyntaxNode,
- workspace::DocumentContent,
-};
-use async_trait::async_trait;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct LatexCommandDefinitionProvider;
-
-#[async_trait]
-impl FeatureProvider for LatexCommandDefinitionProvider {
- type Params = TextDocumentPositionParams;
- type Output = Vec<LocationLink>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let mut links = Vec::new();
- if let DocumentContent::Latex(table) = &req.current().content {
- if let Some(cmd) = table
- .find(req.params.position)
- .last()
- .and_then(|node| table.as_command(*node))
- {
- for doc in req.related() {
- if let DocumentContent::Latex(table) = &doc.content {
- table
- .command_definitions
- .iter()
- .filter(|def| def.definition_name(&table) == cmd.name.text())
- .map(|def| {
- let def_range = table[def.parent].range();
- LocationLink {
- origin_selection_range: Some(cmd.range()),
- target_uri: doc.uri.clone().into(),
- target_range: def_range,
- target_selection_range: def_range,
- }
- })
- .for_each(|link| links.push(link));
-
- table
- .math_operators
- .iter()
- .filter(|op| op.definition_name(&table) == cmd.name.text())
- .map(|op| {
- let def_range = table[op.parent].range();
- LocationLink {
- origin_selection_range: Some(cmd.range()),
- target_uri: doc.uri.clone().into(),
- target_range: def_range,
- target_selection_range: def_range,
- }
- })
- .for_each(|link| links.push(link));
- }
- }
- }
- }
- links
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- feature::FeatureTester,
- protocol::{Range, RangeExt},
- };
- use indoc::indoc;
-
- #[tokio::test]
- async fn empty_latex_document() {
- let actual_links = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_position(LatexCommandDefinitionProvider)
- .await;
-
- assert!(actual_links.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let actual_links = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_position(LatexCommandDefinitionProvider)
- .await;
-
- assert!(actual_links.is_empty());
- }
-
- #[tokio::test]
- async fn command_definition() {
- let actual_links = FeatureTester::new()
- .file(
- "foo.tex",
- indoc!(
- r#"
- \include{bar}
- \foo
- "#
- ),
- )
- .file("bar.tex", r#"\newcommand{\foo}{bar}"#)
- .file("baz.tex", r#"\newcommand{\foo}{baz}"#)
- .main("foo.tex")
- .position(1, 3)
- .test_position(LatexCommandDefinitionProvider)
- .await;
-
- let expected_links = vec![LocationLink {
- origin_selection_range: Some(Range::new_simple(1, 0, 1, 4)),
- target_uri: FeatureTester::uri("bar.tex").into(),
- target_range: Range::new_simple(0, 0, 0, 22),
- target_selection_range: Range::new_simple(0, 0, 0, 22),
- }];
-
- assert_eq!(actual_links, expected_links);
- }
-
- #[tokio::test]
- async fn math_operator() {
- let actual_links = FeatureTester::new()
- .file(
- "foo.tex",
- indoc!(
- r#"
- \include{bar}
- \foo
- "#
- ),
- )
- .file("bar.tex", r#"\DeclareMathOperator{\foo}{bar}"#)
- .file("baz.tex", r#"\DeclareMathOperator{\foo}{baz}"#)
- .main("foo.tex")
- .position(1, 3)
- .test_position(LatexCommandDefinitionProvider)
- .await;
-
- let expected_links = vec![LocationLink {
- origin_selection_range: Some(Range::new_simple(1, 0, 1, 4)),
- target_uri: FeatureTester::uri("bar.tex").into(),
- target_range: Range::new_simple(0, 0, 0, 31),
- target_selection_range: Range::new_simple(0, 0, 0, 31),
- }];
-
- assert_eq!(actual_links, expected_links);
- }
-}
diff --git a/support/texlab/src/definition/latex_label.rs b/support/texlab/src/definition/latex_label.rs
deleted file mode 100644
index 5318982ccd..0000000000
--- a/support/texlab/src/definition/latex_label.rs
+++ /dev/null
@@ -1,155 +0,0 @@
-use crate::{
- feature::{DocumentView, FeatureProvider, FeatureRequest},
- outline::{Outline, OutlineContext, OutlineContextItem},
- protocol::{LocationLink, Options, RangeExt, TextDocumentPositionParams},
- symbol::build_section_tree,
- syntax::{latex, LatexLabelKind, SyntaxNode},
- workspace::DocumentContent,
-};
-use async_trait::async_trait;
-use std::{path::Path, sync::Arc};
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct LatexLabelDefinitionProvider;
-
-#[async_trait]
-impl FeatureProvider for LatexLabelDefinitionProvider {
- type Params = TextDocumentPositionParams;
- type Output = Vec<LocationLink>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let mut links = Vec::new();
- if let Some(reference) = Self::find_reference(req) {
- for doc in req.related() {
- let snapshot = Arc::clone(&req.view.snapshot);
- let view = DocumentView::analyze(
- snapshot,
- Arc::clone(&doc),
- &req.options,
- &req.current_dir,
- );
- Self::find_definitions(
- &view,
- &req.options,
- &req.current_dir,
- &reference,
- &mut links,
- );
- }
- }
- links
- }
-}
-
-impl LatexLabelDefinitionProvider {
- fn find_reference(req: &FeatureRequest<TextDocumentPositionParams>) -> Option<&latex::Token> {
- if let DocumentContent::Latex(table) = &req.current().content {
- table
- .labels
- .iter()
- .flat_map(|label| label.names(&table))
- .find(|label| label.range().contains(req.params.position))
- } else {
- None
- }
- }
-
- fn find_definitions(
- view: &DocumentView,
- options: &Options,
- current_dir: &Path,
- reference: &latex::Token,
- links: &mut Vec<LocationLink>,
- ) {
- if let DocumentContent::Latex(table) = &view.current.content {
- let outline = Outline::analyze(view, options, current_dir);
- let section_tree = build_section_tree(view, table, options, current_dir);
- for label in &table.labels {
- if label.kind == LatexLabelKind::Definition {
- let context = OutlineContext::parse(view, &outline, *label);
- for name in label.names(&table) {
- if name.text() == reference.text() {
- let target_range = if let Some(OutlineContextItem::Section { .. }) =
- context.as_ref().map(|ctx| &ctx.item)
- {
- section_tree
- .find(reference.text())
- .map(|sec| sec.full_range)
- } else {
- context.as_ref().map(|ctx| ctx.range)
- };
-
- links.push(LocationLink {
- origin_selection_range: Some(reference.range()),
- target_uri: view.current.uri.clone().into(),
- target_range: target_range
- .unwrap_or_else(|| table[label.parent].range()),
- target_selection_range: table[label.parent].range(),
- });
- }
- }
- }
- }
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{feature::FeatureTester, protocol::Range};
- use indoc::indoc;
-
- #[tokio::test]
- async fn empty_latex_document() {
- let actual_links = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_position(LatexLabelDefinitionProvider)
- .await;
-
- assert!(actual_links.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let actual_links = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_position(LatexLabelDefinitionProvider)
- .await;
-
- assert!(actual_links.is_empty());
- }
-
- #[tokio::test]
- async fn unknown_context() {
- let actual_links = FeatureTester::new()
- .file("foo.tex", r#"\label{foo}"#)
- .file(
- "bar.tex",
- indoc!(
- r#"
- \begin{a}\begin{b}\label{foo}\end{b}\end{a}
- \input{baz.tex}
- "#
- ),
- )
- .file("baz.tex", r#"\ref{foo}"#)
- .main("baz.tex")
- .position(0, 5)
- .test_position(LatexLabelDefinitionProvider)
- .await;
-
- let expected_links = vec![LocationLink {
- origin_selection_range: Some(Range::new_simple(0, 5, 0, 8)),
- target_uri: FeatureTester::uri("bar.tex").into(),
- target_range: Range::new_simple(0, 18, 0, 29),
- target_selection_range: Range::new_simple(0, 18, 0, 29),
- }];
-
- assert_eq!(actual_links, expected_links);
- }
-}
diff --git a/support/texlab/src/definition/mod.rs b/support/texlab/src/definition/mod.rs
deleted file mode 100644
index 08901454fe..0000000000
--- a/support/texlab/src/definition/mod.rs
+++ /dev/null
@@ -1,47 +0,0 @@
-mod bibtex_string;
-mod latex_citation;
-mod latex_cmd;
-mod latex_label;
-
-use self::{
- bibtex_string::BibtexStringDefinitionProvider, latex_citation::LatexCitationDefinitionProvider,
- latex_cmd::LatexCommandDefinitionProvider, latex_label::LatexLabelDefinitionProvider,
-};
-use crate::{
- feature::{ConcatProvider, FeatureProvider, FeatureRequest},
- protocol::{LocationLink, TextDocumentPositionParams},
-};
-use async_trait::async_trait;
-
-pub struct DefinitionProvider {
- provider: ConcatProvider<TextDocumentPositionParams, LocationLink>,
-}
-
-impl DefinitionProvider {
- pub fn new() -> Self {
- Self {
- provider: ConcatProvider::new(vec![
- Box::new(BibtexStringDefinitionProvider),
- Box::new(LatexCitationDefinitionProvider),
- Box::new(LatexCommandDefinitionProvider),
- Box::new(LatexLabelDefinitionProvider),
- ]),
- }
- }
-}
-
-impl Default for DefinitionProvider {
- fn default() -> Self {
- Self::new()
- }
-}
-
-#[async_trait]
-impl FeatureProvider for DefinitionProvider {
- type Params = TextDocumentPositionParams;
- type Output = Vec<LocationLink>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- self.provider.execute(req).await
- }
-}
diff --git a/support/texlab/src/diagnostics.rs b/support/texlab/src/diagnostics.rs
new file mode 100644
index 0000000000..a4adbafd7d
--- /dev/null
+++ b/support/texlab/src/diagnostics.rs
@@ -0,0 +1,55 @@
+mod bibtex;
+mod build_log;
+mod chktex;
+mod debouncer;
+mod latex;
+
+use std::sync::Arc;
+
+use lsp_types::Diagnostic;
+use multimap::MultiMap;
+use rustc_hash::FxHashMap;
+
+use crate::{Options, Uri, Workspace};
+
+pub use self::debouncer::{DiagnosticsDebouncer, DiagnosticsMessage};
+
+use self::{
+ bibtex::analyze_bibtex_static, build_log::analyze_build_log_static,
+ chktex::analyze_latex_chktex, latex::analyze_latex_static,
+};
+
+#[derive(Default)]
+pub struct DiagnosticsManager {
+ static_diagnostics: FxHashMap<Arc<Uri>, MultiMap<Arc<Uri>, Diagnostic>>,
+ chktex_diagnostics: MultiMap<Arc<Uri>, Diagnostic>,
+}
+
+impl DiagnosticsManager {
+ pub fn update_static(&mut self, workspace: &dyn Workspace, uri: Arc<Uri>) {
+ let mut diagnostics_by_uri = MultiMap::new();
+ analyze_build_log_static(workspace, &mut diagnostics_by_uri, &uri);
+ analyze_bibtex_static(workspace, &mut diagnostics_by_uri, &uri);
+ analyze_latex_static(workspace, &mut diagnostics_by_uri, &uri);
+ self.static_diagnostics.insert(uri, diagnostics_by_uri);
+ }
+
+ pub fn update_chktex(&mut self, workspace: &dyn Workspace, uri: Arc<Uri>, options: &Options) {
+ analyze_latex_chktex(workspace, &mut self.chktex_diagnostics, &uri, options);
+ }
+
+ pub fn publish(&self, uri: Arc<Uri>) -> Vec<Diagnostic> {
+ let mut all_diagnostics = Vec::new();
+ for diagnostics_by_uri in self.static_diagnostics.values() {
+ if let Some(diagnostics) = diagnostics_by_uri.get_vec(&uri) {
+ all_diagnostics.append(&mut diagnostics.clone());
+ }
+ }
+
+ if let Some(diagnostics) = self.chktex_diagnostics.get_vec(&uri) {
+ all_diagnostics.append(&mut diagnostics.clone());
+ }
+
+ all_diagnostics
+ }
+}
diff --git a/support/texlab/src/diagnostics/bibtex.rs b/support/texlab/src/diagnostics/bibtex.rs
index 80cca80319..efea141db0 100644
--- a/support/texlab/src/diagnostics/bibtex.rs
+++ b/support/texlab/src/diagnostics/bibtex.rs
@@ -1,341 +1,147 @@
-use crate::{
- protocol::{Diagnostic, DiagnosticSeverity, Position, Range},
- syntax::{bibtex, SyntaxNode},
- workspace::{Document, DocumentContent},
-};
-use petgraph::graph::NodeIndex;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum BibtexErrorCode {
- MissingBeginBrace,
- MissingEntryKey,
- MissingComma,
- MissingEndBrace,
- MissingAssign,
- MissingContent,
- MissingQuote,
-}
-
-impl BibtexErrorCode {
- pub fn message(self) -> &'static str {
- match self {
- BibtexErrorCode::MissingBeginBrace => "Expecting a curly bracket: \"{\"",
- BibtexErrorCode::MissingEntryKey => "Expecting an entry key",
- BibtexErrorCode::MissingComma => "Expecting a comma: \",\"",
- BibtexErrorCode::MissingEndBrace => "Expecting a curly bracket: \"}\"",
- BibtexErrorCode::MissingAssign => "Expecting an equals sign: \"=\"",
- BibtexErrorCode::MissingContent => "Expecting content",
- BibtexErrorCode::MissingQuote => "Expecting a quote: '\"'",
- }
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub struct BibtexError {
- code: BibtexErrorCode,
- position: Position,
-}
-
-impl BibtexError {
- pub fn new(code: BibtexErrorCode, position: Position) -> Self {
- Self { code, position }
- }
-
- pub fn analyze(tree: &bibtex::Tree) -> Vec<Self> {
- let mut errors = Vec::new();
- for entry_node in tree.children(tree.root) {
- if let Some(entry) = tree.as_entry(entry_node) {
- if entry.is_comment() {
- continue;
- }
-
- if entry.left.is_none() {
- errors.push(BibtexError::new(
- BibtexErrorCode::MissingBeginBrace,
- entry.ty.end(),
- ));
- continue;
- }
+use std::sync::Arc;
- if entry.key.is_none() {
- errors.push(BibtexError::new(
- BibtexErrorCode::MissingEntryKey,
- entry.left.as_ref().unwrap().end(),
- ));
- continue;
- }
+use cstree::TextRange;
+use lsp_types::{Diagnostic, DiagnosticSeverity};
+use multimap::MultiMap;
- if entry.comma.is_none() {
- errors.push(BibtexError::new(
- BibtexErrorCode::MissingComma,
- entry.key.as_ref().unwrap().end(),
- ));
- continue;
- }
-
- let field_count = tree.children(entry_node).count();
- for (i, field_node) in tree.children(entry_node).enumerate() {
- let field = tree.as_field(field_node).unwrap();
- if field.assign.is_none() {
- errors.push(BibtexError::new(
- BibtexErrorCode::MissingAssign,
- field.name.end(),
- ));
- continue;
- }
-
- let content = tree.children(field_node).next();
-
- if content.is_none() {
- errors.push(BibtexError::new(
- BibtexErrorCode::MissingContent,
- field.assign.as_ref().unwrap().end(),
- ));
- continue;
- }
-
- Self::analyze_content(&mut errors, tree, content.unwrap());
-
- if i != field_count - 1 && field.comma.is_none() {
- errors.push(BibtexError::new(
- BibtexErrorCode::MissingComma,
- tree.graph[content.unwrap()].end(),
- ));
- continue;
- }
- }
-
- if entry.right.is_none() {
- errors.push(BibtexError::new(
- BibtexErrorCode::MissingEndBrace,
- entry.end(),
- ));
- continue;
- }
- }
- }
- errors
- }
-
- fn analyze_content(
- mut errors: &mut Vec<BibtexError>,
- tree: &bibtex::Tree,
- content_node: NodeIndex,
- ) {
- match &tree.graph[content_node] {
- bibtex::Node::QuotedContent(content) => {
- for child in tree.children(content_node) {
- Self::analyze_content(&mut errors, tree, child);
- }
-
- if content.right.is_none() {
- errors.push(BibtexError::new(
- BibtexErrorCode::MissingQuote,
- content.end(),
- ));
- }
- }
- bibtex::Node::BracedContent(content) => {
- for child in tree.children(content_node) {
- Self::analyze_content(&mut errors, tree, child);
- }
+use crate::{
+ syntax::{
+ bibtex::{self, HasDelimiters, HasType},
+ CstNode,
+ },
+ Document, LineIndexExt, Uri, Workspace,
+};
- if content.right.is_none() {
- errors.push(BibtexError::new(
- BibtexErrorCode::MissingEndBrace,
- content.end(),
- ));
- }
- }
- bibtex::Node::Concat(concat) => {
- let mut children = tree.children(content_node);
- Self::analyze_content(&mut errors, tree, children.next().unwrap());
- match children.next() {
- Some(right) => {
- Self::analyze_content(&mut errors, tree, right);
- }
- None => {
- errors.push(BibtexError::new(
- BibtexErrorCode::MissingContent,
- concat.end(),
- ));
- }
- }
- }
- bibtex::Node::Root(_)
- | bibtex::Node::Comment(_)
- | bibtex::Node::Preamble(_)
- | bibtex::Node::String(_)
- | bibtex::Node::Entry(_)
- | bibtex::Node::Field(_)
- | bibtex::Node::Word(_)
- | bibtex::Node::Command(_) => {}
- }
- }
-}
+pub fn analyze_bibtex_static(
+ workspace: &dyn Workspace,
+ diagnostics_by_uri: &mut MultiMap<Arc<Uri>, Diagnostic>,
+ uri: &Uri,
+) -> Option<()> {
+ let document = workspace.get(uri)?;
+ let data = document.data.as_bibtex()?;
-impl Into<Diagnostic> for BibtexError {
- fn into(self) -> Diagnostic {
- Diagnostic {
- source: Some("bibtex".into()),
- range: Range::new(self.position, self.position),
- message: self.code.message().into(),
- severity: Some(DiagnosticSeverity::Error),
- code: None,
- related_information: None,
- tags: None,
- }
+ for node in data.root.descendants() {
+ analyze_entry(&document, diagnostics_by_uri, node)
+ .or_else(|| analyze_field(&document, diagnostics_by_uri, node));
}
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct BibtexDiagnosticsProvider;
-impl BibtexDiagnosticsProvider {
- pub fn get(self, doc: &Document) -> Vec<Diagnostic> {
- if let DocumentContent::Bibtex(tree) = &doc.content {
- BibtexError::analyze(&tree)
- .into_iter()
- .map(Into::into)
- .collect()
- } else {
- Vec::new()
- }
- }
+ Some(())
}
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn begin_brace() {
- let errors = BibtexError::analyze(&bibtex::open("@article"));
- assert_eq!(
- errors,
- vec![BibtexError::new(
- BibtexErrorCode::MissingBeginBrace,
- Position::new(0, 8),
- )]
- );
- }
-
- #[test]
- fn entry_key() {
- let errors = BibtexError::analyze(&bibtex::open("@article{"));
- assert_eq!(
- errors,
- vec![BibtexError::new(
- BibtexErrorCode::MissingEntryKey,
- Position::new(0, 9),
- )]
- );
- }
-
- #[test]
- fn entry_comma() {
- let errors = BibtexError::analyze(&bibtex::open("@article{foo"));
- assert_eq!(
- errors,
- vec![BibtexError::new(
- BibtexErrorCode::MissingComma,
- Position::new(0, 12),
- )]
- );
- }
-
- #[test]
- fn entry_end_brace() {
- let errors = BibtexError::analyze(&bibtex::open("@article{foo,"));
- assert_eq!(
- errors,
- vec![BibtexError::new(
- BibtexErrorCode::MissingEndBrace,
- Position::new(0, 13),
- )]
+fn analyze_entry(
+ document: &Document,
+ diagnostics_by_uri: &mut MultiMap<Arc<Uri>, Diagnostic>,
+ node: &bibtex::SyntaxNode,
+) -> Option<()> {
+ let entry = bibtex::Entry::cast(node)?;
+ if entry.left_delimiter().is_none() {
+ diagnostics_by_uri.insert(
+ Arc::clone(&document.uri),
+ Diagnostic {
+ range: document
+ .line_index
+ .line_col_lsp_range(entry.ty()?.text_range()),
+ severity: Some(DiagnosticSeverity::Error),
+ code: None,
+ code_description: None,
+ source: Some("texlab".to_string()),
+ message: "Expecting a curly bracket: \"{\"".to_string(),
+ related_information: None,
+ tags: None,
+ data: None,
+ },
);
- }
-
- #[test]
- fn field_equals() {
- let errors = BibtexError::analyze(&bibtex::open("@article{foo, bar}"));
- assert_eq!(
- errors,
- vec![BibtexError::new(
- BibtexErrorCode::MissingAssign,
- Position::new(0, 17),
- )]
+ return Some(());
+ }
+
+ if entry.key().is_none() {
+ diagnostics_by_uri.insert(
+ Arc::clone(&document.uri),
+ Diagnostic {
+ range: document
+ .line_index
+ .line_col_lsp_range(entry.left_delimiter()?.text_range()),
+ severity: Some(DiagnosticSeverity::Error),
+ code: None,
+ code_description: None,
+ source: Some("texlab".to_string()),
+ message: "Expecting a key".to_string(),
+ related_information: None,
+ tags: None,
+ data: None,
+ },
);
- }
-
- #[test]
- fn field_content() {
- let errors = BibtexError::analyze(&bibtex::open("@article{foo,\nbar = }"));
- assert_eq!(
- errors,
- vec![BibtexError::new(
- BibtexErrorCode::MissingContent,
- Position::new(1, 5),
- )]
- );
- }
-
- #[test]
- fn field_comma() {
- let text = "@article{foo,\nfoo = bar\nbaz = qux}";
- let errors = BibtexError::analyze(&bibtex::open(text));
- assert_eq!(
- errors,
- vec![BibtexError::new(
- BibtexErrorCode::MissingComma,
- Position::new(1, 9),
- )]
+ return Some(());
+ }
+
+ if entry.key().is_none() {
+ diagnostics_by_uri.insert(
+ Arc::clone(&document.uri),
+ Diagnostic {
+ range: document
+ .line_index
+ .line_col_lsp_range(entry.right_delimiter()?.text_range()),
+ severity: Some(DiagnosticSeverity::Error),
+ code: None,
+ code_description: None,
+ source: Some("texlab".to_string()),
+ message: "Expecting a curly bracket: \"}\"".to_string(),
+ related_information: None,
+ tags: None,
+ data: None,
+ },
);
+ return Some(());
}
- #[test]
- fn content_quote() {
- let text = "@article{foo, bar =\n\"}";
- let errors = BibtexError::analyze(&bibtex::open(text));
- assert_eq!(
- errors,
- vec![BibtexError::new(
- BibtexErrorCode::MissingQuote,
- Position::new(1, 1),
- )]
- );
- }
+ Some(())
+}
- #[test]
- fn content_brace() {
- let text = "@article{foo, bar =\n{";
- let errors = BibtexError::analyze(&bibtex::open(text));
- assert_eq!(
- errors,
- vec![
- BibtexError::new(BibtexErrorCode::MissingEndBrace, Position::new(1, 1)),
- BibtexError::new(BibtexErrorCode::MissingEndBrace, Position::new(1, 1)),
- ]
+fn analyze_field(
+ document: &Document,
+ diagnostics_by_uri: &mut MultiMap<Arc<Uri>, Diagnostic>,
+ node: &bibtex::SyntaxNode,
+) -> Option<()> {
+ let field = bibtex::Field::cast(node)?;
+ if field.equality_sign().is_none() {
+ diagnostics_by_uri.insert(
+ Arc::clone(&document.uri),
+ Diagnostic {
+ range: document
+ .line_index
+ .line_col_lsp_range(TextRange::empty(field.name()?.text_range().end())),
+ severity: Some(DiagnosticSeverity::Error),
+ code: None,
+ code_description: None,
+ source: Some("texlab".to_string()),
+ message: "Expecting an equality sign: \"=\"".to_string(),
+ related_information: None,
+ tags: None,
+ data: None,
+ },
);
- }
-
- #[test]
- fn content_concat() {
- let text = "@article{foo, bar = baz \n# }";
- let errors = BibtexError::analyze(&bibtex::open(text));
- assert_eq!(
- errors,
- vec![BibtexError::new(
- BibtexErrorCode::MissingContent,
- Position::new(1, 1)
- )]
+ return Some(());
+ }
+
+ if field.value().is_none() {
+ diagnostics_by_uri.insert(
+ Arc::clone(&document.uri),
+ Diagnostic {
+ range: document.line_index.line_col_lsp_range(TextRange::empty(
+ field.equality_sign()?.text_range().end(),
+ )),
+ severity: Some(DiagnosticSeverity::Error),
+ code: None,
+ code_description: None,
+ source: Some("texlab".to_string()),
+ message: "Expecting a field value".to_string(),
+ related_information: None,
+ tags: None,
+ data: None,
+ },
);
+ return Some(());
}
- #[test]
- fn entry_valid() {
- let text = "@article{foo, bar = \"baz {qux}\" # quux}";
- let errors = BibtexError::analyze(&bibtex::open(text));
- assert_eq!(errors, Vec::new());
- }
+ Some(())
}
diff --git a/support/texlab/src/diagnostics/build_log.rs b/support/texlab/src/diagnostics/build_log.rs
new file mode 100644
index 0000000000..a2069ff762
--- /dev/null
+++ b/support/texlab/src/diagnostics/build_log.rs
@@ -0,0 +1,65 @@
+use std::{path::PathBuf, sync::Arc};
+
+use lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range};
+use multimap::MultiMap;
+
+use crate::{syntax::build_log::BuildErrorLevel, Uri, Workspace};
+
+pub fn analyze_build_log_static(
+ workspace: &dyn Workspace,
+ diagnostics_by_uri: &mut MultiMap<Arc<Uri>, Diagnostic>,
+ build_log_uri: &Uri,
+) -> Option<()> {
+ let build_log_document = workspace.get(build_log_uri)?;
+ let parse = build_log_document.data.as_build_log()?;
+
+ let root_document = workspace.documents().into_iter().find(|document| {
+ if let Some(data) = document.data.as_latex() {
+ data.extras
+ .implicit_links
+ .log
+ .iter()
+ .any(|u| u.as_ref() == build_log_uri)
+ } else {
+ false
+ }
+ })?;
+
+ let base_path = PathBuf::from(root_document.uri.path());
+
+ for error in &parse.errors {
+ let pos = Position::new(error.line.unwrap_or(0), 0);
+ let severity = match error.level {
+ BuildErrorLevel::Error => DiagnosticSeverity::Error,
+ BuildErrorLevel::Warning => DiagnosticSeverity::Warning,
+ };
+ let range = Range::new(pos, pos);
+ let diagnostic = Diagnostic {
+ range,
+ severity: Some(severity),
+ code: None,
+ code_description: None,
+ source: Some("latex".into()),
+ message: error.message.clone(),
+ related_information: None,
+ tags: None,
+ data: None,
+ };
+
+ let full_path = base_path.join(&error.relative_path);
+
+ let uri = if full_path.starts_with(&base_path) {
+ error
+ .relative_path
+ .to_str()
+ .and_then(|path| root_document.uri.join(path).map(Into::into).ok())
+ .map(Arc::new)
+ .unwrap_or_else(|| Arc::clone(&root_document.uri))
+ } else {
+ Arc::clone(&root_document.uri)
+ };
+
+ diagnostics_by_uri.insert(uri, diagnostic);
+ }
+ Some(())
+}
diff --git a/support/texlab/src/diagnostics/chktex.rs b/support/texlab/src/diagnostics/chktex.rs
new file mode 100644
index 0000000000..897887c473
--- /dev/null
+++ b/support/texlab/src/diagnostics/chktex.rs
@@ -0,0 +1,105 @@
+use std::{
+ fs, io,
+ path::Path,
+ process::{Command, Stdio},
+ sync::Arc,
+};
+
+use lsp_types::{Diagnostic, DiagnosticSeverity, NumberOrString, Range};
+use multimap::MultiMap;
+use once_cell::sync::Lazy;
+use regex::Regex;
+use tempfile::tempdir;
+
+use crate::{Options, RangeExt, Uri, Workspace};
+
+pub fn analyze_latex_chktex(
+ workspace: &dyn Workspace,
+ diagnostics_by_uri: &mut MultiMap<Arc<Uri>, Diagnostic>,
+ uri: &Uri,
+ options: &Options,
+) -> Option<()> {
+ let document = workspace.get(uri)?;
+ document.data.as_latex()?;
+
+ let current_dir = options
+ .root_directory
+ .as_ref()
+ .cloned()
+ .or_else(|| {
+ if document.uri.scheme() == "file" {
+ document
+ .uri
+ .to_file_path()
+ .unwrap()
+ .parent()
+ .map(ToOwned::to_owned)
+ } else {
+ None
+ }
+ })
+ .unwrap_or_else(|| ".".into());
+
+ diagnostics_by_uri.remove(uri);
+ diagnostics_by_uri.insert_many(
+ Arc::clone(&document.uri),
+ lint(&document.text, &current_dir).unwrap_or_default(),
+ );
+ Some(())
+}
+
+pub static LINE_REGEX: Lazy<Regex> =
+ Lazy::new(|| Regex::new("(\\d+):(\\d+):(\\d+):(\\w+):(\\w+):(.*)").unwrap());
+
+fn lint(text: &str, current_dir: &Path) -> io::Result<Vec<Diagnostic>> {
+ let directory = tempdir()?;
+ fs::write(directory.path().join("file.tex"), text)?;
+ let _ = fs::copy(
+ current_dir.join("chktexrc"),
+ directory.path().join("chktexrc"),
+ );
+
+ let output = Command::new("chktex")
+ .args(&["-I0", "-f%l:%c:%d:%k:%n:%m\n", "file.tex"])
+ .stdin(Stdio::null())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .current_dir(directory.path())
+ .output()?;
+
+ // let mut writer = BufWriter::new(process.stdin.take().unwrap());
+ // writer.write_all(text.as_bytes())?;
+ // // writer.flush()?;
+ // let output = process.wait_with_output()?;
+
+ let mut diagnostics = Vec::new();
+ for line in String::from_utf8_lossy(&output.stdout).lines() {
+ let captures = LINE_REGEX.captures(&line).unwrap();
+ let line = captures[1].parse::<u32>().unwrap() - 1;
+ let character = captures[2].parse::<u32>().unwrap() - 1;
+ let digit = captures[3].parse::<u32>().unwrap();
+ let kind = &captures[4];
+ let code = &captures[5];
+ let message = captures[6].into();
+ let range = Range::new_simple(line, character, line, character + digit);
+ let severity = match kind {
+ "Message" => DiagnosticSeverity::Information,
+ "Warning" => DiagnosticSeverity::Warning,
+ _ => DiagnosticSeverity::Error,
+ };
+
+ diagnostics.push(Diagnostic {
+ range,
+ severity: Some(severity),
+ code: Some(NumberOrString::String(code.into())),
+ code_description: None,
+ source: Some("chktex".into()),
+ message,
+ related_information: None,
+ tags: None,
+ data: None,
+ });
+ }
+
+ Ok(diagnostics)
+}
diff --git a/support/texlab/src/diagnostics/debouncer.rs b/support/texlab/src/diagnostics/debouncer.rs
new file mode 100644
index 0000000000..46bea80176
--- /dev/null
+++ b/support/texlab/src/diagnostics/debouncer.rs
@@ -0,0 +1,77 @@
+use std::{
+ sync::Arc,
+ thread::{self, JoinHandle},
+ time::{Duration, Instant},
+};
+
+use chashmap::CHashMap;
+use crossbeam_channel::Sender;
+
+use crate::{Document, ServerContext, Uri, Workspace};
+
+pub enum DiagnosticsMessage {
+ Analyze {
+ workspace: Arc<dyn Workspace>,
+ document: Arc<Document>,
+ },
+ Shutdown,
+}
+
+pub struct DiagnosticsDebouncer {
+ pub sender: Sender<DiagnosticsMessage>,
+ handle: Option<JoinHandle<()>>,
+}
+
+impl DiagnosticsDebouncer {
+ pub fn launch<A>(context: Arc<ServerContext>, action: A) -> Self
+ where
+ A: Fn(Arc<dyn Workspace>, Arc<Document>) + Send + Clone + 'static,
+ {
+ let (sender, receiver) = crossbeam_channel::unbounded();
+
+ let handle = thread::spawn(move || {
+ let pool = threadpool::Builder::new().build();
+ let last_task_time_by_uri: Arc<CHashMap<Arc<Uri>, Instant>> = Arc::default();
+ while let Ok(DiagnosticsMessage::Analyze {
+ workspace,
+ document,
+ }) = receiver.recv()
+ {
+ let delay = {
+ context
+ .options
+ .read()
+ .unwrap()
+ .diagnostics_delay
+ .unwrap_or(300)
+ };
+
+ if let Some(time) = last_task_time_by_uri.get(&document.uri) {
+ if time.elapsed().as_millis() < delay as u128 {
+ continue;
+ }
+ }
+
+ let last_task_time_by_uri = Arc::clone(&last_task_time_by_uri);
+ let action = action.clone();
+ pool.execute(move || {
+ thread::sleep(Duration::from_millis(delay));
+ last_task_time_by_uri.insert(Arc::clone(&document.uri), Instant::now());
+ action(workspace, document);
+ });
+ }
+ });
+
+ Self {
+ sender,
+ handle: Some(handle),
+ }
+ }
+}
+
+impl Drop for DiagnosticsDebouncer {
+ fn drop(&mut self) {
+ self.sender.send(DiagnosticsMessage::Shutdown).unwrap();
+ self.handle.take().unwrap().join().unwrap();
+ }
+}
diff --git a/support/texlab/src/diagnostics/latex.rs b/support/texlab/src/diagnostics/latex.rs
index 95fcaeeacf..b835f88398 100644
--- a/support/texlab/src/diagnostics/latex.rs
+++ b/support/texlab/src/diagnostics/latex.rs
@@ -1,113 +1,129 @@
+use std::sync::Arc;
+
+use cstree::TextRange;
+use lsp_types::{Diagnostic, DiagnosticSeverity};
+use multimap::MultiMap;
+
use crate::{
- protocol::{Diagnostic, DiagnosticSeverity, NumberOrString, Range, RangeExt, Uri},
- workspace::Document,
-};
-use chashmap::CHashMap;
-use futures::{
- future::{AbortHandle, Abortable, Aborted},
- lock::Mutex,
+ syntax::{latex, CstNode},
+ Document, LineIndexExt, Uri, Workspace,
};
-use log::trace;
-use once_cell::sync::Lazy;
-use regex::Regex;
-use std::process::Stdio;
-use tokio::{prelude::*, process::Command};
-
-#[derive(Debug, Default)]
-pub struct LatexDiagnosticsProvider {
- diagnostics_by_uri: CHashMap<Uri, Vec<Diagnostic>>,
- handle: Mutex<Option<AbortHandle>>,
-}
-impl LatexDiagnosticsProvider {
- pub fn get(&self, document: &Document) -> Vec<Diagnostic> {
- match self.diagnostics_by_uri.get(&document.uri) {
- Some(diagnostics) => diagnostics.to_owned(),
- None => Vec::new(),
- }
+pub fn analyze_latex_static(
+ workspace: &dyn Workspace,
+ diagnostics_by_uri: &mut MultiMap<Arc<Uri>, Diagnostic>,
+ uri: &Uri,
+) -> Option<()> {
+ let document = workspace.get(uri)?;
+ if !document.uri.as_str().ends_with(".tex") {
+ return None;
}
- pub async fn update(&self, uri: &Uri, text: &str) {
- if uri.scheme() != "file" {
- return;
- }
+ let data = document.data.as_latex()?;
- let mut handle_guard = self.handle.lock().await;
- if let Some(handle) = &*handle_guard {
- handle.abort();
- }
+ for node in data.root.descendants() {
+ analyze_environment(&document, diagnostics_by_uri, node)
+ .or_else(|| analyze_curly_group(&document, diagnostics_by_uri, node))
+ .or_else(|| {
+ if node.kind() == latex::ERROR && node.first_token()?.text() == "}" {
+ diagnostics_by_uri.insert(
+ Arc::clone(&document.uri),
+ Diagnostic {
+ range: document.line_index.line_col_lsp_range(node.text_range()),
+ severity: Some(DiagnosticSeverity::Error),
+ code: None,
+ code_description: None,
+ source: Some("texlab".to_string()),
+ message: "Unexpected \"}\"".to_string(),
+ related_information: None,
+ tags: None,
+ data: None,
+ },
+ );
+ Some(())
+ } else {
+ None
+ }
+ });
+ }
- let (handle, registration) = AbortHandle::new_pair();
- *handle_guard = Some(handle);
- drop(handle_guard);
+ Some(())
+}
- let future = Abortable::new(
- async move {
- self.diagnostics_by_uri
- .insert(uri.clone(), lint(text.into()).await.unwrap_or_default());
+fn analyze_environment(
+ document: &Document,
+ diagnostics_by_uri: &mut MultiMap<Arc<Uri>, Diagnostic>,
+ node: &latex::SyntaxNode,
+) -> Option<()> {
+ let environment = latex::Environment::cast(node)?;
+ let name1 = environment.begin()?.name()?.word()?;
+ let name2 = environment.end()?.name()?.word()?;
+ if name1.text() != name2.text() {
+ diagnostics_by_uri.insert(
+ Arc::clone(&document.uri),
+ Diagnostic {
+ range: document.line_index.line_col_lsp_range(name1.text_range()),
+ severity: Some(DiagnosticSeverity::Error),
+ code: None,
+ code_description: None,
+ source: Some("texlab".to_string()),
+ message: "Mismatched environment".to_string(),
+ related_information: None,
+ tags: None,
+ data: None,
},
- registration,
);
-
- if let Err(Aborted) = future.await {
- trace!("Killed ChkTeX because it took too long to execute")
- }
}
+ Some(())
}
-pub static LINE_REGEX: Lazy<Regex> =
- Lazy::new(|| Regex::new("(\\d+):(\\d+):(\\d+):(\\w+):(\\w+):(.*)").unwrap());
-
-async fn lint(text: String) -> io::Result<Vec<Diagnostic>> {
- let mut process: tokio::process::Child = Command::new("chktex")
- .args(&["-I0", "-f%l:%c:%d:%k:%n:%m\n"])
- .stdin(Stdio::piped())
- .stdout(Stdio::piped())
- .stderr(Stdio::null())
- .kill_on_drop(true)
- .spawn()?;
-
- process
- .stdin
- .take()
- .unwrap()
- .write_all(text.as_bytes())
- .await?;
-
- let mut stdout = String::new();
- process
- .stdout
- .take()
- .unwrap()
- .read_to_string(&mut stdout)
- .await?;
+fn analyze_curly_group(
+ document: &Document,
+ diagnostics_by_uri: &mut MultiMap<Arc<Uri>, Diagnostic>,
+ node: &latex::SyntaxNode,
+) -> Option<()> {
+ if !matches!(
+ node.kind(),
+ latex::CURLY_GROUP
+ | latex::CURLY_GROUP_COMMAND
+ | latex::CURLY_GROUP_KEY_VALUE
+ | latex::CURLY_GROUP_WORD
+ | latex::CURLY_GROUP_WORD_LIST
+ ) {
+ return None;
+ }
- let mut diagnostics = Vec::new();
- for line in stdout.lines() {
- if let Some(captures) = LINE_REGEX.captures(line) {
- let line = captures[1].parse::<u64>().unwrap() - 1;
- let character = captures[2].parse::<u64>().unwrap() - 1;
- let digit = captures[3].parse::<u64>().unwrap();
- let kind = &captures[4];
- let code = &captures[5];
- let message = captures[6].into();
- let range = Range::new_simple(line, character, line, character + digit);
- let severity = match kind {
- "Message" => DiagnosticSeverity::Information,
- "Warning" => DiagnosticSeverity::Warning,
- _ => DiagnosticSeverity::Error,
- };
+ let is_inside_verbatim_environment = node
+ .ancestors()
+ .filter_map(latex::Environment::cast)
+ .filter_map(|env| env.begin())
+ .filter_map(|begin| begin.name())
+ .filter_map(|name| name.word())
+ .any(|name| ["asy", "lstlisting", "minted", "verbatim"].contains(&name.text()));
- diagnostics.push(Diagnostic {
- source: Some("chktex".into()),
- code: Some(NumberOrString::String(code.into())),
- message,
- severity: Some(severity),
- range,
+ if !is_inside_verbatim_environment
+ && !node
+ .children_with_tokens()
+ .filter_map(|element| element.into_token())
+ .any(|token| token.kind() == latex::R_CURLY)
+ {
+ diagnostics_by_uri.insert(
+ Arc::clone(&document.uri),
+ Diagnostic {
+ range: document
+ .line_index
+ .line_col_lsp_range(TextRange::empty(node.text_range().end())),
+ severity: Some(DiagnosticSeverity::Error),
+ code: None,
+ code_description: None,
+ source: Some("texlab".to_string()),
+ message: "Missing \"}\" inserted".to_string(),
related_information: None,
tags: None,
- })
- }
+ data: None,
+ },
+ );
}
- Ok(diagnostics)
+
+ Some(())
}
diff --git a/support/texlab/src/diagnostics/mod.rs b/support/texlab/src/diagnostics/mod.rs
deleted file mode 100644
index 906da9858f..0000000000
--- a/support/texlab/src/diagnostics/mod.rs
+++ /dev/null
@@ -1,28 +0,0 @@
-mod bibtex;
-mod build;
-mod latex;
-
-pub use self::{
- bibtex::{BibtexDiagnosticsProvider, BibtexError, BibtexErrorCode},
- build::BuildDiagnosticsProvider,
- latex::LatexDiagnosticsProvider,
-};
-
-use crate::{protocol::Diagnostic, workspace::Document};
-
-#[derive(Debug, Default)]
-pub struct DiagnosticsManager {
- pub bibtex: BibtexDiagnosticsProvider,
- pub latex: LatexDiagnosticsProvider,
- pub build: BuildDiagnosticsProvider,
-}
-
-impl DiagnosticsManager {
- pub async fn get(&self, doc: &Document) -> Vec<Diagnostic> {
- let mut diagnostics = Vec::new();
- diagnostics.append(&mut self.bibtex.get(doc));
- diagnostics.append(&mut self.latex.get(doc));
- diagnostics.append(&mut self.build.get(doc).await);
- diagnostics
- }
-}
diff --git a/support/texlab/src/dispatch.rs b/support/texlab/src/dispatch.rs
new file mode 100644
index 0000000000..a080016480
--- /dev/null
+++ b/support/texlab/src/dispatch.rs
@@ -0,0 +1,81 @@
+use anyhow::Result;
+use log::warn;
+use lsp_server::{ErrorCode, Notification, Request, RequestId, Response};
+use serde::de::DeserializeOwned;
+
+pub struct NotificationDispatcher {
+ not: Option<Notification>,
+}
+
+impl NotificationDispatcher {
+ pub fn new(not: Notification) -> Self {
+ Self { not: Some(not) }
+ }
+
+ pub fn on<N, F>(mut self, handler: F) -> Result<Self>
+ where
+ N: lsp_types::notification::Notification,
+ N::Params: DeserializeOwned,
+ F: FnOnce(N::Params) -> Result<()>,
+ {
+ if let Some(not) = self.not {
+ match not.extract::<N::Params>(N::METHOD) {
+ Ok(params) => {
+ handler(params)?;
+ self.not = None;
+ }
+ Err(not) => {
+ self.not = Some(not);
+ }
+ };
+ }
+ Ok(self)
+ }
+
+ pub fn default(self) {
+ if let Some(not) = &self.not {
+ warn!("Unknown notification: {}", not.method);
+ }
+ }
+}
+
+pub struct RequestDispatcher {
+ req: Option<Request>,
+}
+
+impl RequestDispatcher {
+ pub fn new(req: Request) -> Self {
+ Self { req: Some(req) }
+ }
+
+ pub fn on<R, F>(mut self, handler: F) -> Result<Self>
+ where
+ R: lsp_types::request::Request,
+ R::Params: DeserializeOwned,
+ F: FnOnce(RequestId, R::Params) -> Result<()>,
+ {
+ if let Some(req) = self.req {
+ match req.extract::<R::Params>(R::METHOD) {
+ Ok((id, params)) => {
+ handler(id, params)?;
+ self.req = None;
+ }
+ Err(req) => {
+ self.req = Some(req);
+ }
+ }
+ }
+ Ok(self)
+ }
+
+ pub fn default(self) -> Option<Response> {
+ self.req.map(|req| {
+ warn!("Unknown request: {}", req.method);
+ Response::new_err(
+ req.id,
+ ErrorCode::MethodNotFound as i32,
+ "method not found".to_string(),
+ )
+ })
+ }
+}
diff --git a/support/texlab/src/distro.rs b/support/texlab/src/distro.rs
new file mode 100644
index 0000000000..1e67a14285
--- /dev/null
+++ b/support/texlab/src/distro.rs
@@ -0,0 +1,74 @@
+mod kpsewhich;
+mod miktex;
+mod texlive;
+
+use std::process::{Command, Stdio};
+
+use anyhow::Result;
+use derive_more::Display;
+use log::warn;
+
+pub use kpsewhich::Resolver;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Display)]
+pub enum DistributionKind {
+ #[display(fmt = "TeXLive")]
+ Texlive,
+ #[display(fmt = "MikTeX")]
+ Miktex,
+ #[display(fmt = "Tectonic")]
+ Tectonic,
+ #[display(fmt = "Unknown")]
+ Unknown,
+}
+
+#[derive(Debug, Clone)]
+pub struct Distribution {
+ pub kind: DistributionKind,
+ pub resolver: Resolver,
+}
+
+impl Distribution {
+ pub fn detect() -> Self {
+ let kind = match Command::new("latex").arg("--version").output() {
+ Ok(output) => {
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ if stdout.contains("TeX Live") {
+ DistributionKind::Texlive
+ } else if stdout.contains("MiKTeX") {
+ DistributionKind::Miktex
+ } else {
+ DistributionKind::Unknown
+ }
+ }
+ Err(_) => {
+ if Command::new("tectonic")
+ .arg("--version")
+ .stdout(Stdio::null())
+ .stderr(Stdio::null())
+ .status()
+ .is_ok()
+ {
+ DistributionKind::Tectonic
+ } else {
+ DistributionKind::Unknown
+ }
+ }
+ };
+
+ let resolver = match kind {
+ DistributionKind::Texlive => Self::load_resolver(texlive::load_resolver),
+ DistributionKind::Miktex => Self::load_resolver(miktex::load_resolver),
+ DistributionKind::Tectonic | DistributionKind::Unknown => Resolver::default(),
+ };
+ Self { kind, resolver }
+ }
+
+ fn load_resolver(loader: impl FnOnce() -> Result<Resolver>) -> Resolver {
+ match loader() {
+ Ok(resolver) => return resolver,
+ Err(why) => warn!("Failed to load resolver: {}", why),
+ };
+ Resolver::default()
+ }
+}
diff --git a/support/texlab/src/distro/kpsewhich.rs b/support/texlab/src/distro/kpsewhich.rs
new file mode 100644
index 0000000000..2dcc98118f
--- /dev/null
+++ b/support/texlab/src/distro/kpsewhich.rs
@@ -0,0 +1,74 @@
+use std::{
+ env,
+ ffi::OsStr,
+ fs,
+ path::{Path, PathBuf},
+ process::Command,
+};
+
+use anyhow::Result;
+use rustc_hash::FxHashMap;
+use smol_str::SmolStr;
+
+use crate::DocumentLanguage;
+
+#[derive(Debug, PartialEq, Eq, Clone, Default)]
+pub struct Resolver {
+ pub files_by_name: FxHashMap<SmolStr, PathBuf>,
+}
+
+impl Resolver {
+ pub fn new(files_by_name: FxHashMap<SmolStr, PathBuf>) -> Self {
+ Self { files_by_name }
+ }
+}
+
+pub fn parse_database(
+ root_directories: &[PathBuf],
+ mut reader: impl FnMut(&Path) -> Result<Vec<PathBuf>>,
+) -> Result<Resolver> {
+ let mut files_by_name = FxHashMap::default();
+ for directory in root_directories {
+ for path in reader(directory)? {
+ if DocumentLanguage::by_path(&path).is_some() {
+ if let Some(path) = make_absolute(root_directories, &path) {
+ if let Some(name) = path.file_name().and_then(OsStr::to_str).map(Into::into) {
+ files_by_name.insert(name, path);
+ }
+ }
+ }
+ }
+ }
+ Ok(Resolver::new(files_by_name))
+}
+
+fn make_absolute(root_directories: &[PathBuf], relative_path: &Path) -> Option<PathBuf> {
+ for dir in root_directories.iter().rev() {
+ if let Ok(path) = fs::canonicalize(dir.join(&relative_path)) {
+ return Some(path);
+ }
+ }
+ None
+}
+
+pub fn root_directories() -> Result<Vec<PathBuf>> {
+ let texmf = run(&["-var-value", "TEXMF"])?;
+ let expand_arg = format!("--expand-braces={}", texmf);
+ let expanded = run(&[&expand_arg])?;
+ let directories = env::split_paths(&expanded.replace("!", ""))
+ .filter(|path| path.exists())
+ .collect();
+ Ok(directories)
+}
+
+fn run(args: impl IntoIterator<Item = impl AsRef<OsStr>>) -> Result<String> {
+ let output = Command::new("kpsewhich").args(args).output()?;
+
+ let result = String::from_utf8(output.stdout)?
+ .lines()
+ .next()
+ .unwrap()
+ .into();
+
+ Ok(result)
+}
diff --git a/support/texlab/src/tex/miktex.rs b/support/texlab/src/distro/miktex.rs
index b543aa8a1b..563c8445e3 100644
--- a/support/texlab/src/tex/miktex.rs
+++ b/support/texlab/src/distro/miktex.rs
@@ -1,45 +1,19 @@
-use super::{
- compile,
- kpsewhich::{self, KpsewhichError, Resolver},
- Artifacts, CompileError, CompileParams, Distribution, DistributionKind,
-};
-use async_trait::async_trait;
-use byteorder::{LittleEndian, ReadBytesExt};
-use futures::lock::Mutex;
use std::{
ffi::OsStr,
+ fs,
io::{self, Cursor},
path::{Path, PathBuf},
- sync::Arc,
};
-use tokio::fs;
-
-#[derive(Debug, Default)]
-pub struct Miktex {
- resolver: Mutex<Arc<Resolver>>,
-}
-
-#[async_trait]
-impl Distribution for Miktex {
- fn kind(&self) -> DistributionKind {
- DistributionKind::Miktex
- }
- async fn compile<'a>(&'a self, params: CompileParams<'a>) -> Result<Artifacts, CompileError> {
- compile(params).await
- }
+use anyhow::{Context, Result};
+use byteorder::{LittleEndian, ReadBytesExt};
- async fn load(&self) -> Result<(), KpsewhichError> {
- let root_directories = kpsewhich::root_directories().await?;
- let resolver = kpsewhich::parse_database(&root_directories, read_database).await?;
- *self.resolver.lock().await = Arc::new(resolver);
- Ok(())
- }
+use super::kpsewhich::{self, Resolver};
- async fn resolver(&self) -> Arc<Resolver> {
- let resolver = self.resolver.lock().await;
- Arc::clone(&resolver)
- }
+pub fn load_resolver() -> Result<Resolver> {
+ let root_directories = kpsewhich::root_directories()?;
+ let resolver = kpsewhich::parse_database(&root_directories, read_database)?;
+ Ok(resolver)
}
const DATABASE_PATH: &str = "miktex/data/le";
@@ -49,18 +23,17 @@ const FNDB_TABLE_POINTER_OFFSET: usize = 4 * FNDB_WORD_SIZE;
const FNDB_TABLE_SIZE_OFFSET: usize = 6 * FNDB_WORD_SIZE;
const FNDB_ENTRY_SIZE: usize = 4 * FNDB_WORD_SIZE;
-async fn read_database(directory: &Path) -> Result<Vec<PathBuf>, KpsewhichError> {
+fn read_database(directory: &Path) -> Result<Vec<PathBuf>> {
let database_directory = directory.join(DATABASE_PATH);
if !database_directory.exists() {
return Ok(Vec::new());
}
let mut database = Vec::new();
- let mut files: tokio::fs::ReadDir = fs::read_dir(database_directory).await?;
- while let Some(file) = files.next_entry().await? {
+ for file in fs::read_dir(database_directory)?.filter_map(Result::ok) {
if file.path().extension().and_then(OsStr::to_str) == Some("fndb-5") {
- let bytes = fs::read(file.path()).await?;
- database.extend(parse_database(&bytes).map_err(|_| KpsewhichError::CorruptDatabase)?);
+ let bytes = fs::read(file.path())?;
+ database.extend(parse_database(&bytes).context("parsing kpsewhich database")?);
}
}
Ok(database)
diff --git a/support/texlab/src/distro/texlive.rs b/support/texlab/src/distro/texlive.rs
new file mode 100644
index 0000000000..c9b4951e34
--- /dev/null
+++ b/support/texlab/src/distro/texlive.rs
@@ -0,0 +1,43 @@
+use std::{
+ fs, io,
+ path::{Path, PathBuf},
+ str::Lines,
+};
+
+use anyhow::Result;
+
+use super::kpsewhich::{self, Resolver};
+
+pub fn load_resolver() -> Result<Resolver> {
+ let root_directories = kpsewhich::root_directories()?;
+ let resolver = kpsewhich::parse_database(&root_directories, read_database)?;
+ Ok(resolver)
+}
+
+const DATABASE_PATH: &str = "ls-R";
+
+fn read_database(directory: &Path) -> Result<Vec<PathBuf>> {
+ let file = directory.join(DATABASE_PATH);
+ if !file.is_file() {
+ return Ok(Vec::new());
+ }
+
+ let text = fs::read_to_string(file)?;
+ let files = parse_database(text.lines())?;
+ Ok(files)
+}
+
+fn parse_database(lines: Lines) -> io::Result<Vec<PathBuf>> {
+ let mut paths = Vec::new();
+ let mut directory = "";
+
+ for line in lines.filter(|x| !x.trim().is_empty() && !x.starts_with('%')) {
+ if line.ends_with(':') {
+ directory = &line[..line.len() - 1];
+ } else {
+ let path = PathBuf::from(directory).join(line);
+ paths.push(path);
+ }
+ }
+ Ok(paths)
+}
diff --git a/support/texlab/src/feature.rs b/support/texlab/src/feature.rs
deleted file mode 100644
index 2b32b64188..0000000000
--- a/support/texlab/src/feature.rs
+++ /dev/null
@@ -1,405 +0,0 @@
-use crate::{
- components::{Component, COMPONENT_DATABASE},
- protocol::*,
- tex::{Distribution, Language, UnknownDistribution},
- workspace::{Document, DocumentContent, DocumentParams, Snapshot},
-};
-use async_trait::async_trait;
-use itertools::Itertools;
-use std::{
- env,
- path::{Path, PathBuf},
- sync::Arc,
-};
-
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub struct DocumentView {
- pub snapshot: Arc<Snapshot>,
- pub current: Arc<Document>,
- pub related: Vec<Arc<Document>>,
-}
-
-impl DocumentView {
- pub fn analyze(
- snapshot: Arc<Snapshot>,
- current: Arc<Document>,
- options: &Options,
- current_dir: &Path,
- ) -> Self {
- let related = snapshot.relations(&current.uri, options, current_dir);
- Self {
- snapshot,
- current,
- related,
- }
- }
-
- pub fn components(&self) -> Vec<&'static Component> {
- let mut start_components = vec![COMPONENT_DATABASE.kernel()];
- for doc in &self.related {
- if let DocumentContent::Latex(table) = &doc.content {
- table
- .components
- .iter()
- .flat_map(|file| COMPONENT_DATABASE.find(file))
- .for_each(|component| start_components.push(component))
- }
- }
-
- let mut all_components = Vec::new();
- for component in start_components {
- all_components.push(component);
- component
- .references
- .iter()
- .flat_map(|file| COMPONENT_DATABASE.find(&file))
- .for_each(|component| all_components.push(component))
- }
-
- all_components
- .into_iter()
- .unique_by(|component| &component.file_names)
- .collect()
- }
-}
-
-#[derive(Clone)]
-pub struct FeatureRequest<P> {
- pub params: P,
- pub view: DocumentView,
- pub distro: Arc<dyn Distribution>,
- pub client_capabilities: Arc<ClientCapabilities>,
- pub options: Options,
- pub current_dir: Arc<PathBuf>,
-}
-
-impl<P> FeatureRequest<P> {
- pub fn snapshot(&self) -> &Snapshot {
- &self.view.snapshot
- }
-
- pub fn current(&self) -> &Document {
- &self.view.current
- }
-
- pub fn related(&self) -> &[Arc<Document>] {
- &self.view.related
- }
-}
-
-#[async_trait]
-pub trait FeatureProvider {
- type Params;
- type Output;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output;
-}
-
-type ListProvider<P, O> = Box<dyn FeatureProvider<Params = P, Output = Vec<O>> + Send + Sync>;
-
-#[derive(Default)]
-pub struct ConcatProvider<P, O> {
- providers: Vec<ListProvider<P, O>>,
-}
-
-impl<P, O> ConcatProvider<P, O> {
- pub fn new(providers: Vec<ListProvider<P, O>>) -> Self {
- Self { providers }
- }
-}
-
-#[async_trait]
-impl<P, O> FeatureProvider for ConcatProvider<P, O>
-where
- P: Send + Sync,
- O: Send + Sync,
-{
- type Params = P;
- type Output = Vec<O>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<P>) -> Vec<O> {
- let mut items = Vec::new();
- for provider in &self.providers {
- items.append(&mut provider.execute(req).await);
- }
- items
- }
-}
-
-type OptionProvider<P, O> = Box<dyn FeatureProvider<Params = P, Output = Option<O>> + Send + Sync>;
-
-#[derive(Default)]
-pub struct ChoiceProvider<P, O> {
- providers: Vec<OptionProvider<P, O>>,
-}
-
-impl<P, O> ChoiceProvider<P, O> {
- pub fn new(providers: Vec<OptionProvider<P, O>>) -> Self {
- Self { providers }
- }
-}
-
-#[async_trait]
-impl<P, O> FeatureProvider for ChoiceProvider<P, O>
-where
- P: Send + Sync,
- O: Send + Sync,
-{
- type Params = P;
- type Output = Option<O>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<P>) -> Option<O> {
- for provider in &self.providers {
- let item = provider.execute(req).await;
- if item.is_some() {
- return item;
- }
- }
- None
- }
-}
-
-#[derive(Debug)]
-pub struct FeatureTester {
- main: String,
- files: Vec<(String, String)>,
- distro: Arc<dyn Distribution>,
- position: Position,
- new_name: String,
- include_declaration: bool,
- client_capabilities: Arc<ClientCapabilities>,
- current_dir: Arc<PathBuf>,
- root_dir: Option<PathBuf>,
- output_dir: Option<PathBuf>,
-}
-
-impl Default for FeatureTester {
- fn default() -> Self {
- Self::new()
- }
-}
-
-impl FeatureTester {
- pub fn new() -> Self {
- Self {
- main: String::new(),
- files: Vec::new(),
- distro: Arc::new(UnknownDistribution::default()),
- position: Position::default(),
- new_name: String::new(),
- include_declaration: false,
- client_capabilities: Arc::default(),
- current_dir: Arc::new(env::temp_dir()),
- root_dir: None,
- output_dir: None,
- }
- }
-
- pub fn main<S: Into<String>>(&mut self, name: S) -> &mut Self {
- self.main = name.into();
- self
- }
-
- pub fn file<S, T>(&mut self, name: S, text: T) -> &mut Self
- where
- S: Into<String>,
- T: Into<String>,
- {
- self.files.push((name.into(), text.into()));
- self
- }
-
- pub fn position(&mut self, line: u64, character: u64) -> &mut Self {
- self.position = Position::new(line, character);
- self
- }
-
- pub fn new_name<S: Into<String>>(&mut self, value: S) -> &mut Self {
- self.new_name = value.into();
- self
- }
-
- pub fn include_declaration(&mut self) -> &mut Self {
- self.include_declaration = true;
- self
- }
-
- pub fn root_directory<P: Into<PathBuf>>(&mut self, path: P) -> &mut Self {
- self.root_dir = Some(path.into());
- self
- }
-
- pub fn output_directory<P: Into<PathBuf>>(&mut self, path: P) -> &mut Self {
- self.output_dir = Some(path.into());
- self
- }
-
- pub fn uri(name: &str) -> Uri {
- let path = env::temp_dir().join(name);
- Uri::from_file_path(path).unwrap()
- }
-
- fn identifier(&self) -> TextDocumentIdentifier {
- let uri = Self::uri(&self.main);
- TextDocumentIdentifier::new(uri.into())
- }
-
- fn options(&self) -> Options {
- Options {
- latex: Some(LatexOptions {
- build: Some(LatexBuildOptions {
- output_directory: self.output_dir.clone(),
- ..LatexBuildOptions::default()
- }),
- root_directory: self.root_dir.clone(),
- ..LatexOptions::default()
- }),
- ..Options::default()
- }
- }
-
- async fn view(&self) -> DocumentView {
- let mut snapshot = Snapshot::new();
- let resolver = self.distro.resolver().await;
- let options = self.options();
- for (name, text) in &self.files {
- let uri = Self::uri(name);
- let path = uri.to_file_path().unwrap();
- let language = path
- .extension()
- .and_then(|ext| ext.to_str())
- .and_then(Language::by_extension)
- .unwrap();
- let doc = Document::open(DocumentParams {
- uri,
- text: text.trim().into(),
- language,
- resolver: &resolver,
- options: &options,
- current_dir: &self.current_dir,
- });
- snapshot.push(doc);
- }
- let current = snapshot.find(&Self::uri(&self.main)).unwrap();
- DocumentView::analyze(Arc::new(snapshot), current, &options, &self.current_dir)
- }
-
- async fn request<P>(&self, params: P) -> FeatureRequest<P> {
- FeatureRequest {
- params,
- view: self.view().await,
- client_capabilities: Arc::clone(&self.client_capabilities),
- distro: self.distro.clone(),
- options: self.options(),
- current_dir: Arc::clone(&self.current_dir),
- }
- }
-
- pub async fn test_position<F, O>(&self, provider: F) -> O
- where
- F: FeatureProvider<Params = TextDocumentPositionParams, Output = O>,
- {
- let text_document = self.identifier();
- let params = TextDocumentPositionParams::new(text_document, self.position);
- let req = self.request(params).await;
- provider.execute(&req).await
- }
-
- pub async fn test_completion<F, O>(&self, provider: F) -> O
- where
- F: FeatureProvider<Params = CompletionParams, Output = O>,
- {
- let req = self.test_completion_request().await;
- provider.execute(&req).await
- }
-
- pub async fn test_completion_request(&self) -> FeatureRequest<CompletionParams> {
- let params = CompletionParams {
- text_document_position: TextDocumentPositionParams::new(
- self.identifier(),
- self.position,
- ),
- context: None,
- work_done_progress_params: WorkDoneProgressParams::default(),
- partial_result_params: PartialResultParams::default(),
- };
- self.request(params).await
- }
-
- pub async fn test_folding<F, O>(&self, provider: F) -> O
- where
- F: FeatureProvider<Params = FoldingRangeParams, Output = O>,
- {
- let text_document = self.identifier();
- let params = FoldingRangeParams {
- text_document,
- work_done_progress_params: WorkDoneProgressParams::default(),
- partial_result_params: PartialResultParams::default(),
- };
- let req = self.request(params).await;
- provider.execute(&req).await
- }
-
- pub async fn test_link<F, O>(&self, provider: F) -> O
- where
- F: FeatureProvider<Params = DocumentLinkParams, Output = O>,
- {
- let text_document = self.identifier();
- let params = DocumentLinkParams {
- text_document,
- work_done_progress_params: WorkDoneProgressParams::default(),
- partial_result_params: PartialResultParams::default(),
- };
- let req = self.request(params).await;
- provider.execute(&req).await
- }
-
- pub async fn test_reference<F, O>(&self, provider: F) -> O
- where
- F: FeatureProvider<Params = ReferenceParams, Output = O>,
- {
- let params = ReferenceParams {
- text_document_position: TextDocumentPositionParams::new(
- self.identifier(),
- self.position,
- ),
- context: ReferenceContext {
- include_declaration: self.include_declaration,
- },
- work_done_progress_params: WorkDoneProgressParams::default(),
- partial_result_params: PartialResultParams::default(),
- };
- let req = self.request(params).await;
- provider.execute(&req).await
- }
-
- pub async fn test_rename<F, O>(&self, provider: F) -> O
- where
- F: FeatureProvider<Params = RenameParams, Output = O>,
- {
- let params = RenameParams {
- text_document_position: TextDocumentPositionParams::new(
- self.identifier(),
- self.position,
- ),
- new_name: self.new_name.clone(),
- work_done_progress_params: WorkDoneProgressParams::default(),
- };
- let req = self.request(params).await;
- provider.execute(&req).await
- }
-
- pub async fn test_symbol<F, O>(&self, provider: F) -> O
- where
- F: FeatureProvider<Params = DocumentSymbolParams, Output = O>,
- {
- let text_document = self.identifier();
- let params = DocumentSymbolParams {
- text_document,
- work_done_progress_params: WorkDoneProgressParams::default(),
- partial_result_params: PartialResultParams::default(),
- };
- let req = self.request(params).await;
- provider.execute(&req).await
- }
-}
diff --git a/support/texlab/src/features.rs b/support/texlab/src/features.rs
new file mode 100644
index 0000000000..09743c0035
--- /dev/null
+++ b/support/texlab/src/features.rs
@@ -0,0 +1,272 @@
+mod build;
+#[cfg(feature = "completion")]
+mod completion;
+mod cursor;
+mod definition;
+mod folding;
+mod formatting;
+mod forward_search;
+mod highlight;
+mod hover;
+mod link;
+mod lsp_kinds;
+mod reference;
+mod rename;
+#[cfg(feature = "semantic")]
+mod semantic;
+mod symbol;
+
+use std::sync::Arc;
+
+use crate::{Document, ServerContext, Workspace, WorkspaceSubset};
+
+#[cfg(feature = "completion")]
+pub use self::completion::{complete, CompletionItemData, COMPLETION_LIMIT};
+#[cfg(feature = "semantic")]
+pub use self::semantic::{find_semantic_tokens_range, legend};
+pub use self::{
+ build::{build_document, BuildParams, BuildResult, BuildStatus},
+ definition::goto_definition,
+ folding::find_foldings,
+ formatting::format_source_code,
+ forward_search::{execute_forward_search, ForwardSearchResult, ForwardSearchStatus},
+ highlight::find_document_highlights,
+ hover::find_hover,
+ link::find_document_links,
+ reference::find_all_references,
+ rename::{prepare_rename_all, rename_all},
+ symbol::{find_document_symbols, find_workspace_symbols},
+};
+
+#[derive(Clone)]
+pub struct FeatureRequest<P> {
+ pub context: Arc<ServerContext>,
+ pub params: P,
+ pub workspace: Arc<dyn Workspace>,
+ pub subset: WorkspaceSubset,
+}
+
+impl<P> FeatureRequest<P> {
+ pub fn main_document(&self) -> &Document {
+ &self.subset.documents[0]
+ }
+}
+
+#[cfg(test)]
+mod testing {
+ use std::{path::PathBuf, sync::Arc};
+
+ use lsp_types::{
+ ClientCapabilities, ClientInfo, CompletionParams, DocumentFormattingParams,
+ DocumentHighlightParams, DocumentLinkParams, FoldingRangeParams, FormattingOptions,
+ GotoDefinitionParams, HoverParams, PartialResultParams, Position, ReferenceContext,
+ ReferenceParams, RenameParams, TextDocumentIdentifier, TextDocumentPositionParams,
+ WorkDoneProgressParams,
+ };
+ use typed_builder::TypedBuilder;
+
+ use crate::{
+ create_workspace_fast, distro::Resolver, DocumentLanguage, Options, ServerContext, Uri,
+ Workspace, WorkspaceSource,
+ };
+
+ use super::*;
+
+ #[derive(Debug, Clone, TypedBuilder)]
+ pub struct FeatureTester<'a> {
+ main: &'a str,
+
+ files: Vec<(&'a str, &'a str)>,
+
+ #[builder(default)]
+ line: u32,
+
+ #[builder(default)]
+ character: u32,
+
+ #[builder(default)]
+ new_name: &'a str,
+
+ #[builder(default)]
+ include_declaration: bool,
+
+ #[builder(default)]
+ client_capabilities: ClientCapabilities,
+
+ #[builder(default)]
+ client_info: Option<ClientInfo>,
+
+ #[builder(default)]
+ resolver: Resolver,
+
+ #[builder(default=std::env::temp_dir())]
+ current_directory: PathBuf,
+
+ #[builder(default, setter(strip_option))]
+ root_directory: Option<PathBuf>,
+
+ #[builder(default, setter(strip_option))]
+ aux_directory: Option<PathBuf>,
+ }
+
+ impl<'a> FeatureTester<'a> {
+ pub fn uri(&self, name: &str) -> Arc<Uri> {
+ let path = self.current_directory.join(name);
+ Arc::new(Uri::from_file_path(path).unwrap())
+ }
+
+ fn options(&self) -> Options {
+ let mut options = Options::default();
+ options.aux_directory = self.aux_directory.clone();
+ options.root_directory = self.root_directory.clone();
+ options
+ }
+
+ fn identifier(&self) -> TextDocumentIdentifier {
+ let uri = self.uri(&self.main);
+ TextDocumentIdentifier::new(uri.as_ref().clone().into())
+ }
+
+ fn context(&self) -> Arc<ServerContext> {
+ let cx = ServerContext::new(self.current_directory.clone());
+ *cx.client_capabilities.lock().unwrap() = self.client_capabilities.clone();
+ *cx.client_info.lock().unwrap() = self.client_info.clone();
+ *cx.options.write().unwrap() = self.options();
+ *cx.resolver.lock().unwrap() = self.resolver.clone();
+ Arc::new(cx)
+ }
+
+ fn workspace(&self, cx: Arc<ServerContext>) -> Arc<dyn Workspace> {
+ let workspace = create_workspace_fast(cx).unwrap();
+ for (name, source_code) in &self.files {
+ let uri = self.uri(name);
+ let path = uri.to_file_path().unwrap();
+ let language = DocumentLanguage::by_path(&path).expect("unknown document language");
+ workspace.open(
+ uri,
+ source_code.trim().to_string(),
+ language,
+ WorkspaceSource::Client,
+ );
+ }
+ Arc::new(workspace)
+ }
+
+ fn request<P>(&self, params: P) -> FeatureRequest<P> {
+ let cx = self.context();
+ let workspace = self.workspace(Arc::clone(&cx));
+ let subset = workspace.subset(self.uri(self.main)).unwrap();
+ FeatureRequest {
+ context: cx,
+ workspace,
+ subset,
+ params,
+ }
+ }
+
+ pub fn link(self) -> FeatureRequest<DocumentLinkParams> {
+ let text_document = self.identifier();
+ let params = DocumentLinkParams {
+ text_document,
+ work_done_progress_params: WorkDoneProgressParams::default(),
+ partial_result_params: PartialResultParams::default(),
+ };
+ self.request(params)
+ }
+
+ pub fn folding(self) -> FeatureRequest<FoldingRangeParams> {
+ let text_document = self.identifier();
+ let params = FoldingRangeParams {
+ text_document,
+ work_done_progress_params: WorkDoneProgressParams::default(),
+ partial_result_params: PartialResultParams::default(),
+ };
+ self.request(params)
+ }
+
+ pub fn reference(self) -> FeatureRequest<ReferenceParams> {
+ let params = ReferenceParams {
+ text_document_position: TextDocumentPositionParams::new(
+ self.identifier(),
+ Position::new(self.line, self.character),
+ ),
+ context: ReferenceContext {
+ include_declaration: self.include_declaration,
+ },
+ work_done_progress_params: WorkDoneProgressParams::default(),
+ partial_result_params: PartialResultParams::default(),
+ };
+ self.request(params)
+ }
+
+ pub fn hover(self) -> FeatureRequest<HoverParams> {
+ let params = HoverParams {
+ text_document_position_params: TextDocumentPositionParams::new(
+ self.identifier(),
+ Position::new(self.line, self.character),
+ ),
+ work_done_progress_params: WorkDoneProgressParams::default(),
+ };
+ self.request(params)
+ }
+
+ pub fn completion(self) -> FeatureRequest<CompletionParams> {
+ let params = CompletionParams {
+ text_document_position: TextDocumentPositionParams::new(
+ self.identifier(),
+ Position::new(self.line, self.character),
+ ),
+ work_done_progress_params: WorkDoneProgressParams::default(),
+ partial_result_params: PartialResultParams::default(),
+ context: None,
+ };
+
+ self.request(params)
+ }
+
+ pub fn definition(self) -> FeatureRequest<GotoDefinitionParams> {
+ let params = GotoDefinitionParams {
+ text_document_position_params: TextDocumentPositionParams::new(
+ self.identifier(),
+ Position::new(self.line, self.character),
+ ),
+ work_done_progress_params: WorkDoneProgressParams::default(),
+ partial_result_params: PartialResultParams::default(),
+ };
+ self.request(params)
+ }
+
+ pub fn rename(self) -> FeatureRequest<RenameParams> {
+ let params = RenameParams {
+ text_document_position: TextDocumentPositionParams::new(
+ self.identifier(),
+ Position::new(self.line, self.character),
+ ),
+ new_name: self.new_name.to_string(),
+ work_done_progress_params: WorkDoneProgressParams::default(),
+ };
+ self.request(params)
+ }
+
+ pub fn formatting(self) -> FeatureRequest<DocumentFormattingParams> {
+ let params = DocumentFormattingParams {
+ text_document: self.identifier(),
+ work_done_progress_params: WorkDoneProgressParams::default(),
+ options: FormattingOptions::default(),
+ };
+ self.request(params)
+ }
+
+ pub fn highlight(self) -> FeatureRequest<DocumentHighlightParams> {
+ let params = DocumentHighlightParams {
+ text_document_position_params: TextDocumentPositionParams::new(
+ self.identifier(),
+ Position::new(self.line, self.character),
+ ),
+ work_done_progress_params: WorkDoneProgressParams::default(),
+ partial_result_params: PartialResultParams::default(),
+ };
+ self.request(params)
+ }
+ }
+}
diff --git a/support/texlab/src/features/build.rs b/support/texlab/src/features/build.rs
new file mode 100644
index 0000000000..c6a670528f
--- /dev/null
+++ b/support/texlab/src/features/build.rs
@@ -0,0 +1,133 @@
+use std::{
+ io::{self, BufRead, BufReader, Read},
+ path::Path,
+ process::{Command, Stdio},
+ thread,
+};
+
+use cancellation::CancellationToken;
+use crossbeam_channel::Sender;
+use encoding_rs_io::DecodeReaderBytesBuilder;
+use lsp_types::TextDocumentIdentifier;
+use serde::{Deserialize, Serialize};
+use serde_repr::{Deserialize_repr, Serialize_repr};
+
+use crate::Options;
+
+use super::FeatureRequest;
+
+#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct BuildParams {
+ pub text_document: TextDocumentIdentifier,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize_repr, Deserialize_repr)]
+#[repr(i32)]
+pub enum BuildStatus {
+ Success = 0,
+ Error = 1,
+ Failure = 2,
+ Cancelled = 3,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct BuildResult {
+ pub status: BuildStatus,
+}
+
+pub fn build_document(
+ request: FeatureRequest<BuildParams>,
+ _cancellation_token: &CancellationToken,
+ log_sender: Sender<String>,
+) -> BuildResult {
+ let document = request
+ .subset
+ .documents
+ .iter()
+ .find(|document| {
+ if let Some(data) = document.data.as_latex() {
+ data.extras.has_document_environment
+ } else {
+ false
+ }
+ })
+ .map(|document| document.as_ref())
+ .unwrap_or_else(|| request.main_document());
+
+ if document.uri.scheme() != "file" {
+ return BuildResult {
+ status: BuildStatus::Failure,
+ };
+ }
+ log::info!("Building document {}", document.uri.as_str());
+
+ let options = { request.context.options.read().unwrap().clone() };
+
+ let status = match build_internal(&document.uri.to_file_path().unwrap(), &options, log_sender) {
+ Ok(true) => BuildStatus::Success,
+ Ok(false) => BuildStatus::Error,
+ Err(why) => {
+ log::error!("Failed to execute textDocument/build: {}", why);
+ BuildStatus::Failure
+ }
+ };
+ BuildResult { status }
+}
+
+fn build_internal(path: &Path, options: &Options, log_sender: Sender<String>) -> io::Result<bool> {
+ let build_dir = options
+ .root_directory
+ .as_ref()
+ .map(AsRef::as_ref)
+ .or_else(|| path.parent())
+ .unwrap();
+
+ let args: Vec<_> = options
+ .build
+ .args()
+ .into_iter()
+ .map(|arg| replace_placeholder(arg, path))
+ .collect();
+
+ let mut process = Command::new(options.build.executable())
+ .args(args)
+ .stdin(Stdio::null())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .current_dir(build_dir)
+ .spawn()?;
+
+ track_output(process.stdout.take().unwrap(), log_sender.clone());
+ track_output(process.stderr.take().unwrap(), log_sender.clone());
+
+ if !options.build.is_continuous {
+ process.wait().map(|status| status.success())
+ } else {
+ Ok(true)
+ }
+}
+
+fn replace_placeholder(arg: String, file: &Path) -> String {
+ if arg.starts_with('"') || arg.ends_with('"') {
+ arg
+ } else {
+ arg.replace("%f", &file.to_string_lossy())
+ }
+}
+
+fn track_output(output: impl Read + Send + 'static, sender: Sender<String>) {
+ let reader = BufReader::new(
+ DecodeReaderBytesBuilder::new()
+ .encoding(Some(encoding_rs::UTF_8))
+ .utf8_passthru(true)
+ .strip_bom(true)
+ .build(output),
+ );
+ thread::spawn(move || {
+ for line in reader.lines() {
+ sender.send(line.unwrap()).unwrap();
+ }
+ });
+}
diff --git a/support/texlab/src/features/completion.rs b/support/texlab/src/features/completion.rs
new file mode 100644
index 0000000000..13c7aad22b
--- /dev/null
+++ b/support/texlab/src/features/completion.rs
@@ -0,0 +1,567 @@
+mod acronym_ref;
+mod argument;
+mod begin_command;
+mod citation;
+mod color;
+mod color_model;
+mod component_command;
+mod component_environment;
+mod entry_type;
+mod field;
+mod glossary_ref;
+mod import;
+mod include;
+mod label;
+mod theorem;
+mod tikz_library;
+mod types;
+mod user_command;
+mod user_environment;
+mod util;
+
+use cancellation::CancellationToken;
+use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
+use lsp_types::{
+ CompletionItem, CompletionList, CompletionParams, CompletionTextEdit, Documentation,
+ InsertTextFormat, MarkupContent, MarkupKind, TextEdit,
+};
+use rustc_hash::FxHashSet;
+
+use crate::{
+ syntax::{bibtex, latex, CstNode},
+ LineIndexExt,
+};
+
+use self::{
+ acronym_ref::complete_acronyms,
+ argument::complete_arguments,
+ begin_command::complete_begin_command,
+ citation::complete_citations,
+ color::complete_colors,
+ color_model::complete_color_models,
+ component_command::complete_component_commands,
+ component_environment::complete_component_environments,
+ entry_type::complete_entry_types,
+ field::complete_fields,
+ glossary_ref::complete_glossary_entries,
+ import::complete_imports,
+ include::complete_includes,
+ label::complete_labels,
+ theorem::complete_theorem_environments,
+ tikz_library::complete_tikz_libraries,
+ types::{InternalCompletionItem, InternalCompletionItemData},
+ user_command::complete_user_commands,
+ user_environment::complete_user_environments,
+ util::{adjust_kind, component_detail, image_documentation},
+};
+
+pub use self::types::CompletionItemData;
+
+use super::{
+ cursor::{Cursor, CursorContext},
+ lsp_kinds::Structure,
+ FeatureRequest,
+};
+
+pub const COMPLETION_LIMIT: usize = 50;
+
+pub fn complete(
+ request: FeatureRequest<CompletionParams>,
+ cancellation_token: &CancellationToken,
+) -> Option<CompletionList> {
+ let mut items = Vec::new();
+ let context = CursorContext::new(request);
+ log::debug!("[Completion] Cursor: {:?}", context.cursor);
+ complete_entry_types(&context, &mut items, cancellation_token);
+ complete_fields(&context, &mut items, cancellation_token);
+ complete_arguments(&context, &mut items, cancellation_token);
+ complete_citations(&context, &mut items, cancellation_token);
+ complete_imports(&context, &mut items, cancellation_token);
+ complete_colors(&context, &mut items, cancellation_token);
+ complete_color_models(&context, &mut items, cancellation_token);
+ complete_acronyms(&context, &mut items, cancellation_token);
+ complete_glossary_entries(&context, &mut items, cancellation_token);
+ complete_includes(&context, &mut items, cancellation_token);
+ complete_labels(&context, &mut items, cancellation_token);
+ complete_tikz_libraries(&context, &mut items, cancellation_token);
+ complete_component_environments(&context, &mut items, cancellation_token);
+ complete_theorem_environments(&context, &mut items, cancellation_token);
+ complete_user_environments(&context, &mut items, cancellation_token);
+ complete_begin_command(&context, &mut items, cancellation_token);
+ complete_component_commands(&context, &mut items, cancellation_token);
+ complete_user_commands(&context, &mut items, cancellation_token);
+
+ cancellation_token.result().ok()?;
+
+ let mut items = dedup(items);
+ preselect(&context, &mut items);
+ score(&context, &mut items);
+
+ items.sort_by_key(|item| (!item.preselect, -item.score.unwrap_or(std::i64::MIN + 1)));
+ let items: Vec<_> = items
+ .into_iter()
+ .take(COMPLETION_LIMIT)
+ .filter(|item| item.score.is_some())
+ .map(|item| convert_internal_items(&context, item))
+ .enumerate()
+ .map(|(i, item)| append_sort_text(item, i))
+ .collect();
+
+ let is_incomplete = if context
+ .request
+ .context
+ .client_info
+ .lock()
+ .unwrap()
+ .as_ref()
+ .map(|info| info.name.as_str())
+ .unwrap_or_default()
+ == "Visual Studio Code"
+ {
+ true
+ } else {
+ items.len() >= COMPLETION_LIMIT
+ };
+
+ Some(CompletionList {
+ is_incomplete,
+ items,
+ })
+}
+
+fn dedup(items: Vec<InternalCompletionItem>) -> Vec<InternalCompletionItem> {
+ let mut labels = FxHashSet::default();
+ let mut insert = vec![false; items.len()];
+ for (i, item) in items.iter().enumerate() {
+ insert[i] = labels.insert(item.data.label());
+ }
+ items
+ .into_iter()
+ .enumerate()
+ .filter(|(i, _)| insert[*i])
+ .map(|(_, item)| item)
+ .collect()
+}
+
+fn score(context: &CursorContext<CompletionParams>, items: &mut Vec<InternalCompletionItem>) {
+ let pattern = match &context.cursor {
+ Cursor::Latex(token) if token.kind().is_command_name() => token.text().trim_end(),
+ Cursor::Latex(token) if token.kind() == latex::WORD => token.text(),
+ Cursor::Latex(_) => "",
+ Cursor::Bibtex(token) if token.kind().is_type() => token.text(),
+ Cursor::Bibtex(token) if token.kind() == bibtex::WORD => token.text(),
+ Cursor::Bibtex(token) if token.kind() == bibtex::COMMAND_NAME => token.text().trim_end(),
+ Cursor::Bibtex(_) => "",
+ Cursor::Nothing => "",
+ };
+
+ let file_pattern = pattern.split('/').last().unwrap();
+ let matcher = SkimMatcherV2::default().ignore_case();
+ for item in items {
+ item.score = match &item.data {
+ InternalCompletionItemData::EntryType { ty } => {
+ matcher.fuzzy_match(&ty.name, &pattern[1..])
+ }
+ InternalCompletionItemData::Field { field } => {
+ matcher.fuzzy_match(&field.name, &pattern)
+ }
+ InternalCompletionItemData::Argument { name, .. } => {
+ matcher.fuzzy_match(name, &pattern)
+ }
+ InternalCompletionItemData::BeginCommand => matcher.fuzzy_match("begin", &pattern[1..]),
+ InternalCompletionItemData::Citation { key, .. } => matcher.fuzzy_match(&key, &pattern),
+ InternalCompletionItemData::ComponentCommand { name, .. } => {
+ matcher.fuzzy_match(name, &pattern[1..])
+ }
+ InternalCompletionItemData::ComponentEnvironment { name, .. } => {
+ matcher.fuzzy_match(name, &pattern)
+ }
+ InternalCompletionItemData::Class { name } => matcher.fuzzy_match(&name, &pattern),
+ InternalCompletionItemData::Package { name } => matcher.fuzzy_match(&name, &pattern),
+ InternalCompletionItemData::Color { name } => matcher.fuzzy_match(&name, &pattern),
+ InternalCompletionItemData::ColorModel { name } => matcher.fuzzy_match(&name, &pattern),
+ InternalCompletionItemData::Acronym { name } => matcher.fuzzy_match(&name, &pattern),
+ InternalCompletionItemData::GlossaryEntry { name } => {
+ matcher.fuzzy_match(&name, &pattern)
+ }
+ InternalCompletionItemData::File { name } => matcher.fuzzy_match(&name, file_pattern),
+ InternalCompletionItemData::Directory { name } => {
+ matcher.fuzzy_match(&name, file_pattern)
+ }
+ InternalCompletionItemData::Label { name, .. } => matcher.fuzzy_match(&name, &pattern),
+ InternalCompletionItemData::UserCommand { name } => {
+ matcher.fuzzy_match(&name, &pattern[1..])
+ }
+ InternalCompletionItemData::UserEnvironment { name } => {
+ matcher.fuzzy_match(&name, &pattern)
+ }
+ InternalCompletionItemData::PgfLibrary { name } => matcher.fuzzy_match(&name, &pattern),
+ InternalCompletionItemData::TikzLibrary { name } => {
+ matcher.fuzzy_match(&name, &pattern)
+ }
+ };
+ }
+}
+
+fn preselect(
+ context: &CursorContext<CompletionParams>,
+ items: &mut [InternalCompletionItem],
+) -> Option<()> {
+ let name = context.cursor.as_latex()?;
+ let group = latex::CurlyGroupWord::cast(name.parent())?;
+ let end = latex::End::cast(group.syntax().parent()?)?;
+ let environment = latex::Environment::cast(end.syntax().parent()?)?;
+ let name = environment.begin()?.name()?.word()?.text();
+
+ for item in items {
+ if item.data.label() == name {
+ item.preselect = true;
+ }
+ }
+ Some(())
+}
+
+fn convert_internal_items(
+ context: &CursorContext<CompletionParams>,
+ item: InternalCompletionItem,
+) -> CompletionItem {
+ let range = context
+ .request
+ .main_document()
+ .line_index
+ .line_col_lsp_range(item.range);
+
+ let mut new_item = match item.data {
+ InternalCompletionItemData::EntryType { ty } => {
+ let text_edit = TextEdit::new(range, (&ty.name).into());
+ let kind = Structure::Entry(ty.category).completion_kind();
+ CompletionItem {
+ label: (&ty.name).into(),
+ kind: Some(adjust_kind(&context.request, kind)),
+ documentation: ty.documentation.as_ref().map(|doc| {
+ Documentation::MarkupContent(MarkupContent {
+ kind: MarkupKind::Markdown,
+ value: doc.into(),
+ })
+ }),
+ text_edit: Some(CompletionTextEdit::Edit(text_edit)),
+ data: Some(serde_json::to_value(CompletionItemData::EntryType).unwrap()),
+ ..CompletionItem::default()
+ }
+ }
+ InternalCompletionItemData::Field { field } => {
+ let text_edit = TextEdit::new(range, (&field.name).into());
+ CompletionItem {
+ label: (&field.name).into(),
+ kind: Some(adjust_kind(
+ &context.request,
+ Structure::Field.completion_kind(),
+ )),
+ documentation: Some(Documentation::MarkupContent(MarkupContent {
+ kind: MarkupKind::Markdown,
+ value: (&field.documentation).into(),
+ })),
+ text_edit: Some(CompletionTextEdit::Edit(text_edit)),
+ data: Some(serde_json::to_value(CompletionItemData::FieldName).unwrap()),
+ ..CompletionItem::default()
+ }
+ }
+ InternalCompletionItemData::Argument { name, image } => {
+ let text_edit = TextEdit::new(range, name.into());
+ CompletionItem {
+ label: name.into(),
+ kind: Some(adjust_kind(
+ &context.request,
+ Structure::Argument.completion_kind(),
+ )),
+ data: Some(serde_json::to_value(CompletionItemData::Argument).unwrap()),
+ text_edit: Some(CompletionTextEdit::Edit(text_edit)),
+ documentation: image
+ .and_then(|image| image_documentation(&context.request, &name, image)),
+ ..CompletionItem::default()
+ }
+ }
+ InternalCompletionItemData::BeginCommand => {
+ if context
+ .request
+ .context
+ .client_capabilities
+ .lock()
+ .unwrap()
+ .text_document
+ .as_ref()
+ .and_then(|cap| cap.completion.as_ref())
+ .and_then(|cap| cap.completion_item.as_ref())
+ .and_then(|cap| cap.snippet_support)
+ == Some(true)
+ {
+ CompletionItem {
+ kind: Some(adjust_kind(
+ &context.request,
+ Structure::Snippet.completion_kind(),
+ )),
+ data: Some(serde_json::to_value(CompletionItemData::CommandSnippet).unwrap()),
+ insert_text: Some("begin{$1}\n\t$0\n\\end{$1}".into()),
+ insert_text_format: Some(InsertTextFormat::Snippet),
+ ..CompletionItem::new_simple("begin".into(), component_detail(&[]))
+ }
+ } else {
+ let text_edit = TextEdit::new(range, "begin".to_string());
+ CompletionItem {
+ kind: Some(adjust_kind(
+ &context.request,
+ Structure::Command.completion_kind(),
+ )),
+ data: Some(serde_json::to_value(CompletionItemData::Command).unwrap()),
+ text_edit: Some(CompletionTextEdit::Edit(text_edit)),
+ ..CompletionItem::new_simple("begin".to_string(), component_detail(&[]))
+ }
+ }
+ }
+ InternalCompletionItemData::Citation { uri, key, text, ty } => {
+ let text_edit = TextEdit::new(range, key.to_string());
+ CompletionItem {
+ label: key.to_string(),
+ kind: Some(adjust_kind(&context.request, ty.completion_kind())),
+ filter_text: Some(text.clone()),
+ sort_text: Some(text),
+ data: Some(
+ serde_json::to_value(CompletionItemData::Citation {
+ uri: uri.as_ref().clone(),
+ key: key.into(),
+ })
+ .unwrap(),
+ ),
+ text_edit: Some(CompletionTextEdit::Edit(text_edit)),
+ ..CompletionItem::default()
+ }
+ }
+ InternalCompletionItemData::ComponentCommand {
+ name,
+ image,
+ glyph,
+ file_names,
+ } => {
+ let detail = glyph.map_or_else(
+ || component_detail(file_names),
+ |glyph| format!("{}, {}", glyph, component_detail(file_names)),
+ );
+ let documentation =
+ image.and_then(|img| image_documentation(&context.request, &name, img));
+ let text_edit = TextEdit::new(range, name.to_string());
+ CompletionItem {
+ kind: Some(adjust_kind(
+ &context.request,
+ Structure::Command.completion_kind(),
+ )),
+ data: Some(serde_json::to_value(CompletionItemData::Command).unwrap()),
+ documentation,
+ text_edit: Some(CompletionTextEdit::Edit(text_edit)),
+ ..CompletionItem::new_simple(name.to_string(), detail)
+ }
+ }
+ InternalCompletionItemData::ComponentEnvironment { name, file_names } => {
+ let text_edit = TextEdit::new(range, name.to_string());
+ CompletionItem {
+ kind: Some(adjust_kind(
+ &context.request,
+ Structure::Environment.completion_kind(),
+ )),
+ data: Some(serde_json::to_value(CompletionItemData::Environment).unwrap()),
+ text_edit: Some(CompletionTextEdit::Edit(text_edit)),
+ ..CompletionItem::new_simple(name.to_string(), component_detail(file_names))
+ }
+ }
+ InternalCompletionItemData::Class { name } => {
+ let text_edit = TextEdit::new(range, name.to_string());
+ CompletionItem {
+ label: name.into(),
+ kind: Some(adjust_kind(
+ &context.request,
+ Structure::Class.completion_kind(),
+ )),
+ data: Some(serde_json::to_value(CompletionItemData::Class).unwrap()),
+ text_edit: Some(CompletionTextEdit::Edit(text_edit)),
+ ..CompletionItem::default()
+ }
+ }
+ InternalCompletionItemData::Package { name } => {
+ let text_edit = TextEdit::new(range, name.to_string());
+ CompletionItem {
+ label: name.into(),
+ kind: Some(adjust_kind(
+ &context.request,
+ Structure::Package.completion_kind(),
+ )),
+ data: Some(serde_json::to_value(CompletionItemData::Package).unwrap()),
+ text_edit: Some(CompletionTextEdit::Edit(text_edit)),
+ ..CompletionItem::default()
+ }
+ }
+ InternalCompletionItemData::Color { name } => {
+ let text_edit = TextEdit::new(range, name.into());
+ CompletionItem {
+ label: name.into(),
+ kind: Some(adjust_kind(
+ &context.request,
+ Structure::Color.completion_kind(),
+ )),
+ data: Some(serde_json::to_value(CompletionItemData::Color).unwrap()),
+ text_edit: Some(CompletionTextEdit::Edit(text_edit)),
+ ..CompletionItem::default()
+ }
+ }
+ InternalCompletionItemData::ColorModel { name } => {
+ let text_edit = TextEdit::new(range, name.into());
+ CompletionItem {
+ label: name.into(),
+ kind: Some(adjust_kind(
+ &context.request,
+ Structure::ColorModel.completion_kind(),
+ )),
+ data: Some(serde_json::to_value(CompletionItemData::ColorModel).unwrap()),
+ text_edit: Some(CompletionTextEdit::Edit(text_edit)),
+ ..CompletionItem::default()
+ }
+ }
+ InternalCompletionItemData::Acronym { name } => {
+ let text_edit = TextEdit::new(range, name.to_string());
+ CompletionItem {
+ label: name.into(),
+ kind: Some(adjust_kind(
+ &context.request,
+ Structure::GlossaryEntry.completion_kind(),
+ )),
+ data: Some(serde_json::to_value(CompletionItemData::Acronym).unwrap()),
+ text_edit: Some(CompletionTextEdit::Edit(text_edit)),
+ ..CompletionItem::default()
+ }
+ }
+ InternalCompletionItemData::GlossaryEntry { name } => {
+ let text_edit = TextEdit::new(range, name.to_string());
+ CompletionItem {
+ label: name.into(),
+ kind: Some(adjust_kind(
+ &context.request,
+ Structure::GlossaryEntry.completion_kind(),
+ )),
+ data: Some(serde_json::to_value(CompletionItemData::GlossaryEntry).unwrap()),
+ text_edit: Some(CompletionTextEdit::Edit(text_edit)),
+ ..CompletionItem::default()
+ }
+ }
+ InternalCompletionItemData::File { name } => {
+ let text_edit = TextEdit::new(range, name.to_string());
+ CompletionItem {
+ label: name.into(),
+ kind: Some(adjust_kind(
+ &context.request,
+ Structure::File.completion_kind(),
+ )),
+ data: Some(serde_json::to_value(CompletionItemData::File).unwrap()),
+ text_edit: Some(CompletionTextEdit::Edit(text_edit)),
+ ..CompletionItem::default()
+ }
+ }
+ InternalCompletionItemData::Directory { name } => {
+ let text_edit = TextEdit::new(range, name.to_string());
+ CompletionItem {
+ label: name.into(),
+ kind: Some(adjust_kind(
+ &context.request,
+ Structure::Folder.completion_kind(),
+ )),
+ data: Some(serde_json::to_value(CompletionItemData::Folder).unwrap()),
+ text_edit: Some(CompletionTextEdit::Edit(text_edit)),
+ ..CompletionItem::default()
+ }
+ }
+ InternalCompletionItemData::Label {
+ name,
+ kind,
+ header,
+ footer,
+ text,
+ } => {
+ let text_edit = TextEdit::new(range, name.to_string());
+ CompletionItem {
+ label: name.into(),
+ kind: Some(adjust_kind(&context.request, kind.completion_kind())),
+ detail: header,
+ documentation: footer.map(Documentation::String),
+ sort_text: Some(text.clone()),
+ filter_text: Some(text),
+ text_edit: Some(CompletionTextEdit::Edit(text_edit)),
+ data: Some(serde_json::to_value(CompletionItemData::Label).unwrap()),
+ ..CompletionItem::default()
+ }
+ }
+ InternalCompletionItemData::UserCommand { name } => {
+ let detail = "user-defined".into();
+ let text_edit = TextEdit::new(range, name.to_string());
+ CompletionItem {
+ kind: Some(adjust_kind(
+ &context.request,
+ Structure::Command.completion_kind(),
+ )),
+ data: Some(serde_json::to_value(CompletionItemData::Command).unwrap()),
+ text_edit: Some(CompletionTextEdit::Edit(text_edit)),
+ ..CompletionItem::new_simple(name.into(), detail)
+ }
+ }
+ InternalCompletionItemData::UserEnvironment { name } => {
+ let detail = "user-defined".into();
+ let text_edit = TextEdit::new(range, name.to_string());
+ CompletionItem {
+ kind: Some(adjust_kind(
+ &context.request,
+ Structure::Environment.completion_kind(),
+ )),
+ data: Some(serde_json::to_value(CompletionItemData::Environment).unwrap()),
+ text_edit: Some(CompletionTextEdit::Edit(text_edit)),
+ ..CompletionItem::new_simple(name.into(), detail)
+ }
+ }
+ InternalCompletionItemData::PgfLibrary { name } => {
+ let text_edit = TextEdit::new(range, name.into());
+ CompletionItem {
+ label: name.into(),
+ kind: Some(adjust_kind(
+ &context.request,
+ Structure::PgfLibrary.completion_kind(),
+ )),
+ data: Some(serde_json::to_value(CompletionItemData::PgfLibrary).unwrap()),
+ text_edit: Some(CompletionTextEdit::Edit(text_edit)),
+ ..CompletionItem::default()
+ }
+ }
+ InternalCompletionItemData::TikzLibrary { name } => {
+ let text_edit = TextEdit::new(range, name.into());
+ CompletionItem {
+ label: name.into(),
+ kind: Some(adjust_kind(
+ &context.request,
+ Structure::TikzLibrary.completion_kind(),
+ )),
+ data: Some(serde_json::to_value(CompletionItemData::TikzLibrary).unwrap()),
+ text_edit: Some(CompletionTextEdit::Edit(text_edit)),
+ ..CompletionItem::default()
+ }
+ }
+ };
+ new_item.preselect = Some(item.preselect);
+ new_item
+}
+
+fn append_sort_text(mut item: CompletionItem, index: usize) -> CompletionItem {
+ let sort_prefix = format!("{:0>2}", index);
+ match &item.sort_text {
+ Some(sort_text) => {
+ item.sort_text = Some(format!("{} {}", sort_prefix, sort_text));
+ }
+ None => {
+ item.sort_text = Some(sort_prefix);
+ }
+ };
+ item
+}
diff --git a/support/texlab/src/features/completion/acronym_ref.rs b/support/texlab/src/features/completion/acronym_ref.rs
new file mode 100644
index 0000000000..9d8bcc6c5f
--- /dev/null
+++ b/support/texlab/src/features/completion/acronym_ref.rs
@@ -0,0 +1,169 @@
+use cancellation::CancellationToken;
+use cstree::TextRange;
+use lsp_types::CompletionParams;
+
+use crate::{
+ features::cursor::CursorContext,
+ syntax::{latex, CstNode},
+};
+
+use super::types::{InternalCompletionItem, InternalCompletionItemData};
+
+pub fn complete_acronyms<'a>(
+ context: &'a CursorContext<CompletionParams>,
+ items: &mut Vec<InternalCompletionItem<'a>>,
+ cancellation_token: &CancellationToken,
+) -> Option<()> {
+ cancellation_token.result().ok()?;
+
+ let token = context.cursor.as_latex()?;
+ let group = latex::CurlyGroupWord::cast(token.parent())
+ .filter(|group| context.is_inside_latex_curly(group))?;
+
+ latex::AcronymReference::cast(group.syntax().parent()?)?;
+
+ let range = if token.kind() == latex::WORD {
+ token.text_range()
+ } else {
+ TextRange::empty(context.offset)
+ };
+
+ for document in &context.request.subset.documents {
+ if let Some(data) = document.data.as_latex() {
+ for name in data
+ .root
+ .descendants()
+ .filter_map(latex::AcronymDefinition::cast)
+ .filter_map(|node| node.name())
+ .filter_map(|name| name.word())
+ .map(|name| name.text())
+ {
+ items.push(InternalCompletionItem::new(
+ range,
+ InternalCompletionItemData::Acronym { name },
+ ));
+ }
+ }
+ }
+ Some(())
+}
+
+#[cfg(test)]
+mod tests {
+ use cstree::TextRange;
+
+ use crate::features::testing::FeatureTester;
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_acronyms(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_acronyms(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_simple() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\newacronym[longplural={Frames per Second}]{fpsLabel}{FPS}{Frame per Second}\n\\acrshort{f}")])
+ .main("main.tex")
+ .line(1)
+ .character(11)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_acronyms(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(87.into(), 88.into()));
+ }
+ }
+
+ #[test]
+ fn test_empty() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\newacronym[longplural={Frames per Second}]{fpsLabel}{FPS}{Frame per Second}\n\\acrshort{}")])
+ .main("main.tex")
+ .line(1)
+ .character(10)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_acronyms(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(87.into(), 87.into()));
+ }
+ }
+
+ #[test]
+ fn test_after_group() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\newacronym[longplural={Frames per Second}]{fpsLabel}{FPS}{Frame per Second}\n\\acrshort{}")])
+ .main("main.tex")
+ .line(1)
+ .character(11)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_acronyms(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_open_brace() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\newacronym[longplural={Frames per Second}]{fpsLabel}{FPS}{Frame per Second}\n\\acrshort{f")])
+ .main("main.tex")
+ .line(1)
+ .character(11)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_acronyms(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(87.into(), 88.into()));
+ }
+ }
+}
diff --git a/support/texlab/src/features/completion/argument.rs b/support/texlab/src/features/completion/argument.rs
new file mode 100644
index 0000000000..e2637e026f
--- /dev/null
+++ b/support/texlab/src/features/completion/argument.rs
@@ -0,0 +1,189 @@
+use cancellation::CancellationToken;
+use cstree::TextRange;
+use lsp_types::CompletionParams;
+
+use crate::{
+ component_db::COMPONENT_DATABASE,
+ features::cursor::CursorContext,
+ syntax::{latex, CstNode},
+};
+
+use super::types::{InternalCompletionItem, InternalCompletionItemData};
+
+pub fn complete_arguments<'a>(
+ context: &'a CursorContext<CompletionParams>,
+ items: &mut Vec<InternalCompletionItem<'a>>,
+ cancellation_token: &CancellationToken,
+) -> Option<()> {
+ cancellation_token.result().ok()?;
+
+ let token = context.cursor.as_latex()?;
+
+ let range = if token.kind() == latex::WORD {
+ token.text_range()
+ } else {
+ TextRange::empty(context.offset)
+ };
+
+ let group = latex::CurlyGroup::cast(token.parent())
+ .or_else(|| token.parent().parent().and_then(latex::CurlyGroup::cast))
+ .filter(|group| context.is_inside_latex_curly(group))?;
+
+ let command = latex::GenericCommand::cast(group.syntax().parent()?)?;
+
+ let index = command
+ .syntax()
+ .children()
+ .filter_map(latex::CurlyGroup::cast)
+ .position(|g| g.syntax().text_range() == group.syntax().text_range())?;
+
+ let command_name = &command.name()?.text()[1..];
+
+ for component in COMPONENT_DATABASE.linked_components(&context.request.subset) {
+ for component_command in component
+ .commands
+ .iter()
+ .filter(|command| command.name == command_name)
+ {
+ for (_, param) in component_command
+ .parameters
+ .iter()
+ .enumerate()
+ .filter(|(i, _)| *i == index)
+ {
+ for arg in &param.0 {
+ let item = InternalCompletionItem::new(
+ range,
+ InternalCompletionItemData::Argument {
+ name: &arg.name,
+ image: arg.image.as_deref(),
+ },
+ );
+ items.push(item);
+ }
+ }
+ }
+ }
+
+ Some(())
+}
+
+#[cfg(test)]
+mod tests {
+ use cstree::TextRange;
+
+ use crate::features::testing::FeatureTester;
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_arguments(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_arguments(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_latex_simple() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", r#"\mathbb{}\usepackage{amsfonts}"#)])
+ .main("main.tex")
+ .line(0)
+ .character(8)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_arguments(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(8.into(), 8.into()));
+ }
+ }
+
+ #[test]
+ fn test_latex_word() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", r#"\mathbb{foo}\usepackage{amsfonts}"#)])
+ .main("main.tex")
+ .line(0)
+ .character(8)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_arguments(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(8.into(), 11.into()));
+ }
+ }
+
+ #[test]
+ fn test_latex_open_brace() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\mathbb{ \\usepackage{amsfonts}")])
+ .main("main.tex")
+ .line(0)
+ .character(8)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_arguments(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(8.into(), 8.into()));
+ }
+ }
+
+ #[test]
+ fn test_latex_open_brace_second() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\mathbb{}{\\usepackage{amsfonts}")])
+ .main("main.tex")
+ .line(0)
+ .character(10)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_arguments(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+}
diff --git a/support/texlab/src/features/completion/begin_command.rs b/support/texlab/src/features/completion/begin_command.rs
new file mode 100644
index 0000000000..ebd272711e
--- /dev/null
+++ b/support/texlab/src/features/completion/begin_command.rs
@@ -0,0 +1,84 @@
+use cancellation::CancellationToken;
+use lsp_types::CompletionParams;
+
+use crate::features::cursor::CursorContext;
+
+use super::types::{InternalCompletionItem, InternalCompletionItemData};
+
+pub fn complete_begin_command(
+ context: &CursorContext<CompletionParams>,
+ items: &mut Vec<InternalCompletionItem>,
+ cancellation_token: &CancellationToken,
+) -> Option<()> {
+ cancellation_token.result().ok()?;
+ let range = context.cursor.command_range(context.offset)?;
+
+ items.push(InternalCompletionItem::new(
+ range,
+ InternalCompletionItemData::BeginCommand,
+ ));
+ Some(())
+}
+
+#[cfg(test)]
+mod tests {
+ use cstree::TextRange;
+
+ use crate::features::testing::FeatureTester;
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_begin_command(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_begin_command(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_latex_simple() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\")])
+ .main("main.tex")
+ .line(0)
+ .character(1)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_begin_command(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(1.into(), 1.into()));
+ }
+ }
+}
diff --git a/support/texlab/src/features/completion/citation.rs b/support/texlab/src/features/completion/citation.rs
new file mode 100644
index 0000000000..6df3364099
--- /dev/null
+++ b/support/texlab/src/features/completion/citation.rs
@@ -0,0 +1,244 @@
+use std::sync::Arc;
+
+use cancellation::CancellationToken;
+use cstree::TextRange;
+use lsp_types::CompletionParams;
+use once_cell::sync::Lazy;
+use regex::Regex;
+
+use crate::{
+ features::{cursor::CursorContext, lsp_kinds::Structure},
+ syntax::{
+ bibtex::{self, HasType},
+ latex, CstNode,
+ },
+ BibtexEntryTypeCategory, Document, LANGUAGE_DATA,
+};
+
+use super::types::{InternalCompletionItem, InternalCompletionItemData};
+
+pub fn complete_citations<'a>(
+ context: &'a CursorContext<CompletionParams>,
+ items: &mut Vec<InternalCompletionItem<'a>>,
+ cancellation_token: &CancellationToken,
+) -> Option<()> {
+ cancellation_token.result().ok()?;
+ let token = context.cursor.as_latex()?;
+
+ let range = if token.kind() == latex::WORD {
+ token.text_range()
+ } else {
+ TextRange::empty(context.offset)
+ };
+
+ check_citation(context, token).or_else(|| check_acronym(token))?;
+ for document in &context.request.subset.documents {
+ if let Some(data) = document.data.as_bibtex() {
+ for entry in data.root.children().filter_map(bibtex::Entry::cast) {
+ cancellation_token.result().ok()?;
+ if let Some(item) = make_item(document, entry, range) {
+ items.push(item);
+ }
+ }
+ }
+ }
+
+ Some(())
+}
+
+fn check_citation(
+ context: &CursorContext<CompletionParams>,
+ token: &latex::SyntaxToken,
+) -> Option<()> {
+ let group = latex::CurlyGroupWordList::cast(token.parent())
+ .filter(|group| context.is_inside_latex_curly(group))?;
+ latex::Citation::cast(group.syntax().parent()?)?;
+ Some(())
+}
+
+fn check_acronym(token: &latex::SyntaxToken) -> Option<()> {
+ let pair = token.ancestors().find_map(latex::KeyValuePair::cast)?;
+ let mut key_words = pair.key()?.words();
+ if key_words.next()?.text() != "cite" || key_words.next().is_some() {
+ return None;
+ }
+
+ latex::AcronymDeclaration::cast(pair.syntax().parent()?.parent()?.parent()?)?;
+ Some(())
+}
+
+fn make_item<'a>(
+ document: &'a Document,
+ entry: bibtex::Entry<'a>,
+ range: TextRange,
+) -> Option<InternalCompletionItem<'a>> {
+ let key = entry.key()?.text();
+ let ty = LANGUAGE_DATA
+ .find_entry_type(&entry.ty()?.text()[1..])
+ .map(|ty| Structure::Entry(ty.category))
+ .unwrap_or_else(|| Structure::Entry(BibtexEntryTypeCategory::Misc));
+
+ let entry_code = entry.syntax().text().to_string();
+ let text = format!(
+ "{} {}",
+ key,
+ WHITESPACE_REGEX
+ .replace_all(
+ &entry_code
+ .replace('{', "")
+ .replace('}', "")
+ .replace(',', " ")
+ .replace("=", " "),
+ " "
+ )
+ .trim(),
+ );
+
+ Some(InternalCompletionItem::new(
+ range,
+ InternalCompletionItemData::Citation {
+ uri: Arc::clone(&document.uri),
+ key: key.into(),
+ text,
+ ty,
+ },
+ ))
+}
+
+static WHITESPACE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("\\s+").unwrap());
+
+#[cfg(test)]
+mod tests {
+ use crate::features::testing::FeatureTester;
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_citations(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_citations(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_latex_simple() {
+ let request = FeatureTester::builder()
+ .files(vec![
+ ("main.tex", "\\addbibresource{main.bib}\n\\cite{}"),
+ ("main.bib", "@article{foo,}"),
+ ])
+ .main("main.tex")
+ .line(1)
+ .character(6)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_citations(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(32.into(), 32.into()));
+ }
+ }
+
+ #[test]
+ fn test_latex_open_brace() {
+ let request = FeatureTester::builder()
+ .files(vec![
+ ("main.tex", "\\addbibresource{main.bib}\n\\cite{"),
+ ("main.bib", "@article{foo,}"),
+ ])
+ .main("main.tex")
+ .line(1)
+ .character(6)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_citations(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(32.into(), 32.into()));
+ }
+ }
+
+ #[test]
+ fn test_latex_open_brace_second() {
+ let request = FeatureTester::builder()
+ .files(vec![
+ ("main.tex", "\\addbibresource{main.bib}\n\\cite{foo,a"),
+ ("main.bib", "@article{foo,}"),
+ ])
+ .main("main.tex")
+ .line(1)
+ .character(10)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_citations(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(36.into(), 37.into()));
+ }
+ }
+
+ #[test]
+ fn test_latex_acronym() {
+ let request = FeatureTester::builder()
+ .files(vec![
+ (
+ "main.tex",
+ "\\addbibresource{main.bib}\n\\DeclareAcronym{foo}{cite={\n}}",
+ ),
+ ("main.bib", "@article{foo,}"),
+ ])
+ .main("main.tex")
+ .line(2)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_citations(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(54.into(), 54.into()));
+ }
+ }
+}
diff --git a/support/texlab/src/features/completion/color.rs b/support/texlab/src/features/completion/color.rs
new file mode 100644
index 0000000000..0e94af15fe
--- /dev/null
+++ b/support/texlab/src/features/completion/color.rs
@@ -0,0 +1,122 @@
+use cancellation::CancellationToken;
+use cstree::TextRange;
+use lsp_types::CompletionParams;
+
+use crate::{
+ features::cursor::CursorContext,
+ syntax::{latex, CstNode},
+ LANGUAGE_DATA,
+};
+
+use super::types::{InternalCompletionItem, InternalCompletionItemData};
+
+pub fn complete_colors<'a>(
+ context: &'a CursorContext<CompletionParams>,
+ items: &mut Vec<InternalCompletionItem<'a>>,
+ cancellation_token: &CancellationToken,
+) -> Option<()> {
+ cancellation_token.result().ok()?;
+
+ let token = context.cursor.as_latex()?;
+ let range = if token.kind() == latex::WORD {
+ token.text_range()
+ } else {
+ TextRange::empty(context.offset)
+ };
+
+ let group = latex::CurlyGroupWord::cast(token.parent())
+ .filter(|group| context.is_inside_latex_curly(group))?;
+ latex::ColorReference::cast(group.syntax().parent()?)?;
+
+ for name in &LANGUAGE_DATA.colors {
+ items.push(InternalCompletionItem::new(
+ range,
+ InternalCompletionItemData::Color { name },
+ ));
+ }
+
+ Some(())
+}
+
+#[cfg(test)]
+mod tests {
+ use cstree::TextRange;
+
+ use crate::features::testing::FeatureTester;
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_colors(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_colors(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_latex_simple() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\color{}")])
+ .main("main.tex")
+ .line(0)
+ .character(7)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_colors(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(7.into(), 7.into()));
+ }
+ }
+
+ #[test]
+ fn test_latex_open_brace() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\color{")])
+ .main("main.tex")
+ .line(0)
+ .character(7)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_colors(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(7.into(), 7.into()));
+ }
+ }
+}
diff --git a/support/texlab/src/features/completion/color_model.rs b/support/texlab/src/features/completion/color_model.rs
new file mode 100644
index 0000000000..f7f09b6367
--- /dev/null
+++ b/support/texlab/src/features/completion/color_model.rs
@@ -0,0 +1,188 @@
+use cancellation::CancellationToken;
+use cstree::TextRange;
+use lsp_types::CompletionParams;
+
+use crate::{
+ features::cursor::CursorContext,
+ syntax::{latex, CstNode},
+};
+
+use super::types::{InternalCompletionItem, InternalCompletionItemData};
+
+const MODEL_NAMES: &[&str] = &["gray", "rgb", "RGB", "HTML", "cmyk"];
+
+pub fn complete_color_models<'a>(
+ context: &'a CursorContext<CompletionParams>,
+ items: &mut Vec<InternalCompletionItem<'a>>,
+ cancellation_token: &CancellationToken,
+) -> Option<()> {
+ cancellation_token.result().ok()?;
+
+ let token = context.cursor.as_latex()?;
+ let range = if token.kind() == latex::WORD {
+ token.text_range()
+ } else {
+ TextRange::empty(context.offset)
+ };
+
+ check_color_definition(context, token)
+ .or_else(|| check_color_definition_set(context, token))?;
+
+ for name in MODEL_NAMES {
+ items.push(InternalCompletionItem::new(
+ range,
+ InternalCompletionItemData::ColorModel { name },
+ ));
+ }
+
+ Some(())
+}
+
+fn check_color_definition(
+ context: &CursorContext<CompletionParams>,
+ token: &latex::SyntaxToken,
+) -> Option<()> {
+ let group = latex::CurlyGroupWord::cast(token.parent())
+ .filter(|group| context.is_inside_latex_curly(group))?;
+ let definition = latex::ColorDefinition::cast(group.syntax().parent()?)?;
+ definition
+ .model()
+ .filter(|model| model.syntax().text_range() == group.syntax().text_range())?;
+ Some(())
+}
+
+fn check_color_definition_set(
+ context: &CursorContext<CompletionParams>,
+ token: &latex::SyntaxToken,
+) -> Option<()> {
+ let group = latex::CurlyGroupWordList::cast(token.parent())
+ .filter(|group| context.is_inside_latex_curly(group))?;
+ let definition = latex::ColorSetDefinition::cast(group.syntax().parent()?)?;
+ definition
+ .model_list()
+ .filter(|model| model.syntax().text_range() == group.syntax().text_range())?;
+ Some(())
+}
+
+#[cfg(test)]
+mod tests {
+ use cstree::TextRange;
+
+ use crate::features::testing::FeatureTester;
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_color_models(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_color_models(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_definition_simple() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\definecolor{foo}{}")])
+ .main("main.tex")
+ .line(0)
+ .character(18)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_color_models(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(18.into(), 18.into()));
+ }
+ }
+
+ #[test]
+ fn test_definition_open_brace() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\definecolor{foo}{")])
+ .main("main.tex")
+ .line(0)
+ .character(18)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_color_models(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(18.into(), 18.into()));
+ }
+ }
+
+ #[test]
+ fn test_definition_set_simple() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\definecolorset{}")])
+ .main("main.tex")
+ .line(0)
+ .character(16)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_color_models(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(16.into(), 16.into()));
+ }
+ }
+
+ #[test]
+ fn test_definition_set_open_brace() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\definecolorset{")])
+ .main("main.tex")
+ .line(0)
+ .character(16)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_color_models(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(16.into(), 16.into()));
+ }
+ }
+}
diff --git a/support/texlab/src/features/completion/component_command.rs b/support/texlab/src/features/completion/component_command.rs
new file mode 100644
index 0000000000..aba5a78597
--- /dev/null
+++ b/support/texlab/src/features/completion/component_command.rs
@@ -0,0 +1,177 @@
+use cancellation::CancellationToken;
+use lsp_types::CompletionParams;
+
+use crate::{component_db::COMPONENT_DATABASE, features::cursor::CursorContext};
+
+use super::types::{InternalCompletionItem, InternalCompletionItemData};
+
+pub fn complete_component_commands<'a>(
+ context: &'a CursorContext<CompletionParams>,
+ items: &mut Vec<InternalCompletionItem<'a>>,
+ cancellation_token: &CancellationToken,
+) -> Option<()> {
+ cancellation_token.result().ok()?;
+
+ let range = context.cursor.command_range(context.offset)?;
+
+ for component in COMPONENT_DATABASE.linked_components(&context.request.subset) {
+ cancellation_token.result().ok()?;
+ for command in &component.commands {
+ items.push(InternalCompletionItem::new(
+ range,
+ InternalCompletionItemData::ComponentCommand {
+ name: &command.name,
+ image: command.image.as_deref(),
+ glyph: command.glyph.as_deref(),
+ file_names: &component.file_names,
+ },
+ ));
+ }
+ }
+
+ Some(())
+}
+
+#[cfg(test)]
+mod tests {
+ use cstree::TextRange;
+
+ use crate::features::testing::FeatureTester;
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_component_commands(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_component_commands(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_latex_simple() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\")])
+ .main("main.tex")
+ .line(0)
+ .character(1)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_component_commands(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(1.into(), 1.into()));
+ }
+ }
+
+ #[test]
+ fn test_latex_simple_before() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_component_commands(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_latex_simple_package() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\\n\\usepackage{lipsum}")])
+ .main("main.tex")
+ .line(0)
+ .character(1)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_component_commands(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in &actual_items {
+ assert_eq!(item.range, TextRange::new(1.into(), 1.into()));
+ }
+
+ assert!(actual_items
+ .iter()
+ .any(|item| item.data.label() == "lipsum"));
+ }
+
+ #[test]
+ fn test_latex_simple_existing() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\foo")])
+ .main("main.tex")
+ .line(0)
+ .character(2)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_component_commands(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(1.into(), 4.into()));
+ }
+ }
+
+ #[test]
+ fn test_bibtex_simple() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@a{b,c={\\ }}")])
+ .main("main.bib")
+ .line(0)
+ .character(9)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_component_commands(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(9.into(), 10.into()));
+ }
+ }
+}
diff --git a/support/texlab/src/features/completion/component_environment.rs b/support/texlab/src/features/completion/component_environment.rs
new file mode 100644
index 0000000000..fca9d03780
--- /dev/null
+++ b/support/texlab/src/features/completion/component_environment.rs
@@ -0,0 +1,174 @@
+use cancellation::CancellationToken;
+use cstree::TextRange;
+use lsp_types::CompletionParams;
+
+use crate::{
+ component_db::COMPONENT_DATABASE,
+ features::cursor::CursorContext,
+ syntax::{latex, CstNode},
+};
+
+use super::types::{InternalCompletionItem, InternalCompletionItemData};
+
+pub fn complete_component_environments<'a>(
+ context: &'a CursorContext<CompletionParams>,
+ items: &mut Vec<InternalCompletionItem<'a>>,
+ cancellation_token: &CancellationToken,
+) -> Option<()> {
+ cancellation_token.result().ok()?;
+
+ let token = context.cursor.as_latex()?;
+ let group = latex::CurlyGroupWord::cast(token.parent())
+ .filter(|group| context.is_inside_latex_curly(group))?;
+ if !matches!(group.syntax().parent()?.kind(), latex::BEGIN | latex::END) {
+ return None;
+ }
+
+ let range = if token.kind() == latex::WORD {
+ token.text_range()
+ } else {
+ TextRange::empty(context.offset)
+ };
+
+ for component in COMPONENT_DATABASE.linked_components(&context.request.subset) {
+ cancellation_token.result().ok()?;
+ for name in &component.environments {
+ items.push(InternalCompletionItem::new(
+ range,
+ InternalCompletionItemData::ComponentEnvironment {
+ name,
+ file_names: &component.file_names,
+ },
+ ));
+ }
+ }
+
+ Some(())
+}
+
+#[cfg(test)]
+mod tests {
+ use cstree::TextRange;
+
+ use crate::features::testing::FeatureTester;
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_component_environments(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_component_environments(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_simple() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\begin{")])
+ .main("main.tex")
+ .line(0)
+ .character(7)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_component_environments(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(7.into(), 7.into()));
+ }
+ }
+
+ #[test]
+ fn test_simple_end() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\begin{a}\n\\end{")])
+ .main("main.tex")
+ .line(1)
+ .character(5)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_component_environments(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(15.into(), 15.into()));
+ }
+ }
+
+ #[test]
+ fn test_simple_class() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\begin{}\n\\documentclass{article}")])
+ .main("main.tex")
+ .line(0)
+ .character(7)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_component_environments(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in &actual_items {
+ assert_eq!(item.range, TextRange::new(7.into(), 7.into()));
+ }
+
+ assert!(actual_items
+ .iter()
+ .any(|item| item.data.label() == "theindex"));
+ }
+
+ #[test]
+ fn test_simple_existing() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\begin{d}")])
+ .main("main.tex")
+ .line(0)
+ .character(7)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_component_environments(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(7.into(), 8.into()));
+ }
+ }
+}
diff --git a/support/texlab/src/features/completion/entry_type.rs b/support/texlab/src/features/completion/entry_type.rs
new file mode 100644
index 0000000000..c8774ea3ff
--- /dev/null
+++ b/support/texlab/src/features/completion/entry_type.rs
@@ -0,0 +1,231 @@
+use cancellation::CancellationToken;
+use cstree::{TextRange, TextSize};
+use lsp_types::CompletionParams;
+
+use crate::{features::cursor::CursorContext, LANGUAGE_DATA};
+
+use super::types::{InternalCompletionItem, InternalCompletionItemData};
+
+pub fn complete_entry_types<'a>(
+ context: &'a CursorContext<CompletionParams>,
+ items: &mut Vec<InternalCompletionItem<'a>>,
+ cancellation_token: &CancellationToken,
+) -> Option<()> {
+ if cancellation_token.is_canceled() {
+ return None;
+ }
+
+ let range = context
+ .cursor
+ .as_bibtex()
+ .filter(|token| token.kind().is_type())
+ .filter(|token| token.text_range().start() != context.offset)
+ .map(|token| token.text_range())
+ .map(|range| TextRange::new(range.start() + TextSize::from(1), range.end()))?;
+
+ for ty in &LANGUAGE_DATA.entry_types {
+ let data = InternalCompletionItemData::EntryType { ty };
+ let item = InternalCompletionItem::new(range, data);
+ items.push(item);
+ }
+
+ Some(())
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::features::testing::FeatureTester;
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_entry_types(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_entry_types(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_at_empty() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@")])
+ .main("main.bib")
+ .line(0)
+ .character(1)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_entry_types(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(1.into(), 1.into()));
+ }
+ }
+
+ #[test]
+ fn test_before_preamble() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@preamble")])
+ .main("main.bib")
+ .line(0)
+ .character(1)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_entry_types(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(1.into(), 9.into()));
+ }
+ }
+
+ #[test]
+ fn test_before_string() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@string")])
+ .main("main.bib")
+ .line(0)
+ .character(1)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_entry_types(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(1.into(), 7.into()));
+ }
+ }
+
+ #[test]
+ fn test_before_article() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@article")])
+ .main("main.bib")
+ .line(0)
+ .character(1)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_entry_types(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(1.into(), 8.into()));
+ }
+ }
+
+ #[test]
+ fn test_after_preamble() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@preamble{")])
+ .main("main.bib")
+ .line(0)
+ .character(9)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_entry_types(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(1.into(), 9.into()));
+ }
+ }
+
+ #[test]
+ fn test_after_string() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@string{")])
+ .main("main.bib")
+ .line(0)
+ .character(7)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_entry_types(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(1.into(), 7.into()));
+ }
+ }
+
+ #[test]
+ fn test_complete_entry() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@article{foo, author = {foo}}")])
+ .main("main.bib")
+ .line(0)
+ .character(3)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_entry_types(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(1.into(), 8.into()));
+ }
+ }
+
+ #[test]
+ fn test_complete_entry_field() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@article{foo, author = {foo}}")])
+ .main("main.bib")
+ .line(0)
+ .character(17)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_entry_types(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+}
diff --git a/support/texlab/src/features/completion/field.rs b/support/texlab/src/features/completion/field.rs
new file mode 100644
index 0000000000..d7297b481b
--- /dev/null
+++ b/support/texlab/src/features/completion/field.rs
@@ -0,0 +1,200 @@
+use cancellation::CancellationToken;
+use cstree::TextRange;
+use lsp_types::CompletionParams;
+
+use crate::{
+ features::cursor::CursorContext,
+ syntax::{bibtex, CstNode},
+ LANGUAGE_DATA,
+};
+
+use super::types::{InternalCompletionItem, InternalCompletionItemData};
+
+pub fn complete_fields<'a>(
+ context: &'a CursorContext<CompletionParams>,
+ items: &mut Vec<InternalCompletionItem<'a>>,
+ cancellation_token: &CancellationToken,
+) -> Option<()> {
+ cancellation_token.result().ok()?;
+
+ let token = context.cursor.as_bibtex()?;
+ let range = if token.kind() == bibtex::WORD {
+ token.text_range()
+ } else {
+ TextRange::empty(context.offset)
+ };
+
+ let parent = token.parent();
+ if let Some(entry) = bibtex::Entry::cast(parent) {
+ if entry.key()?.text_range() == token.text_range() {
+ return None;
+ }
+ } else {
+ bibtex::Field::cast(parent)?;
+ }
+
+ for field in &LANGUAGE_DATA.fields {
+ let data = InternalCompletionItemData::Field { field };
+ let item = InternalCompletionItem::new(range, data);
+ items.push(item);
+ }
+ Some(())
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::features::testing::FeatureTester;
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_fields(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_fields(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_empty_entry_open() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@article{foo,")])
+ .main("main.bib")
+ .line(0)
+ .character(13)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_fields(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(13.into(), 13.into()));
+ }
+ }
+
+ #[test]
+ fn test_empty_entry_closed() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@article{foo,}")])
+ .main("main.bib")
+ .line(0)
+ .character(13)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_fields(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(13.into(), 13.into()));
+ }
+ }
+
+ #[test]
+ fn test_entry_field_name() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@article{foo, a")])
+ .main("main.bib")
+ .line(0)
+ .character(15)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_fields(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(14.into(), 15.into()));
+ }
+ }
+
+ #[test]
+ fn test_entry_field_value() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@article{foo, author = bar}")])
+ .main("main.bib")
+ .line(0)
+ .character(24)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_fields(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_entry_two_fields_name_closed() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@article{foo, author = bar, baz}")])
+ .main("main.bib")
+ .line(0)
+ .character(29)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_fields(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(28.into(), 31.into()));
+ }
+ }
+
+ #[test]
+ fn test_entry_two_fields_name_open() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@article{foo, author = bar, baz")])
+ .main("main.bib")
+ .line(0)
+ .character(29)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_fields(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(28.into(), 31.into()));
+ }
+ }
+}
diff --git a/support/texlab/src/features/completion/glossary_ref.rs b/support/texlab/src/features/completion/glossary_ref.rs
new file mode 100644
index 0000000000..4529129fd7
--- /dev/null
+++ b/support/texlab/src/features/completion/glossary_ref.rs
@@ -0,0 +1,141 @@
+use cancellation::CancellationToken;
+use cstree::TextRange;
+use lsp_types::CompletionParams;
+
+use crate::{
+ features::cursor::CursorContext,
+ syntax::{latex, CstNode},
+};
+
+use super::types::{InternalCompletionItem, InternalCompletionItemData};
+
+pub fn complete_glossary_entries<'a>(
+ context: &'a CursorContext<CompletionParams>,
+ items: &mut Vec<InternalCompletionItem<'a>>,
+ cancellation_token: &CancellationToken,
+) -> Option<()> {
+ cancellation_token.result().ok()?;
+
+ let token = context.cursor.as_latex()?;
+ let group = latex::CurlyGroupWord::cast(token.parent())
+ .filter(|group| context.is_inside_latex_curly(group))?;
+ latex::GlossaryEntryReference::cast(group.syntax().parent()?)?;
+ let range = if token.kind() == latex::WORD {
+ token.text_range()
+ } else {
+ TextRange::empty(context.offset)
+ };
+
+ for document in &context.request.subset.documents {
+ if let Some(data) = document.data.as_latex() {
+ for node in data.root.descendants() {
+ cancellation_token.result().ok()?;
+
+ if let Some(name) = latex::GlossaryEntryDefinition::cast(node)
+ .and_then(|entry| entry.name())
+ .and_then(|name| name.word())
+ .map(|name| name.text())
+ {
+ items.push(InternalCompletionItem::new(
+ range,
+ InternalCompletionItemData::GlossaryEntry { name },
+ ));
+ } else if let Some(name) = latex::AcronymDefinition::cast(node)
+ .and_then(|entry| entry.name())
+ .and_then(|name| name.word())
+ .map(|name| name.text())
+ {
+ items.push(InternalCompletionItem::new(
+ range,
+ InternalCompletionItemData::Acronym { name },
+ ));
+ }
+ }
+ }
+ }
+
+ Some(())
+}
+
+#[cfg(test)]
+mod tests {
+ use cstree::TextRange;
+
+ use crate::features::testing::FeatureTester;
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_glossary_entries(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_glossary_entries(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_simple() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\newacronym[longplural={Frames per Second}]{fpsLabel}{FPS}{Frame per Second}\n\\gls{f}")])
+ .main("main.tex")
+ .line(1)
+ .character(6)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_glossary_entries(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(82.into(), 83.into()));
+ }
+ }
+
+ #[test]
+ fn test_open_brace() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\newacronym[longplural={Frames per Second}]{fpsLabel}{FPS}{Frame per Second}\n\\gls{f")])
+ .main("main.tex")
+ .line(1)
+ .character(6)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_glossary_entries(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(82.into(), 83.into()));
+ }
+ }
+}
diff --git a/support/texlab/src/features/completion/import.rs b/support/texlab/src/features/completion/import.rs
new file mode 100644
index 0000000000..ac5f2e6e40
--- /dev/null
+++ b/support/texlab/src/features/completion/import.rs
@@ -0,0 +1,196 @@
+use cancellation::CancellationToken;
+use cstree::TextRange;
+use lsp_types::CompletionParams;
+use rustc_hash::FxHashSet;
+use smol_str::SmolStr;
+
+use crate::{
+ component_db::COMPONENT_DATABASE,
+ features::cursor::CursorContext,
+ syntax::{latex, CstNode},
+};
+
+use super::types::{InternalCompletionItem, InternalCompletionItemData};
+
+pub fn complete_imports<'a>(
+ context: &'a CursorContext<CompletionParams>,
+ items: &mut Vec<InternalCompletionItem<'a>>,
+ cancellation_token: &CancellationToken,
+) -> Option<()> {
+ let token = context.cursor.as_latex()?;
+ let range = if token.kind() == latex::WORD {
+ token.text_range()
+ } else {
+ TextRange::empty(context.offset)
+ };
+
+ let group = latex::CurlyGroupWordList::cast(token.parent())
+ .filter(|group| context.is_inside_latex_curly(group))?;
+
+ let (extension, mut factory): (
+ &str,
+ Box<dyn FnMut(SmolStr) -> InternalCompletionItemData<'a>>,
+ ) = match group.syntax().parent()?.kind() {
+ latex::PACKAGE_INCLUDE => (
+ "sty",
+ Box::new(|name| InternalCompletionItemData::Package { name }),
+ ),
+ latex::CLASS_INCLUDE => (
+ "cls",
+ Box::new(|name| InternalCompletionItemData::Class { name }),
+ ),
+ _ => return None,
+ };
+
+ let mut file_names = FxHashSet::default();
+ for file_name in COMPONENT_DATABASE
+ .components
+ .iter()
+ .flat_map(|comp| comp.file_names.iter())
+ .filter(|file_name| file_name.ends_with(extension))
+ {
+ cancellation_token.result().ok()?;
+ file_names.insert(file_name);
+ let stem = &file_name[0..file_name.len() - 4];
+ let data = factory(stem.into());
+ let item = InternalCompletionItem::new(range, data);
+ items.push(item);
+ }
+
+ let resolver = context.request.context.resolver.lock().unwrap();
+ for file_name in resolver
+ .files_by_name
+ .keys()
+ .filter(|file_name| file_name.ends_with(extension) && !file_names.contains(file_name))
+ {
+ cancellation_token.result().ok()?;
+
+ let stem = &file_name[0..file_name.len() - 4];
+ let data = factory(stem.into());
+ let item = InternalCompletionItem::new(range, data);
+ items.push(item);
+ }
+
+ Some(())
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::features::testing::FeatureTester;
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_imports(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_imports(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_latex_simple_package() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\usepackage{}")])
+ .main("main.tex")
+ .line(0)
+ .character(12)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_imports(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(12.into(), 12.into()));
+ }
+ }
+
+ #[test]
+ fn test_latex_open_brace_package() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\usepackage{ \\foo")])
+ .main("main.tex")
+ .line(0)
+ .character(12)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_imports(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(12.into(), 12.into()));
+ }
+ }
+
+ #[test]
+ fn test_latex_simple_class() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\documentclass{}")])
+ .main("main.tex")
+ .line(0)
+ .character(15)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_imports(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(15.into(), 15.into()));
+ }
+ }
+
+ #[test]
+ fn test_latex_open_brace_class() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\documentclass{ \\foo")])
+ .main("main.tex")
+ .line(0)
+ .character(15)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_imports(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(15.into(), 15.into()));
+ }
+ }
+}
diff --git a/support/texlab/src/features/completion/include.rs b/support/texlab/src/features/completion/include.rs
new file mode 100644
index 0000000000..c711ea5ce7
--- /dev/null
+++ b/support/texlab/src/features/completion/include.rs
@@ -0,0 +1,181 @@
+use std::{
+ convert::TryFrom,
+ fs,
+ path::{Path, PathBuf},
+};
+
+use cancellation::CancellationToken;
+use cstree::{TextRange, TextSize};
+use lsp_types::CompletionParams;
+
+use crate::{
+ features::cursor::CursorContext,
+ syntax::{latex, CstNode},
+};
+
+use super::types::{InternalCompletionItem, InternalCompletionItemData};
+
+pub fn complete_includes<'a>(
+ context: &'a CursorContext<CompletionParams>,
+ items: &mut Vec<InternalCompletionItem<'a>>,
+ cancellation_token: &CancellationToken,
+) -> Option<()> {
+ cancellation_token.result().ok()?;
+ if context.request.main_document().uri.scheme() != "file" {
+ return None;
+ }
+
+ let token = context.cursor.as_latex()?;
+ let (path_text, path_range) = if token.kind() == latex::WORD {
+ (token.text(), token.text_range())
+ } else {
+ ("", TextRange::empty(context.offset))
+ };
+
+ let group = latex::CurlyGroupWordList::cast(token.parent())
+ .filter(|group| context.is_inside_latex_curly(group))?;
+ let include = group.syntax().parent()?;
+ let (include_extension, extensions): (bool, &[&str]) = match include.kind() {
+ latex::PACKAGE_INCLUDE => (false, &["sty"]),
+ latex::CLASS_INCLUDE => (false, &["cls"]),
+ latex::LATEX_INCLUDE => {
+ let include = latex::Include::cast(include)?;
+ (
+ matches!(include.command()?.text(), "\\input" | "\\subfile"),
+ &["tex"],
+ )
+ }
+ latex::BIBLATEX_INCLUDE => (true, &["bib"]),
+ latex::BIBTEX_INCLUDE => (false, &["bib"]),
+ latex::GRAPHICS_INCLUDE => (true, &["pdf", "png", "jpg", "jpeg", "bmp"]),
+ latex::SVG_INCLUDE => (true, &["svg"]),
+ latex::INKSCAPE_INCLUDE => (true, &["pdf", "eps", "ps", "png"]),
+ latex::VERBATIM_INCLUDE => (true, &[]),
+ _ => return None,
+ };
+
+ let segment_range = if path_text.is_empty() {
+ path_range
+ } else {
+ let start =
+ path_range.end() - TextSize::try_from(path_text.split('/').last()?.len()).ok()?;
+ TextRange::new(start, path_range.end())
+ };
+
+ let current_dir = current_dir(context, path_text)?;
+ for entry in fs::read_dir(current_dir).ok()?.filter_map(Result::ok) {
+ let mut path = entry.path();
+
+ let file_type = entry.file_type().ok()?;
+ if file_type.is_file() && is_included(&path, extensions) {
+ if !include_extension {
+ remove_extension(&mut path);
+ }
+ let name = path.file_name()?.to_str()?.into();
+ let data = InternalCompletionItemData::File { name };
+ let item = InternalCompletionItem::new(segment_range, data);
+ items.push(item);
+ } else if file_type.is_dir() {
+ let name = path.file_name()?.to_str()?.into();
+ let data = InternalCompletionItemData::Directory { name };
+ let item = InternalCompletionItem::new(segment_range, data);
+ items.push(item);
+ }
+ }
+
+ Some(())
+}
+
+fn current_dir(context: &CursorContext<CompletionParams>, path_text: &str) -> Option<PathBuf> {
+ let mut path = context
+ .request
+ .context
+ .options
+ .read()
+ .unwrap()
+ .root_directory
+ .as_ref()
+ .map(|root_directory| {
+ context
+ .request
+ .context
+ .current_directory
+ .join(root_directory)
+ })
+ .unwrap_or_else(|| {
+ let mut path = context.request.main_document().uri.to_file_path().unwrap();
+ path.pop();
+ path
+ });
+
+ path = PathBuf::from(path.to_str()?.replace('\\', "/"));
+ if !path_text.is_empty() {
+ path.push(&path_text);
+ if !path_text.ends_with('/') {
+ path.pop();
+ }
+ }
+ Some(path)
+}
+
+fn is_included(file: &Path, allowed_extensions: &[&str]) -> bool {
+ allowed_extensions.is_empty()
+ || file
+ .extension()
+ .and_then(|ext| ext.to_str())
+ .map(|ext| ext.to_lowercase())
+ .map(|ext| allowed_extensions.contains(&ext.as_str()))
+ .unwrap_or_default()
+}
+
+fn remove_extension(path: &mut PathBuf) {
+ if let Some(stem) = path
+ .file_stem()
+ .and_then(|stem| stem.to_str())
+ .map(ToOwned::to_owned)
+ {
+ path.pop();
+ path.push(stem);
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::features::testing::FeatureTester;
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_includes(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_includes(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+}
diff --git a/support/texlab/src/features/completion/label.rs b/support/texlab/src/features/completion/label.rs
new file mode 100644
index 0000000000..2f7e2d8f77
--- /dev/null
+++ b/support/texlab/src/features/completion/label.rs
@@ -0,0 +1,196 @@
+use cancellation::CancellationToken;
+use cstree::TextRange;
+use lsp_types::CompletionParams;
+
+use crate::{
+ features::{cursor::CursorContext, lsp_kinds::Structure},
+ render_label,
+ syntax::{latex, CstNode},
+ LabelledObject,
+};
+
+use super::types::{InternalCompletionItem, InternalCompletionItemData};
+
+pub fn complete_labels<'a>(
+ context: &'a CursorContext<CompletionParams>,
+ items: &mut Vec<InternalCompletionItem<'a>>,
+ cancellation_token: &CancellationToken,
+) -> Option<()> {
+ cancellation_token.result().ok()?;
+
+ let token = context.cursor.as_latex()?;
+ let is_math = latex::CurlyGroupWordList::cast(token.parent())
+ .filter(|group| context.is_inside_latex_curly(group))
+ .and_then(|group| group.syntax().parent())
+ .and_then(|reference| latex::LabelReference::cast(reference))
+ .and_then(|refernce| refernce.command())
+ .map(|reference| reference.text() == "\\eqref")
+ .or_else(|| {
+ latex::CurlyGroupWord::cast(token.parent())
+ .filter(|group| context.is_inside_latex_curly(group))
+ .and_then(|group| group.syntax().parent())
+ .and_then(|reference| latex::LabelReferenceRange::cast(reference))
+ .map(|_| false)
+ })?;
+
+ let range = if token.kind() == latex::WORD {
+ token.text_range()
+ } else {
+ TextRange::empty(context.offset)
+ };
+
+ for document in &context.request.subset.documents {
+ if let Some(data) = document.data.as_latex() {
+ for label in data
+ .root
+ .descendants()
+ .filter(|_| !cancellation_token.is_canceled())
+ .filter_map(latex::LabelDefinition::cast)
+ {
+ if let Some(name) = label
+ .name()
+ .and_then(|name| name.word())
+ .map(|name| name.text())
+ {
+ match render_label(&context.request.subset, name, Some(label)) {
+ Some(rendered_label) => {
+ let kind = match &rendered_label.object {
+ LabelledObject::Section { .. } => Structure::Section,
+ LabelledObject::Float { .. } => Structure::Float,
+ LabelledObject::Theorem { .. } => Structure::Theorem,
+ LabelledObject::Equation => Structure::Equation,
+ LabelledObject::EnumItem => Structure::Item,
+ };
+
+ if is_math && kind != Structure::Equation {
+ continue;
+ }
+
+ let header = rendered_label.detail();
+ let footer = match &rendered_label.object {
+ LabelledObject::Float { caption, .. } => Some(caption.clone()),
+ _ => None,
+ };
+
+ let text = format!("{} {}", name, rendered_label.reference());
+
+ let item = InternalCompletionItem::new(
+ range,
+ InternalCompletionItemData::Label {
+ name,
+ kind,
+ header,
+ footer,
+ text,
+ },
+ );
+ items.push(item);
+ }
+ None => {
+ let kind = Structure::Label;
+ let header = None;
+ let footer = None;
+ let text = name.to_string();
+ let item = InternalCompletionItem::new(
+ range,
+ InternalCompletionItemData::Label {
+ name,
+ kind,
+ header,
+ footer,
+ text,
+ },
+ );
+ items.push(item);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Some(())
+}
+
+#[cfg(test)]
+mod tests {
+ use cstree::TextRange;
+
+ use crate::features::testing::FeatureTester;
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_labels(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_labels(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_simple() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\ref{}\\label{foo}")])
+ .main("main.tex")
+ .line(0)
+ .character(5)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_labels(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(5.into(), 5.into()));
+ }
+ }
+
+ #[test]
+ fn test_simple_range() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\crefrange{\n\\label{foo}")])
+ .main("main.tex")
+ .line(0)
+ .character(11)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_labels(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(11.into(), 11.into()));
+ }
+ }
+}
diff --git a/support/texlab/src/features/completion/theorem.rs b/support/texlab/src/features/completion/theorem.rs
new file mode 100644
index 0000000000..54db031a4b
--- /dev/null
+++ b/support/texlab/src/features/completion/theorem.rs
@@ -0,0 +1,152 @@
+use cancellation::CancellationToken;
+use cstree::TextRange;
+use lsp_types::CompletionParams;
+
+use crate::{
+ features::cursor::CursorContext,
+ syntax::{latex, CstNode},
+};
+
+use super::types::{InternalCompletionItem, InternalCompletionItemData};
+
+pub fn complete_theorem_environments<'a>(
+ context: &'a CursorContext<CompletionParams>,
+ items: &mut Vec<InternalCompletionItem<'a>>,
+ token: &CancellationToken,
+) -> Option<()> {
+ if token.is_canceled() {
+ return None;
+ }
+
+ let token = context.cursor.as_latex()?;
+ let group = latex::CurlyGroupWord::cast(token.parent())
+ .filter(|group| context.is_inside_latex_curly(group))?;
+ if !matches!(group.syntax().parent()?.kind(), latex::BEGIN | latex::END) {
+ return None;
+ }
+
+ let range = if token.kind() == latex::WORD {
+ token.text_range()
+ } else {
+ TextRange::empty(context.offset)
+ };
+
+ for document in &context.request.subset.documents {
+ if let Some(data) = document.data.as_latex() {
+ for environment in &data.extras.theorem_environments {
+ items.push(InternalCompletionItem::new(
+ range,
+ InternalCompletionItemData::UserEnvironment {
+ name: &environment.name,
+ },
+ ));
+ }
+ }
+ }
+
+ Some(())
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::features::testing::FeatureTester;
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_theorem_environments(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_theorem_environments(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_simple() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\begin{ \\newtheorem{lemma}{Lemma}")])
+ .main("main.tex")
+ .line(0)
+ .character(7)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_theorem_environments(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(7.into(), 7.into()));
+ }
+ }
+
+ #[test]
+ fn test_simple_end() {
+ let request = FeatureTester::builder()
+ .files(vec![(
+ "main.tex",
+ "\\newtheorem{lemma}{Lemma}\\begin{a}\n\\end{",
+ )])
+ .main("main.tex")
+ .line(1)
+ .character(5)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_theorem_environments(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(40.into(), 40.into()));
+ }
+ }
+
+ #[test]
+ fn test_simple_existing() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\begin{d}\\newtheorem{lemma}{Lemma}")])
+ .main("main.tex")
+ .line(0)
+ .character(7)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_theorem_environments(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(7.into(), 8.into()));
+ }
+ }
+}
diff --git a/support/texlab/src/features/completion/tikz_library.rs b/support/texlab/src/features/completion/tikz_library.rs
new file mode 100644
index 0000000000..da2a26cc9b
--- /dev/null
+++ b/support/texlab/src/features/completion/tikz_library.rs
@@ -0,0 +1,169 @@
+use cancellation::CancellationToken;
+use cstree::TextRange;
+use lsp_types::CompletionParams;
+
+use crate::{
+ features::cursor::CursorContext,
+ syntax::{latex, CstNode},
+ LANGUAGE_DATA,
+};
+
+use super::types::{InternalCompletionItem, InternalCompletionItemData};
+
+pub fn complete_tikz_libraries<'a>(
+ context: &'a CursorContext<CompletionParams>,
+ items: &mut Vec<InternalCompletionItem<'a>>,
+ cancellation_token: &CancellationToken,
+) -> Option<()> {
+ cancellation_token.result().ok()?;
+
+ let token = context.cursor.as_latex()?;
+ let range = if token.kind() == latex::WORD {
+ token.text_range()
+ } else {
+ TextRange::empty(context.offset)
+ };
+
+ let group = latex::CurlyGroupWordList::cast(token.parent())
+ .filter(|group| context.is_inside_latex_curly(group))?;
+ let import = latex::TikzLibraryImport::cast(group.syntax().parent()?)?;
+
+ if import.command()?.text() == "\\usepgflibrary" {
+ for name in &LANGUAGE_DATA.pgf_libraries {
+ items.push(InternalCompletionItem::new(
+ range,
+ InternalCompletionItemData::PgfLibrary { name },
+ ));
+ }
+ } else {
+ for name in &LANGUAGE_DATA.tikz_libraries {
+ items.push(InternalCompletionItem::new(
+ range,
+ InternalCompletionItemData::TikzLibrary { name },
+ ));
+ }
+ }
+
+ Some(())
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::features::testing::FeatureTester;
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_tikz_libraries(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_tikz_libraries(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_latex_simple_pgf() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\usepgflibrary{}")])
+ .main("main.tex")
+ .line(0)
+ .character(15)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_tikz_libraries(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(15.into(), 15.into()));
+ }
+ }
+
+ #[test]
+ fn test_latex_open_brace_pgf() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\usepgflibrary{")])
+ .main("main.tex")
+ .line(0)
+ .character(15)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_tikz_libraries(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(15.into(), 15.into()));
+ }
+ }
+
+ #[test]
+ fn test_latex_simple_tikz() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\usetikzlibrary{}")])
+ .main("main.tex")
+ .line(0)
+ .character(16)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_tikz_libraries(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(16.into(), 16.into()));
+ }
+ }
+
+ #[test]
+ fn test_latex_open_brace_tikz() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\usetikzlibrary{")])
+ .main("main.tex")
+ .line(0)
+ .character(16)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_tikz_libraries(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(!actual_items.is_empty());
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(16.into(), 16.into()));
+ }
+ }
+}
diff --git a/support/texlab/src/completion/types.rs b/support/texlab/src/features/completion/types.rs
index f1b272f21e..b58a155fc4 100644
--- a/support/texlab/src/completion/types.rs
+++ b/support/texlab/src/features/completion/types.rs
@@ -1,20 +1,21 @@
-use crate::{
- protocol::{Range, Uri},
- syntax::{BibtexEntryTypeDoc, BibtexFieldDoc, Structure},
-};
+use std::sync::Arc;
+
+use cstree::TextRange;
use serde::{Deserialize, Serialize};
-use std::borrow::Cow;
+use smol_str::SmolStr;
+
+use crate::{features::lsp_kinds::Structure, BibtexEntryTypeDoc, BibtexFieldDoc, Uri};
#[derive(Debug, Clone)]
-pub struct Item<'a> {
- pub range: Range,
- pub data: ItemData<'a>,
+pub struct InternalCompletionItem<'a> {
+ pub range: TextRange,
+ pub data: InternalCompletionItemData<'a>,
pub preselect: bool,
pub score: Option<i64>,
}
-impl<'a> Item<'a> {
- pub fn new(range: Range, data: ItemData<'a>) -> Self {
+impl<'a> InternalCompletionItem<'a> {
+ pub fn new(range: TextRange, data: InternalCompletionItemData<'a>) -> Self {
Self {
range,
data,
@@ -25,98 +26,102 @@ impl<'a> Item<'a> {
}
#[derive(Debug, Clone)]
-pub enum ItemData<'a> {
- ComponentCommand {
+pub enum InternalCompletionItemData<'a> {
+ EntryType {
+ ty: &'a BibtexEntryTypeDoc,
+ },
+ Field {
+ field: &'a BibtexFieldDoc,
+ },
+ Argument {
name: &'a str,
image: Option<&'a str>,
+ },
+ BeginCommand,
+ Citation {
+ uri: Arc<Uri>,
+ key: &'a str,
+ text: String,
+ ty: Structure,
+ },
+ ComponentCommand {
+ name: &'a SmolStr,
+ image: Option<&'a str>,
glyph: Option<&'a str>,
- file_names: &'a [String],
+ file_names: &'a [SmolStr],
},
ComponentEnvironment {
- name: &'a str,
- file_names: &'a [String],
+ name: &'a SmolStr,
+ file_names: &'a [SmolStr],
},
- UserCommand {
- name: &'a str,
+ Class {
+ name: SmolStr,
},
- UserEnvironment {
- name: &'a str,
+ Package {
+ name: SmolStr,
},
- Label {
+ Color {
name: &'a str,
- kind: Structure,
- header: Option<String>,
- footer: Option<String>,
- text: String,
},
- PgfLibrary {
+ ColorModel {
name: &'a str,
},
- TikzLibrary {
+ Acronym {
name: &'a str,
},
- Package {
- name: Cow<'a, str>,
- },
- Class {
- name: Cow<'a, str>,
+ GlossaryEntry {
+ name: &'a str,
},
File {
- name: String,
+ name: SmolStr,
},
Directory {
- name: String,
+ name: SmolStr,
},
- Citation {
- uri: &'a Uri,
- key: &'a str,
+ Label {
+ name: &'a str,
+ kind: Structure,
+ header: Option<String>,
+ footer: Option<String>,
text: String,
- ty: Structure,
},
- Argument {
+ UserCommand {
name: &'a str,
- image: Option<&'a str>,
},
- BeginCommand,
- Color {
+ UserEnvironment {
name: &'a str,
},
- ColorModel {
+ PgfLibrary {
name: &'a str,
},
- GlossaryEntry {
+ TikzLibrary {
name: &'a str,
},
- EntryType {
- ty: &'a BibtexEntryTypeDoc,
- },
- Field {
- field: &'a BibtexFieldDoc,
- },
}
-impl<'a> ItemData<'a> {
- pub fn label<'b: 'a>(&'b self) -> &'b str {
+impl<'a> InternalCompletionItemData<'a> {
+ pub fn label<'b: 'a>(&'b self) -> &'a str {
match self {
+ Self::EntryType { ty } => &ty.name,
+ Self::Field { field } => &field.name,
+ Self::Argument { name, .. } => name,
+ Self::BeginCommand => "begin",
+ Self::Citation { key, .. } => &key,
Self::ComponentCommand { name, .. } => name,
Self::ComponentEnvironment { name, .. } => name,
+ Self::Class { name } => name,
+ Self::Package { name } => name,
+ Self::Color { name } => name,
+ Self::ColorModel { name } => name,
+ Self::Acronym { name } => name,
+ Self::GlossaryEntry { name } => name,
+ Self::File { name } => name,
+ Self::Directory { name } => name,
+ Self::Label { name, .. } => name,
Self::UserCommand { name } => name,
Self::UserEnvironment { name } => name,
- Self::Label { name, .. } => name,
- Self::Class { name } => &name,
- Self::Package { name } => &name,
Self::PgfLibrary { name } => name,
Self::TikzLibrary { name } => name,
- Self::File { name } => &name,
- Self::Directory { name } => &name,
- Self::Citation { key, .. } => key,
- Self::Argument { name, .. } => name,
- Self::BeginCommand => "begin",
- Self::Color { name } => name,
- Self::ColorModel { name } => name,
- Self::GlossaryEntry { name } => name,
- Self::EntryType { ty } => &ty.name,
- Self::Field { field } => &field.name,
}
}
}
@@ -138,13 +143,8 @@ pub enum CompletionItemData {
Class,
EntryType,
FieldName,
- Citation { uri: Uri, key: String },
+ Citation { uri: Uri, key: SmolStr },
Argument,
+ Acronym,
GlossaryEntry,
}
-
-impl Into<serde_json::Value> for CompletionItemData {
- fn into(self) -> serde_json::Value {
- serde_json::to_value(self).unwrap()
- }
-}
diff --git a/support/texlab/src/features/completion/user_command.rs b/support/texlab/src/features/completion/user_command.rs
new file mode 100644
index 0000000000..8b4473f31c
--- /dev/null
+++ b/support/texlab/src/features/completion/user_command.rs
@@ -0,0 +1,100 @@
+use cancellation::CancellationToken;
+use lsp_types::CompletionParams;
+
+use crate::features::cursor::CursorContext;
+
+use super::types::{InternalCompletionItem, InternalCompletionItemData};
+
+pub fn complete_user_commands<'a>(
+ context: &'a CursorContext<CompletionParams>,
+ items: &mut Vec<InternalCompletionItem<'a>>,
+ cancellation_token: &CancellationToken,
+) -> Option<()> {
+ cancellation_token.result().ok()?;
+
+ let range = context.cursor.command_range(context.offset)?;
+ let token = context.cursor.as_latex()?;
+
+ for document in &context.request.subset.documents {
+ if let Some(data) = document.data.as_latex() {
+ for name in data
+ .extras
+ .command_names
+ .iter()
+ .filter(|name| name.as_str() != token.text())
+ .map(|name| &name[1..])
+ {
+ cancellation_token.result().ok()?;
+ items.push(InternalCompletionItem::new(
+ range,
+ InternalCompletionItemData::UserCommand { name },
+ ));
+ }
+ }
+ }
+
+ Some(())
+}
+
+#[cfg(test)]
+mod tests {
+ use cstree::TextRange;
+
+ use crate::features::testing::FeatureTester;
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_user_commands(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_user_commands(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_latex_simple() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "\\foo\\bar")])
+ .main("main.tex")
+ .line(0)
+ .character(4)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_user_commands(&context, &mut actual_items, CancellationToken::none());
+
+ assert_eq!(actual_items.len(), 1);
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(1.into(), 4.into()));
+ }
+ }
+}
diff --git a/support/texlab/src/features/completion/user_environment.rs b/support/texlab/src/features/completion/user_environment.rs
new file mode 100644
index 0000000000..a1a5701fac
--- /dev/null
+++ b/support/texlab/src/features/completion/user_environment.rs
@@ -0,0 +1,116 @@
+use cancellation::CancellationToken;
+use cstree::TextRange;
+use lsp_types::CompletionParams;
+
+use crate::{
+ features::cursor::CursorContext,
+ syntax::{latex, CstNode},
+};
+
+use super::types::{InternalCompletionItem, InternalCompletionItemData};
+
+pub fn complete_user_environments<'a>(
+ context: &'a CursorContext<CompletionParams>,
+ items: &mut Vec<InternalCompletionItem<'a>>,
+ cancellation_token: &CancellationToken,
+) -> Option<()> {
+ cancellation_token.result().ok()?;
+
+ let token = context.cursor.as_latex()?;
+ let group = latex::CurlyGroupWord::cast(token.parent())
+ .filter(|group| context.is_inside_latex_curly(group))?;
+ if !matches!(group.syntax().parent()?.kind(), latex::BEGIN | latex::END) {
+ return None;
+ }
+
+ let range = if token.kind() == latex::WORD {
+ token.text_range()
+ } else {
+ TextRange::empty(context.offset)
+ };
+
+ for document in &context.request.subset.documents {
+ if let Some(data) = document.data.as_latex() {
+ for name in data
+ .extras
+ .environment_names
+ .iter()
+ .filter(|name| name.as_str() != token.text())
+ {
+ cancellation_token.result().ok()?;
+ items.push(InternalCompletionItem::new(
+ range,
+ InternalCompletionItemData::UserEnvironment { name },
+ ));
+ }
+ }
+ }
+
+ Some(())
+}
+
+#[cfg(test)]
+mod tests {
+ use cstree::TextRange;
+
+ use crate::features::testing::FeatureTester;
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_user_environments(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_user_environments(&context, &mut actual_items, CancellationToken::none());
+
+ assert!(actual_items.is_empty());
+ }
+
+ #[test]
+ fn test_latex_simple() {
+ let request = FeatureTester::builder()
+ .files(vec![(
+ "main.tex",
+ "\\begin{foo}\\end{foo} \\begin{bar}\\end{bar}",
+ )])
+ .main("main.tex")
+ .line(0)
+ .character(7)
+ .build()
+ .completion();
+
+ let context = CursorContext::new(request);
+ let mut actual_items = Vec::new();
+ complete_user_environments(&context, &mut actual_items, CancellationToken::none());
+
+ assert_eq!(actual_items.len(), 1);
+ for item in actual_items {
+ assert_eq!(item.range, TextRange::new(7.into(), 10.into()));
+ }
+ }
+}
diff --git a/support/texlab/src/features/completion/util.rs b/support/texlab/src/features/completion/util.rs
new file mode 100644
index 0000000000..296d6d25d9
--- /dev/null
+++ b/support/texlab/src/features/completion/util.rs
@@ -0,0 +1,66 @@
+use lsp_types::{CompletionItemKind, CompletionParams, Documentation, MarkupContent, MarkupKind};
+use smol_str::SmolStr;
+
+use crate::features::FeatureRequest;
+
+pub fn component_detail(file_names: &[SmolStr]) -> String {
+ if file_names.is_empty() {
+ "built-in".to_owned()
+ } else {
+ file_names.join(", ")
+ }
+}
+
+pub fn image_documentation(
+ request: &FeatureRequest<CompletionParams>,
+ name: &str,
+ image: &str,
+) -> Option<Documentation> {
+ if supports_images(request) {
+ Some(Documentation::MarkupContent(MarkupContent {
+ kind: MarkupKind::Markdown,
+ value: format!(
+ "![{}](data:image/png;base64,{}|width=48,height=48)",
+ name, image
+ ),
+ }))
+ } else {
+ None
+ }
+}
+
+fn supports_images(request: &FeatureRequest<CompletionParams>) -> bool {
+ request
+ .context
+ .client_capabilities
+ .lock()
+ .unwrap()
+ .text_document
+ .as_ref()
+ .and_then(|cap| cap.completion.as_ref())
+ .and_then(|cap| cap.completion_item.as_ref())
+ .and_then(|cap| cap.documentation_format.as_ref())
+ .map_or(true, |formats| formats.contains(&MarkupKind::Markdown))
+}
+
+pub fn adjust_kind(
+ request: &FeatureRequest<CompletionParams>,
+ kind: CompletionItemKind,
+) -> CompletionItemKind {
+ if let Some(value_set) = request
+ .context
+ .client_capabilities
+ .lock()
+ .unwrap()
+ .text_document
+ .as_ref()
+ .and_then(|cap| cap.completion.as_ref())
+ .and_then(|cap| cap.completion_item_kind.as_ref())
+ .and_then(|cap| cap.value_set.as_ref())
+ {
+ if value_set.contains(&kind) {
+ return kind;
+ }
+ }
+ CompletionItemKind::Text
+}
diff --git a/support/texlab/src/features/cursor.rs b/support/texlab/src/features/cursor.rs
new file mode 100644
index 0000000000..19079b841b
--- /dev/null
+++ b/support/texlab/src/features/cursor.rs
@@ -0,0 +1,252 @@
+use cstree::{TextRange, TextSize};
+use lsp_types::{
+ CompletionParams, GotoDefinitionParams, HoverParams, Position, ReferenceParams, RenameParams,
+ TextDocumentPositionParams,
+};
+
+use crate::{
+ syntax::{bibtex, latex, CstNode},
+ DocumentData, LineIndexExt,
+};
+
+use super::FeatureRequest;
+
+#[derive(Debug)]
+pub enum Cursor {
+ Latex(latex::SyntaxToken),
+ Bibtex(bibtex::SyntaxToken),
+ Nothing,
+}
+
+impl Cursor {
+ pub fn new_latex(
+ left: Option<latex::SyntaxToken>,
+ right: Option<latex::SyntaxToken>,
+ ) -> Option<Self> {
+ let left = left?;
+ let right = right?;
+
+ if left.kind().is_command_name() {
+ return Some(Self::Latex(left));
+ }
+
+ if right.kind() == latex::WORD {
+ return Some(Self::Latex(right));
+ }
+
+ if left.kind() == latex::WORD {
+ return Some(Self::Latex(left));
+ }
+
+ if right.kind().is_command_name() {
+ return Some(Self::Latex(right));
+ }
+
+ Some(Self::Latex(right))
+ }
+
+ pub fn new_bibtex(
+ left: Option<bibtex::SyntaxToken>,
+ right: Option<bibtex::SyntaxToken>,
+ ) -> Option<Self> {
+ let left = left?;
+ let right = right?;
+
+ if right.kind().is_type() {
+ return Some(Self::Bibtex(right));
+ }
+
+ if left.kind().is_type() {
+ return Some(Self::Bibtex(left));
+ }
+
+ if left.kind() == bibtex::COMMAND_NAME {
+ return Some(Self::Bibtex(left));
+ }
+
+ if right.kind() == bibtex::WORD {
+ return Some(Self::Bibtex(right));
+ }
+
+ if left.kind() == bibtex::WORD {
+ return Some(Self::Bibtex(left));
+ }
+
+ if right.kind() == bibtex::COMMAND_NAME {
+ return Some(Self::Bibtex(right));
+ }
+
+ Some(Self::Bibtex(right))
+ }
+
+ pub fn as_latex(&self) -> Option<&latex::SyntaxToken> {
+ if let Self::Latex(v) = self {
+ Some(v)
+ } else {
+ None
+ }
+ }
+
+ pub fn as_bibtex(&self) -> Option<&bibtex::SyntaxToken> {
+ if let Self::Bibtex(v) = self {
+ Some(v)
+ } else {
+ None
+ }
+ }
+
+ pub fn command_range(&self, offset: TextSize) -> Option<TextRange> {
+ self.as_latex()
+ .filter(|token| token.kind().is_command_name())
+ .filter(|token| token.text_range().start() != offset)
+ .map(|token| token.text_range())
+ .map(|range| TextRange::new(range.start() + TextSize::from(1), range.end()))
+ .or_else(|| {
+ self.as_bibtex()
+ .filter(|token| token.kind() == bibtex::COMMAND_NAME)
+ .filter(|token| token.text_range().start() != offset)
+ .map(|token| token.text_range())
+ .map(|range| TextRange::new(range.start() + TextSize::from(1), range.end()))
+ })
+ }
+}
+
+pub struct CursorContext<P> {
+ pub request: FeatureRequest<P>,
+ pub cursor: Cursor,
+ pub offset: TextSize,
+}
+
+impl<P: HasPosition> CursorContext<P> {
+ pub fn new(request: FeatureRequest<P>) -> Self {
+ let main_document = request.main_document();
+ let offset = main_document
+ .line_index
+ .offset_lsp(request.params.position());
+
+ let cursor = match &main_document.data {
+ DocumentData::Latex(data) => {
+ let left = data.root.token_at_offset(offset).left_biased();
+ let right = data.root.token_at_offset(offset).right_biased();
+ Cursor::new_latex(left, right)
+ }
+ DocumentData::Bibtex(data) => {
+ let left = data.root.token_at_offset(offset).left_biased();
+ let right = data.root.token_at_offset(offset).right_biased();
+ Cursor::new_bibtex(left, right)
+ }
+ DocumentData::BuildLog(_) => None,
+ };
+
+ Self {
+ request,
+ cursor: cursor.unwrap_or(Cursor::Nothing),
+ offset,
+ }
+ }
+
+ pub fn is_inside_latex_curly<'a>(&self, group: &impl latex::HasCurly<'a>) -> bool {
+ group.small_range().contains(self.offset) || group.right_curly().is_none()
+ }
+
+ pub fn find_citation_key_word(&self) -> Option<(&str, TextRange)> {
+ let key = self
+ .cursor
+ .as_latex()
+ .filter(|token| token.kind() == latex::WORD)?;
+
+ let group = latex::CurlyGroupWordList::cast(key.parent())?;
+ latex::Citation::cast(group.syntax().parent()?)?;
+ Some((key.text(), key.text_range()))
+ }
+
+ pub fn find_citation_key_command(&self) -> Option<(&str, TextRange)> {
+ let command = self.cursor.as_latex()?;
+
+ let citation = latex::Citation::cast(command.parent())?;
+ let key = citation.key_list()?.words().next()?;
+ Some((key.text(), key.text_range()))
+ }
+
+ pub fn find_entry_key(&self) -> Option<(&str, TextRange)> {
+ let key = self
+ .cursor
+ .as_bibtex()
+ .filter(|token| token.kind() == bibtex::WORD)?;
+
+ bibtex::Entry::cast(key.parent())?;
+ Some((key.text(), key.text_range()))
+ }
+
+ pub fn find_label_name_word(&self) -> Option<(&str, TextRange)> {
+ let name = self
+ .cursor
+ .as_latex()
+ .filter(|token| token.kind() == latex::WORD)?;
+
+ if matches!(
+ name.parent().parent()?.kind(),
+ latex::LABEL_DEFINITION | latex::LABEL_REFERENCE | latex::LABEL_REFERENCE_RANGE
+ ) {
+ Some((name.text(), name.text_range()))
+ } else {
+ None
+ }
+ }
+
+ pub fn find_label_name_command(&self) -> Option<(&str, TextRange)> {
+ let node = self.cursor.as_latex()?.parent();
+ if let Some(label) = latex::LabelDefinition::cast(node) {
+ let name = label.name()?.word()?;
+ Some((name.text(), name.text_range()))
+ } else if let Some(label) = latex::LabelReference::cast(node) {
+ let name = label.name_list()?.words().next()?;
+ Some((name.text(), name.text_range()))
+ } else if let Some(label) = latex::LabelReferenceRange::cast(node) {
+ let name = label.from()?.word()?;
+ Some((name.text(), name.text_range()))
+ } else {
+ None
+ }
+ }
+}
+
+pub trait HasPosition {
+ fn position(&self) -> Position;
+}
+
+impl HasPosition for CompletionParams {
+ fn position(&self) -> Position {
+ self.text_document_position.position
+ }
+}
+
+impl HasPosition for TextDocumentPositionParams {
+ fn position(&self) -> Position {
+ self.position
+ }
+}
+
+impl HasPosition for RenameParams {
+ fn position(&self) -> Position {
+ self.text_document_position.position
+ }
+}
+
+impl HasPosition for ReferenceParams {
+ fn position(&self) -> Position {
+ self.text_document_position.position
+ }
+}
+
+impl HasPosition for HoverParams {
+ fn position(&self) -> Position {
+ self.text_document_position_params.position
+ }
+}
+
+impl HasPosition for GotoDefinitionParams {
+ fn position(&self) -> Position {
+ self.text_document_position_params.position
+ }
+}
diff --git a/support/texlab/src/features/definition.rs b/support/texlab/src/features/definition.rs
new file mode 100644
index 0000000000..c49f172399
--- /dev/null
+++ b/support/texlab/src/features/definition.rs
@@ -0,0 +1,29 @@
+mod command;
+mod document;
+mod entry;
+mod label;
+mod string;
+
+use cancellation::CancellationToken;
+use lsp_types::{GotoDefinitionParams, GotoDefinitionResponse};
+
+use self::{
+ command::goto_command_definition, document::goto_document_definition,
+ entry::goto_entry_definition, label::goto_label_definition, string::goto_string_definition,
+};
+
+use super::{cursor::CursorContext, FeatureRequest};
+
+pub fn goto_definition(
+ request: FeatureRequest<GotoDefinitionParams>,
+ cancellation_token: &CancellationToken,
+) -> Option<GotoDefinitionResponse> {
+ let context = CursorContext::new(request);
+ log::debug!("[Definition] Cursor: {:?}", context.cursor);
+ let links = goto_command_definition(&context, cancellation_token)
+ .or_else(|| goto_document_definition(&context, cancellation_token))
+ .or_else(|| goto_entry_definition(&context, cancellation_token))
+ .or_else(|| goto_label_definition(&context, cancellation_token))
+ .or_else(|| goto_string_definition(&context, cancellation_token))?;
+ Some(GotoDefinitionResponse::Link(links))
+}
diff --git a/support/texlab/src/features/definition/command.rs b/support/texlab/src/features/definition/command.rs
new file mode 100644
index 0000000000..a69796c33f
--- /dev/null
+++ b/support/texlab/src/features/definition/command.rs
@@ -0,0 +1,131 @@
+use cancellation::CancellationToken;
+use lsp_types::{GotoDefinitionParams, LocationLink};
+
+use crate::{
+ features::cursor::CursorContext,
+ syntax::{latex, CstNode},
+ LineIndexExt,
+};
+
+pub fn goto_command_definition(
+ context: &CursorContext<GotoDefinitionParams>,
+ cancellation_token: &CancellationToken,
+) -> Option<Vec<LocationLink>> {
+ let main_document = context.request.main_document();
+
+ let name = context
+ .cursor
+ .as_latex()
+ .filter(|token| token.kind().is_command_name())?;
+
+ let origin_selection_range = main_document
+ .line_index
+ .line_col_lsp_range(name.text_range());
+
+ for document in &context.request.subset.documents {
+ if let Some(data) = document.data.as_latex() {
+ for node in data.root.descendants() {
+ cancellation_token.result().ok()?;
+
+ if let Some(defintion) = latex::CommandDefinition::cast(node).filter(|def| {
+ def.name()
+ .and_then(|name| name.command())
+ .map(|name| name.text())
+ == Some(name.text())
+ }) {
+ let target_selection_range = document
+ .line_index
+ .line_col_lsp_range(defintion.name()?.command()?.text_range());
+
+ let target_range = document
+ .line_index
+ .line_col_lsp_range(defintion.small_range());
+
+ return Some(vec![LocationLink {
+ origin_selection_range: Some(origin_selection_range),
+ target_uri: document.uri.as_ref().clone().into(),
+ target_range,
+ target_selection_range,
+ }]);
+ }
+ }
+ }
+ }
+
+ None
+}
+
+#[cfg(test)]
+mod tests {
+ use indoc::indoc;
+ use lsp_types::Range;
+
+ use crate::{features::testing::FeatureTester, RangeExt};
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .definition();
+
+ let context = CursorContext::new(request);
+
+ let actual_links = goto_command_definition(&context, CancellationToken::none());
+
+ assert!(actual_links.is_none());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .definition();
+
+ let context = CursorContext::new(request);
+ let actual_links = goto_command_definition(&context, CancellationToken::none());
+
+ assert!(actual_links.is_none());
+ }
+
+ #[test]
+ fn test_command_definition() {
+ let tester = FeatureTester::builder()
+ .files(vec![(
+ "main.tex",
+ indoc! {
+ r#"
+ \DeclareMathOperator{\foo}{foo}
+ \foo
+ "#
+ },
+ )])
+ .main("main.tex")
+ .line(1)
+ .character(2)
+ .build();
+ let target_uri = tester.uri("main.tex").as_ref().clone().into();
+
+ let request = tester.definition();
+ let context = CursorContext::new(request);
+ let actual_links = goto_command_definition(&context, CancellationToken::none()).unwrap();
+
+ let expected_links = vec![LocationLink {
+ origin_selection_range: Some(Range::new_simple(1, 0, 1, 4)),
+ target_uri,
+ target_range: Range::new_simple(0, 0, 0, 31),
+ target_selection_range: Range::new_simple(0, 21, 0, 25),
+ }];
+
+ assert_eq!(actual_links, expected_links);
+ }
+}
diff --git a/support/texlab/src/features/definition/document.rs b/support/texlab/src/features/definition/document.rs
new file mode 100644
index 0000000000..b2b713f260
--- /dev/null
+++ b/support/texlab/src/features/definition/document.rs
@@ -0,0 +1,112 @@
+use cancellation::CancellationToken;
+use lsp_types::{GotoDefinitionParams, LocationLink, Range};
+
+use crate::{features::cursor::CursorContext, LineIndexExt, RangeExt};
+
+pub fn goto_document_definition(
+ context: &CursorContext<GotoDefinitionParams>,
+ cancellation_token: &CancellationToken,
+) -> Option<Vec<LocationLink>> {
+ let main_document = context.request.main_document();
+ if let Some(data) = main_document.data.as_latex() {
+ for include in data
+ .extras
+ .explicit_links
+ .iter()
+ .filter(|link| link.stem_range.contains_inclusive(context.offset))
+ {
+ for target in &include.targets {
+ cancellation_token.result().ok()?;
+
+ if context
+ .request
+ .subset
+ .documents
+ .iter()
+ .any(|document| document.uri.as_ref() == target.as_ref())
+ {
+ return Some(vec![LocationLink {
+ origin_selection_range: Some(
+ main_document
+ .line_index
+ .line_col_lsp_range(include.stem_range),
+ ),
+ target_uri: target.as_ref().clone().into(),
+ target_range: Range::new_simple(0, 0, 0, 0),
+ target_selection_range: Range::new_simple(0, 0, 0, 0),
+ }]);
+ }
+ }
+ }
+ }
+ None
+}
+
+#[cfg(test)]
+mod tests {
+ use lsp_types::Range;
+
+ use crate::{features::testing::FeatureTester, RangeExt};
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .definition();
+
+ let context = CursorContext::new(request);
+ let actual_links = goto_document_definition(&context, CancellationToken::none());
+
+ assert!(actual_links.is_none());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .definition();
+
+ let context = CursorContext::new(request);
+ let actual_links = goto_document_definition(&context, CancellationToken::none());
+
+ assert!(actual_links.is_none());
+ }
+
+ #[test]
+ fn test_simple() {
+ let tester = FeatureTester::builder()
+ .files(vec![
+ ("foo.tex", r#"\addbibresource{baz.bib}"#),
+ ("bar.bib", r#"@article{foo, bar = {baz}}"#),
+ ("baz.bib", r#"@article{foo, bar = {baz}}"#),
+ ])
+ .main("foo.tex")
+ .line(0)
+ .character(18)
+ .build();
+ let target_uri = tester.uri("baz.bib").as_ref().clone().into();
+
+ let request = tester.definition();
+ let context = CursorContext::new(request);
+ let actual_links = goto_document_definition(&context, CancellationToken::none()).unwrap();
+
+ let expected_links = vec![LocationLink {
+ origin_selection_range: Some(Range::new_simple(0, 16, 0, 23)),
+ target_uri,
+ target_range: Range::new_simple(0, 0, 0, 0),
+ target_selection_range: Range::new_simple(0, 0, 0, 0),
+ }];
+
+ assert_eq!(actual_links, expected_links);
+ }
+}
diff --git a/support/texlab/src/features/definition/entry.rs b/support/texlab/src/features/definition/entry.rs
new file mode 100644
index 0000000000..7f489ec848
--- /dev/null
+++ b/support/texlab/src/features/definition/entry.rs
@@ -0,0 +1,125 @@
+use cancellation::CancellationToken;
+use lsp_types::{GotoDefinitionParams, LocationLink};
+
+use crate::{
+ features::cursor::CursorContext,
+ syntax::{bibtex, latex, CstNode},
+ LineIndexExt,
+};
+
+pub fn goto_entry_definition(
+ context: &CursorContext<GotoDefinitionParams>,
+ cancellation_token: &CancellationToken,
+) -> Option<Vec<LocationLink>> {
+ let main_document = context.request.main_document();
+
+ let key = context
+ .cursor
+ .as_latex()
+ .filter(|token| token.kind() == latex::WORD)?;
+
+ latex::Citation::cast(key.parent().parent()?)?;
+
+ let origin_selection_range = main_document
+ .line_index
+ .line_col_lsp_range(key.text_range());
+
+ for document in &context.request.subset.documents {
+ if let Some(data) = document.data.as_bibtex() {
+ for entry in data.root.children().filter_map(bibtex::Entry::cast) {
+ cancellation_token.result().ok()?;
+
+ if let Some(key) = entry.key().filter(|k| k.text() == key.text()) {
+ return Some(vec![LocationLink {
+ origin_selection_range: Some(origin_selection_range),
+ target_uri: document.uri.as_ref().clone().into(),
+ target_selection_range: document
+ .line_index
+ .line_col_lsp_range(key.text_range()),
+ target_range: document.line_index.line_col_lsp_range(entry.small_range()),
+ }]);
+ }
+ }
+ }
+ }
+
+ None
+}
+
+#[cfg(test)]
+mod tests {
+ use indoc::indoc;
+ use lsp_types::Range;
+
+ use crate::{features::testing::FeatureTester, RangeExt};
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .definition();
+
+ let context = CursorContext::new(request);
+ let actual_links = goto_entry_definition(&context, CancellationToken::none());
+
+ assert!(actual_links.is_none());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .definition();
+
+ let context = CursorContext::new(request);
+ let actual_links = goto_entry_definition(&context, CancellationToken::none());
+
+ assert!(actual_links.is_none());
+ }
+
+ #[test]
+ fn test_simple() {
+ let tester = FeatureTester::builder()
+ .files(vec![
+ (
+ "foo.tex",
+ indoc!(
+ r#"
+ \addbibresource{baz.bib}
+ \cite{foo}
+ "#
+ ),
+ ),
+ ("bar.bib", r#"@article{foo, bar = {baz}}"#),
+ ("baz.bib", r#"@article{foo, bar = {baz}}"#),
+ ])
+ .main("foo.tex")
+ .line(1)
+ .character(6)
+ .build();
+ let target_uri = tester.uri("baz.bib").as_ref().clone().into();
+
+ let request = tester.definition();
+ let context = CursorContext::new(request);
+ let actual_links = goto_entry_definition(&context, CancellationToken::none()).unwrap();
+
+ let expected_links = vec![LocationLink {
+ origin_selection_range: Some(Range::new_simple(1, 6, 1, 9)),
+ target_uri,
+ target_range: Range::new_simple(0, 0, 0, 26),
+ target_selection_range: Range::new_simple(0, 9, 0, 12),
+ }];
+
+ assert_eq!(actual_links, expected_links);
+ }
+}
diff --git a/support/texlab/src/features/definition/label.rs b/support/texlab/src/features/definition/label.rs
new file mode 100644
index 0000000000..65f0981de0
--- /dev/null
+++ b/support/texlab/src/features/definition/label.rs
@@ -0,0 +1,80 @@
+use cancellation::CancellationToken;
+use lsp_types::{GotoDefinitionParams, LocationLink};
+
+use crate::{features::cursor::CursorContext, find_label_definition, render_label, LineIndexExt};
+
+pub fn goto_label_definition(
+ context: &CursorContext<GotoDefinitionParams>,
+ cancellation_token: &CancellationToken,
+) -> Option<Vec<LocationLink>> {
+ let main_document = context.request.main_document();
+
+ let (name_text, name_range) = context
+ .find_label_name_word()
+ .or_else(|| context.find_label_name_command())?;
+
+ let origin_selection_range = main_document.line_index.line_col_lsp_range(name_range);
+
+ for document in &context.request.subset.documents {
+ cancellation_token.result().ok()?;
+ if let Some(data) = document.data.as_latex() {
+ if let Some(definition) = find_label_definition(&data.root, name_text) {
+ let target_selection_range = definition.name()?.word()?.text_range();
+ let target_range =
+ render_label(&context.request.subset, name_text, Some(definition))
+ .map(|label| label.range)
+ .unwrap_or(target_selection_range);
+
+ return Some(vec![LocationLink {
+ origin_selection_range: Some(origin_selection_range),
+ target_uri: document.uri.as_ref().clone().into(),
+ target_range: document.line_index.line_col_lsp_range(target_range),
+ target_selection_range: document
+ .line_index
+ .line_col_lsp_range(target_selection_range),
+ }]);
+ }
+ }
+ }
+
+ None
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::features::testing::FeatureTester;
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .definition();
+
+ let context = CursorContext::new(request);
+ let actual_links = goto_label_definition(&context, CancellationToken::none());
+
+ assert!(actual_links.is_none());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .definition();
+
+ let context = CursorContext::new(request);
+ let actual_links = goto_label_definition(&context, CancellationToken::none());
+
+ assert!(actual_links.is_none());
+ }
+}
diff --git a/support/texlab/src/features/definition/string.rs b/support/texlab/src/features/definition/string.rs
new file mode 100644
index 0000000000..7162c8b722
--- /dev/null
+++ b/support/texlab/src/features/definition/string.rs
@@ -0,0 +1,176 @@
+use cancellation::CancellationToken;
+use lsp_types::{GotoDefinitionParams, LocationLink};
+
+use crate::{
+ features::cursor::CursorContext,
+ syntax::{bibtex, CstNode},
+ LineIndexExt,
+};
+
+pub fn goto_string_definition(
+ context: &CursorContext<GotoDefinitionParams>,
+ cancellation_token: &CancellationToken,
+) -> Option<Vec<LocationLink>> {
+ let main_document = context.request.main_document();
+
+ let data = main_document.data.as_bibtex()?;
+ let name = context
+ .cursor
+ .as_bibtex()
+ .filter(|token| token.kind() == bibtex::WORD)?;
+
+ bibtex::Token::cast(name.parent())?;
+
+ let origin_selection_range = main_document
+ .line_index
+ .line_col_lsp_range(name.text_range());
+
+ for string in data.root.children().filter_map(bibtex::String::cast) {
+ cancellation_token.result().ok()?;
+
+ if let Some(string_name) = string.name().filter(|n| n.text() == name.text()) {
+ return Some(vec![LocationLink {
+ origin_selection_range: Some(origin_selection_range),
+ target_uri: main_document.uri.as_ref().clone().into(),
+ target_selection_range: main_document
+ .line_index
+ .line_col_lsp_range(string_name.text_range()),
+ target_range: main_document
+ .line_index
+ .line_col_lsp_range(string.small_range()),
+ }]);
+ }
+ }
+
+ None
+}
+
+#[cfg(test)]
+mod tests {
+ use indoc::indoc;
+ use lsp_types::Range;
+
+ use crate::{features::testing::FeatureTester, RangeExt};
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .definition();
+
+ let context = CursorContext::new(request);
+ let actual_links = goto_string_definition(&context, CancellationToken::none());
+
+ assert!(actual_links.is_none());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .definition();
+
+ let context = CursorContext::new(request);
+ let actual_links = goto_string_definition(&context, CancellationToken::none());
+
+ assert!(actual_links.is_none());
+ }
+
+ #[test]
+ fn test_simple() {
+ let tester = FeatureTester::builder()
+ .files(vec![(
+ "main.bib",
+ indoc! {
+ r#"
+ @string{foo = {bar}}
+ @article{bar, author = foo}
+ "#
+ },
+ )])
+ .main("main.bib")
+ .line(1)
+ .character(24)
+ .build();
+ let target_uri = tester.uri("main.bib").as_ref().clone().into();
+
+ let request = tester.definition();
+ let context = CursorContext::new(request);
+ let actual_links = goto_string_definition(&context, CancellationToken::none()).unwrap();
+
+ let expected_links = vec![LocationLink {
+ origin_selection_range: Some(Range::new_simple(1, 23, 1, 26)),
+ target_uri,
+ target_range: Range::new_simple(0, 0, 0, 20),
+ target_selection_range: Range::new_simple(0, 8, 0, 11),
+ }];
+
+ assert_eq!(actual_links, expected_links);
+ }
+
+ #[test]
+ fn concat() {
+ let tester = FeatureTester::builder()
+ .files(vec![(
+ "main.bib",
+ indoc! {
+ r#"
+ @string{foo = {bar}}
+ @article{bar, author = foo # "bar"}
+ "#
+ },
+ )])
+ .main("main.bib")
+ .line(1)
+ .character(24)
+ .build();
+ let target_uri = tester.uri("main.bib").as_ref().clone().into();
+
+ let request = tester.definition();
+ let context = CursorContext::new(request);
+ let actual_links = goto_string_definition(&context, CancellationToken::none()).unwrap();
+
+ let expected_links = vec![LocationLink {
+ origin_selection_range: Some(Range::new_simple(1, 23, 1, 26)),
+ target_uri,
+ target_range: Range::new_simple(0, 0, 0, 20),
+ target_selection_range: Range::new_simple(0, 8, 0, 11),
+ }];
+
+ assert_eq!(actual_links, expected_links);
+ }
+
+ #[test]
+ fn test_field() {
+ let tester = FeatureTester::builder()
+ .files(vec![(
+ "main.bib",
+ indoc! {
+ r#"
+ @string{foo = {bar}}
+ @article{bar, author = foo}
+ "#
+ },
+ )])
+ .main("main.bib")
+ .line(1)
+ .character(18)
+ .build();
+
+ let request = tester.definition();
+ let context = CursorContext::new(request);
+ let actual_links = goto_string_definition(&context, CancellationToken::none());
+
+ assert!(actual_links.is_none());
+ }
+}
diff --git a/support/texlab/src/features/folding.rs b/support/texlab/src/features/folding.rs
new file mode 100644
index 0000000000..0c44b6aa61
--- /dev/null
+++ b/support/texlab/src/features/folding.rs
@@ -0,0 +1,237 @@
+use cancellation::CancellationToken;
+use lsp_types::{FoldingRange, FoldingRangeKind, FoldingRangeParams, Range};
+
+use crate::{
+ syntax::{bibtex, latex, CstNode},
+ DocumentData, LineIndexExt,
+};
+
+use super::FeatureRequest;
+
+pub fn find_foldings(
+ request: FeatureRequest<FoldingRangeParams>,
+ token: &CancellationToken,
+) -> Vec<FoldingRange> {
+ let mut foldings = Vec::new();
+ let main_document = request.main_document();
+ match &main_document.data {
+ DocumentData::Latex(data) => {
+ for node in data.root.descendants() {
+ if token.is_canceled() {
+ break;
+ }
+
+ if let Some(folding) = latex::Environment::cast(node)
+ .map(|node| node.small_range())
+ .or_else(|| latex::Section::cast(node).map(|node| node.small_range()))
+ .or_else(|| latex::EnumItem::cast(node).map(|node| node.small_range()))
+ .map(|node| main_document.line_index.line_col_lsp_range(node))
+ .map(create_range)
+ {
+ foldings.push(folding);
+ }
+ }
+ }
+ DocumentData::Bibtex(data) => {
+ for node in data.root.descendants() {
+ if token.is_canceled() {
+ break;
+ }
+
+ if let Some(folding) = bibtex::Preamble::cast(node)
+ .map(|node| node.small_range())
+ .or_else(|| bibtex::String::cast(node).map(|node| node.small_range()))
+ .or_else(|| bibtex::Entry::cast(node).map(|node| node.small_range()))
+ .map(|node| main_document.line_index.line_col_lsp_range(node))
+ .map(create_range)
+ {
+ foldings.push(folding);
+ }
+ }
+ }
+ DocumentData::BuildLog(_) => {}
+ }
+ foldings
+}
+
+fn create_range(range: Range) -> FoldingRange {
+ FoldingRange {
+ start_line: range.start.line,
+ start_character: Some(range.start.character),
+ end_line: range.end.line,
+ end_character: Some(range.end.character),
+ kind: Some(FoldingRangeKind::Region),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use indoc::indoc;
+
+ use crate::features::testing::FeatureTester;
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .build()
+ .folding();
+
+ let actual_foldings = find_foldings(request, CancellationToken::none());
+ assert!(actual_foldings.is_empty());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .build()
+ .folding();
+
+ let actual_foldings = find_foldings(request, CancellationToken::none());
+ assert!(actual_foldings.is_empty());
+ }
+
+ #[test]
+ fn test_latex() {
+ let req = FeatureTester::builder()
+ .files(vec![(
+ "main.tex",
+ indoc! {r#"
+ \begin{document}
+ \section{Foo}
+ foo
+ \subsection{Bar}
+ bar
+ \section{Baz}
+ baz
+ \section{Qux}
+ \end{document}"# },
+ )])
+ .main("main.tex")
+ .build()
+ .folding();
+
+ let mut actual_foldings = find_foldings(req, CancellationToken::none());
+ actual_foldings.sort_by_key(|folding| (folding.start_line, folding.start_character));
+
+ assert_eq!(actual_foldings.len(), 5);
+ assert_eq!(
+ actual_foldings[0],
+ FoldingRange {
+ start_line: 0,
+ start_character: Some(0),
+ end_line: 8,
+ end_character: Some(14),
+ kind: Some(FoldingRangeKind::Region)
+ }
+ );
+ assert_eq!(
+ actual_foldings[1],
+ FoldingRange {
+ start_line: 1,
+ start_character: Some(4),
+ end_line: 4,
+ end_character: Some(7),
+ kind: Some(FoldingRangeKind::Region)
+ }
+ );
+ assert_eq!(
+ actual_foldings[2],
+ FoldingRange {
+ start_line: 3,
+ start_character: Some(4),
+ end_line: 4,
+ end_character: Some(7),
+ kind: Some(FoldingRangeKind::Region)
+ }
+ );
+ assert_eq!(
+ actual_foldings[3],
+ FoldingRange {
+ start_line: 5,
+ start_character: Some(4),
+ end_line: 6,
+ end_character: Some(7),
+ kind: Some(FoldingRangeKind::Region)
+ }
+ );
+ assert_eq!(
+ actual_foldings[4],
+ FoldingRange {
+ start_line: 7,
+ start_character: Some(4),
+ end_line: 7,
+ end_character: Some(17),
+ kind: Some(FoldingRangeKind::Region)
+ }
+ );
+ }
+
+ #[test]
+ fn test_bibtex() {
+ let request = FeatureTester::builder()
+ .files(vec![(
+ "main.bib",
+ indoc! {r#"
+ some junk
+ here
+
+ @article{foo,
+ author = {bar},
+ title = {baz}
+ }
+
+ @string{foo = "bar"}
+
+ @comment{foo,
+ author = {bar},
+ title = {baz}
+ }
+
+ @preamble{"foo"}"# },
+ )])
+ .main("main.bib")
+ .build()
+ .folding();
+
+ let mut actual_foldings = find_foldings(request, CancellationToken::none());
+ actual_foldings.sort_by_key(|folding| (folding.start_line, folding.start_character));
+
+ assert_eq!(actual_foldings.len(), 3);
+ assert_eq!(
+ actual_foldings[0],
+ FoldingRange {
+ start_line: 3,
+ start_character: Some(0),
+ end_line: 6,
+ end_character: Some(1),
+ kind: Some(FoldingRangeKind::Region)
+ }
+ );
+ assert_eq!(
+ actual_foldings[1],
+ FoldingRange {
+ start_line: 8,
+ start_character: Some(0),
+ end_line: 8,
+ end_character: Some(20),
+ kind: Some(FoldingRangeKind::Region)
+ }
+ );
+ assert_eq!(
+ actual_foldings[2],
+ FoldingRange {
+ start_line: 15,
+ start_character: Some(0),
+ end_line: 15,
+ end_character: Some(16),
+ kind: Some(FoldingRangeKind::Region)
+ }
+ );
+ }
+}
diff --git a/support/texlab/src/features/formatting.rs b/support/texlab/src/features/formatting.rs
new file mode 100644
index 0000000000..4492a0532d
--- /dev/null
+++ b/support/texlab/src/features/formatting.rs
@@ -0,0 +1,24 @@
+mod bibtex_internal;
+mod latexindent;
+
+use cancellation::CancellationToken;
+use lsp_types::{DocumentFormattingParams, TextEdit};
+
+use crate::BibtexFormatter;
+
+use self::{bibtex_internal::format_bibtex_internal, latexindent::format_with_latexindent};
+
+use super::FeatureRequest;
+
+pub fn format_source_code(
+ request: FeatureRequest<DocumentFormattingParams>,
+ cancellation_token: &CancellationToken,
+) -> Option<Vec<TextEdit>> {
+ let mut edits = None;
+ if request.context.options.read().unwrap().bibtex_formatter == BibtexFormatter::Texlab {
+ edits = edits.or_else(|| format_bibtex_internal(&request, cancellation_token));
+ }
+
+ edits = edits.or_else(|| format_with_latexindent(&request, cancellation_token));
+ edits
+}
diff --git a/support/texlab/src/features/formatting/bibtex_internal.rs b/support/texlab/src/features/formatting/bibtex_internal.rs
new file mode 100644
index 0000000000..8481f561e7
--- /dev/null
+++ b/support/texlab/src/features/formatting/bibtex_internal.rs
@@ -0,0 +1,381 @@
+use cancellation::CancellationToken;
+use cstree::NodeOrToken;
+use lsp_types::{DocumentFormattingParams, TextEdit};
+
+use crate::{
+ features::FeatureRequest,
+ syntax::{
+ bibtex::{self, HasType},
+ CstNode,
+ },
+ LineIndex, LineIndexExt,
+};
+
+pub fn format_bibtex_internal(
+ request: &FeatureRequest<DocumentFormattingParams>,
+ _cancellation_token: &CancellationToken,
+) -> Option<Vec<TextEdit>> {
+ let mut indent = String::new();
+ if request.params.options.insert_spaces {
+ for _ in 0..request.params.options.tab_size {
+ indent.push(' ');
+ }
+ } else {
+ indent.push('\t');
+ }
+
+ let line_length = {
+ request
+ .context
+ .options
+ .read()
+ .unwrap()
+ .formatter_line_length
+ .map(|value| {
+ if value <= 0 {
+ usize::MAX
+ } else {
+ value as usize
+ }
+ })
+ .unwrap_or(80)
+ };
+
+ let document = request.main_document();
+ let data = document.data.as_bibtex()?;
+ let mut edits = Vec::new();
+
+ for node in data.root.children() {
+ let range = if let Some(entry) = bibtex::Entry::cast(node) {
+ entry.small_range()
+ } else if let Some(string) = bibtex::String::cast(node) {
+ string.small_range()
+ } else if let Some(preamble) = bibtex::Preamble::cast(node) {
+ preamble.small_range()
+ } else {
+ continue;
+ };
+
+ let mut formatter = Formatter::new(
+ indent.clone(),
+ request.params.options.tab_size,
+ line_length,
+ &document.line_index,
+ );
+
+ formatter.visit_node(node);
+ edits.push(TextEdit {
+ range: document.line_index.line_col_lsp_range(range),
+ new_text: formatter.output,
+ });
+ }
+
+ Some(edits)
+}
+
+struct Formatter<'a> {
+ indent: String,
+ tab_size: u32,
+ line_length: usize,
+ output: String,
+ align: Vec<usize>,
+ line_index: &'a LineIndex,
+}
+
+impl<'a> Formatter<'a> {
+ fn new(indent: String, tab_size: u32, line_length: usize, line_index: &'a LineIndex) -> Self {
+ Self {
+ indent,
+ tab_size,
+ line_length,
+ output: String::new(),
+ align: Vec::new(),
+ line_index,
+ }
+ }
+
+ fn visit_token_lowercase(&mut self, token: &bibtex::SyntaxToken) {
+ self.output.push_str(&token.text().to_lowercase());
+ }
+
+ fn should_insert_space(
+ &self,
+ previous: &bibtex::SyntaxToken,
+ current: &bibtex::SyntaxToken,
+ ) -> bool {
+ let previous_range = self.line_index.line_col_lsp_range(previous.text_range());
+ let current_range = self.line_index.line_col_lsp_range(current.text_range());
+ previous_range.start.line != current_range.start.line
+ || previous_range.end.character < current_range.start.character
+ }
+
+ fn base_align(&self) -> usize {
+ self.output[self.output.rfind('\n').unwrap_or(0)..]
+ .chars()
+ .count()
+ }
+
+ fn visit_node(&mut self, parent: &bibtex::SyntaxNode) {
+ match parent.kind() {
+ bibtex::PREAMBLE => {
+ let preamble = bibtex::Preamble::cast(parent).unwrap();
+ self.visit_token_lowercase(preamble.ty().unwrap());
+ self.output.push('{');
+ if preamble.syntax().arity() > 0 {
+ self.align.push(self.base_align());
+ for node in preamble.syntax().children() {
+ self.visit_node(node);
+ }
+ self.output.push('}');
+ }
+ }
+ bibtex::STRING => {
+ let string = bibtex::String::cast(parent).unwrap();
+ self.visit_token_lowercase(string.ty().unwrap());
+ self.output.push('{');
+ if let Some(name) = string.name() {
+ self.output.push_str(name.text());
+ self.output.push_str(" = ");
+ if let Some(value) = string.value() {
+ self.align.push(self.base_align());
+ self.visit_node(value.syntax());
+ self.output.push('}');
+ }
+ }
+ }
+ bibtex::ENTRY => {
+ let entry = bibtex::Entry::cast(parent).unwrap();
+ self.visit_token_lowercase(entry.ty().unwrap());
+ self.output.push('{');
+ if let Some(key) = entry.key() {
+ self.output.push_str(key.text());
+ self.output.push(',');
+ self.output.push('\n');
+ for field in entry.fields() {
+ self.visit_node(field.syntax());
+ }
+ self.output.push('}');
+ }
+ }
+ bibtex::FIELD => {
+ let field = bibtex::Field::cast(parent).unwrap();
+ self.output.push_str(&self.indent);
+ let name = field.name().unwrap();
+ self.output.push_str(name.text());
+ self.output.push_str(" = ");
+ if let Some(value) = field.value() {
+ let count = name.text().chars().count();
+ self.align.push(self.tab_size as usize + count + 3);
+ self.visit_node(value.syntax());
+ self.output.push(',');
+ self.output.push('\n');
+ }
+ }
+ bibtex::VALUE => {
+ let tokens: Vec<_> = parent
+ .descendants_with_tokens()
+ .filter_map(|element| element.into_token())
+ .filter(|token| token.kind() != bibtex::WHITESPACE)
+ .collect();
+
+ self.output.push_str(tokens[0].text());
+
+ let align = self.align.pop().unwrap_or_default();
+ let mut length = align + tokens[0].text().chars().count();
+ for i in 1..tokens.len() {
+ let previous = tokens[i - 1];
+ let current = tokens[i];
+ let current_length = current.text().chars().count();
+
+ let insert_space = self.should_insert_space(previous, current);
+ let space_length = if insert_space { 1 } else { 0 };
+
+ if length + current_length + space_length > self.line_length {
+ self.output.push('\n');
+ self.output.push_str(self.indent.as_ref());
+ for _ in 0..=align - self.tab_size as usize {
+ self.output.push(' ');
+ }
+ length = align;
+ } else if insert_space {
+ self.output.push(' ');
+ length += 1;
+ }
+ self.output.push_str(current.text());
+ length += current_length;
+ }
+ }
+ bibtex::ROOT | bibtex::JUNK | bibtex::COMMENT => {
+ for element in parent.children_with_tokens() {
+ match element {
+ NodeOrToken::Token(token) => {
+ self.output.push_str(token.text());
+ }
+ NodeOrToken::Node(node) => {
+ self.visit_node(node);
+ }
+ }
+ }
+ }
+ _ => unreachable!(),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use insta::{assert_debug_snapshot, assert_display_snapshot};
+
+ use crate::features::testing::FeatureTester;
+
+ use super::*;
+
+ #[test]
+ fn test_wrap_long_lines() {
+ let request = FeatureTester::builder()
+ .files(vec![(
+ "main.bib",
+ "@article{foo, bar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.},}",
+ )])
+ .main("main.bib")
+ .build()
+ .formatting();
+
+ let edit = format_bibtex_internal(&request, CancellationToken::none())
+ .unwrap()
+ .pop()
+ .unwrap();
+
+ assert_display_snapshot!(edit.new_text);
+ }
+
+ #[test]
+ fn test_multiple_entries() {
+ let request = FeatureTester::builder()
+ .files(vec![(
+ "main.bib",
+ "@article{foo, bar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.},}\n\n@article{foo, bar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.},}",
+ )])
+ .main("main.bib")
+ .build()
+ .formatting();
+
+ let mut edits = format_bibtex_internal(&request, CancellationToken::none()).unwrap();
+ let edit2 = edits.pop().unwrap();
+ let edit1 = edits.pop().unwrap();
+
+ assert_debug_snapshot!((edit1.new_text, edit2.new_text));
+ }
+
+ #[test]
+ fn test_trailing_comma() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@article{foo, bar = baz}")])
+ .main("main.bib")
+ .build()
+ .formatting();
+
+ let edit = format_bibtex_internal(&request, CancellationToken::none())
+ .unwrap()
+ .pop()
+ .unwrap();
+
+ assert_display_snapshot!(edit.new_text);
+ }
+
+ #[test]
+ fn test_insert_braces() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@article{foo, bar = baz,")])
+ .main("main.bib")
+ .build()
+ .formatting();
+
+ let edit = format_bibtex_internal(&request, CancellationToken::none())
+ .unwrap()
+ .pop()
+ .unwrap();
+
+ assert_display_snapshot!(edit.new_text);
+ }
+
+ #[test]
+ fn test_command() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@article{foo, bar = \"\\baz\",}")])
+ .main("main.bib")
+ .build()
+ .formatting();
+
+ let edit = format_bibtex_internal(&request, CancellationToken::none())
+ .unwrap()
+ .pop()
+ .unwrap();
+
+ assert_display_snapshot!(edit.new_text);
+ }
+
+ #[test]
+ fn test_concatenation() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@article{foo, bar = \"baz\" # \"qux\"}")])
+ .main("main.bib")
+ .build()
+ .formatting();
+
+ let edit = format_bibtex_internal(&request, CancellationToken::none())
+ .unwrap()
+ .pop()
+ .unwrap();
+
+ assert_display_snapshot!(edit.new_text);
+ }
+
+ #[test]
+ fn test_parens() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@article(foo,)")])
+ .main("main.bib")
+ .build()
+ .formatting();
+
+ let edit = format_bibtex_internal(&request, CancellationToken::none())
+ .unwrap()
+ .pop()
+ .unwrap();
+
+ assert_display_snapshot!(edit.new_text);
+ }
+
+ #[test]
+ fn test_string() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@string{foo=\"bar\"}")])
+ .main("main.bib")
+ .build()
+ .formatting();
+
+ let edit = format_bibtex_internal(&request, CancellationToken::none())
+ .unwrap()
+ .pop()
+ .unwrap();
+
+ assert_display_snapshot!(edit.new_text);
+ }
+
+ #[test]
+ fn test_preamble() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@preamble{\n\"foo bar baz\"}")])
+ .main("main.bib")
+ .build()
+ .formatting();
+
+ let edit = format_bibtex_internal(&request, CancellationToken::none())
+ .unwrap()
+ .pop()
+ .unwrap();
+
+ assert_display_snapshot!(edit.new_text);
+ }
+}
diff --git a/support/texlab/src/features/formatting/latexindent.rs b/support/texlab/src/features/formatting/latexindent.rs
new file mode 100644
index 0000000000..f603728a5d
--- /dev/null
+++ b/support/texlab/src/features/formatting/latexindent.rs
@@ -0,0 +1,50 @@
+use std::{
+ io::{BufWriter, Write},
+ process::{Command, Stdio},
+};
+
+use cancellation::CancellationToken;
+use cstree::{TextLen, TextRange};
+use lsp_types::{DocumentFormattingParams, TextEdit};
+
+use crate::{features::FeatureRequest, LineIndexExt};
+
+pub fn format_with_latexindent(
+ request: &FeatureRequest<DocumentFormattingParams>,
+ _cancellation_token: &CancellationToken,
+) -> Option<Vec<TextEdit>> {
+ let document = request.main_document();
+
+ let current_dir = &request.context.current_directory;
+ let options = request.context.options.read().unwrap();
+ let current_dir = match &options.root_directory {
+ Some(root_directory) => current_dir.join(root_directory),
+ None => current_dir.clone(),
+ };
+ drop(options);
+
+ let mut process = Command::new("latexindent")
+ .arg("-l")
+ .current_dir(current_dir)
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::null())
+ .spawn()
+ .ok()?;
+
+ let stdin = process.stdin.take()?;
+ let mut stdin = BufWriter::new(stdin);
+ stdin.write_all(document.text.as_bytes()).ok()?;
+ drop(stdin);
+
+ let output = process.wait_with_output().ok()?;
+
+ let new_text = String::from_utf8_lossy(&output.stdout).into_owned();
+
+ Some(vec![TextEdit {
+ range: document
+ .line_index
+ .line_col_lsp_range(TextRange::new(0.into(), document.text.text_len())),
+ new_text,
+ }])
+}
diff --git a/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__command.snap b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__command.snap
new file mode 100644
index 0000000000..792e7be277
--- /dev/null
+++ b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__command.snap
@@ -0,0 +1,8 @@
+---
+source: src/features/formatting/bibtex_internal.rs
+expression: edit.new_text
+
+---
+@article{foo,
+ bar = "\baz",
+}
diff --git a/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__concatenation.snap b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__concatenation.snap
new file mode 100644
index 0000000000..8cd51dfa92
--- /dev/null
+++ b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__concatenation.snap
@@ -0,0 +1,8 @@
+---
+source: src/features/formatting/bibtex_internal.rs
+expression: edit.new_text
+
+---
+@article{foo,
+ bar = "baz" # "qux",
+}
diff --git a/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__insert_braces.snap b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__insert_braces.snap
new file mode 100644
index 0000000000..3be9b15e9d
--- /dev/null
+++ b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__insert_braces.snap
@@ -0,0 +1,8 @@
+---
+source: src/features/formatting/bibtex_internal.rs
+expression: edit.new_text
+
+---
+@article{foo,
+ bar = baz,
+}
diff --git a/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__multiple_entries.snap b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__multiple_entries.snap
new file mode 100644
index 0000000000..636b4b665c
--- /dev/null
+++ b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__multiple_entries.snap
@@ -0,0 +1,9 @@
+---
+source: src/features/formatting/bibtex_internal.rs
+expression: "(edit1.new_text, edit2.new_text)"
+
+---
+(
+ "@article{foo,\n\tbar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum\n\t dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet,\n\t consectetur adipiscing elit.},\n}",
+ "@article{foo,\n\tbar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum\n\t dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet,\n\t consectetur adipiscing elit.},\n}",
+)
diff --git a/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__parens.snap b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__parens.snap
new file mode 100644
index 0000000000..5aff28cb5f
--- /dev/null
+++ b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__parens.snap
@@ -0,0 +1,7 @@
+---
+source: src/features/formatting/bibtex_internal.rs
+expression: edit.new_text
+
+---
+@article{foo,
+}
diff --git a/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__preamble.snap b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__preamble.snap
new file mode 100644
index 0000000000..663569eb5b
--- /dev/null
+++ b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__preamble.snap
@@ -0,0 +1,6 @@
+---
+source: src/features/formatting/bibtex_internal.rs
+expression: edit.new_text
+
+---
+@preamble{"foo bar baz"}
diff --git a/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__string.snap b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__string.snap
new file mode 100644
index 0000000000..6d37a69e8e
--- /dev/null
+++ b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__string.snap
@@ -0,0 +1,6 @@
+---
+source: src/features/formatting/bibtex_internal.rs
+expression: edit.new_text
+
+---
+@string{foo = "bar"}
diff --git a/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__trailing_comma.snap b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__trailing_comma.snap
new file mode 100644
index 0000000000..3be9b15e9d
--- /dev/null
+++ b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__trailing_comma.snap
@@ -0,0 +1,8 @@
+---
+source: src/features/formatting/bibtex_internal.rs
+expression: edit.new_text
+
+---
+@article{foo,
+ bar = baz,
+}
diff --git a/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__wrap_long_lines.snap b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__wrap_long_lines.snap
new file mode 100644
index 0000000000..6c0ec724b5
--- /dev/null
+++ b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__wrap_long_lines.snap
@@ -0,0 +1,10 @@
+---
+source: src/features/formatting/bibtex_internal.rs
+expression: edit.new_text
+
+---
+@article{foo,
+ bar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum
+ dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet,
+ consectetur adipiscing elit.},
+}
diff --git a/support/texlab/src/features/forward_search.rs b/support/texlab/src/features/forward_search.rs
new file mode 100644
index 0000000000..39470a64ec
--- /dev/null
+++ b/support/texlab/src/features/forward_search.rs
@@ -0,0 +1,124 @@
+use std::{
+ io,
+ path::Path,
+ process::{Command, Stdio},
+};
+
+use cancellation::CancellationToken;
+use log::error;
+use lsp_types::TextDocumentPositionParams;
+use serde::{Deserialize, Serialize};
+use serde_repr::{Deserialize_repr, Serialize_repr};
+
+use super::FeatureRequest;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize_repr, Deserialize_repr)]
+#[repr(i32)]
+pub enum ForwardSearchStatus {
+ Success = 0,
+ Error = 1,
+ Failure = 2,
+ Unconfigured = 3,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
+pub struct ForwardSearchResult {
+ pub status: ForwardSearchStatus,
+}
+
+pub fn execute_forward_search(
+ request: FeatureRequest<TextDocumentPositionParams>,
+ _cancellation_token: &CancellationToken,
+) -> Option<ForwardSearchResult> {
+ let options = {
+ request
+ .context
+ .options
+ .read()
+ .unwrap()
+ .forward_search
+ .clone()
+ .unwrap_or_default()
+ };
+
+ if options.executable.is_none() || options.args.is_none() {
+ return Some(ForwardSearchResult {
+ status: ForwardSearchStatus::Unconfigured,
+ });
+ }
+
+ let root_document = request
+ .subset
+ .documents
+ .iter()
+ .find(|document| {
+ if let Some(data) = document.data.as_latex() {
+ data.extras.has_document_environment
+ && !data
+ .extras
+ .explicit_links
+ .iter()
+ .filter_map(|link| link.as_component_name())
+ .any(|name| name == "subfiles.cls")
+ } else {
+ false
+ }
+ })
+ .filter(|document| document.uri.scheme() == "file")?;
+
+ let data = root_document.data.as_latex()?;
+ let pdf_path = data
+ .extras
+ .implicit_links
+ .pdf
+ .iter()
+ .filter_map(|uri| uri.to_file_path().ok())
+ .find(|path| path.exists())?;
+
+ let tex_path = request.main_document().uri.to_file_path().ok()?;
+
+ let args: Vec<String> = options
+ .args
+ .unwrap()
+ .into_iter()
+ .flat_map(|arg| {
+ replace_placeholder(&tex_path, &pdf_path, request.params.position.line, arg)
+ })
+ .collect();
+
+ let status = match run_process(options.executable.unwrap(), args) {
+ Ok(()) => ForwardSearchStatus::Success,
+ Err(why) => {
+ error!("Unable to execute forward search: {}", why);
+ ForwardSearchStatus::Failure
+ }
+ };
+ Some(ForwardSearchResult { status })
+}
+
+fn replace_placeholder(
+ tex_file: &Path,
+ pdf_file: &Path,
+ line_number: u32,
+ argument: String,
+) -> Option<String> {
+ let result = if argument.starts_with('"') || argument.ends_with('"') {
+ argument
+ } else {
+ argument
+ .replace("%f", tex_file.to_str()?)
+ .replace("%p", pdf_file.to_str()?)
+ .replace("%l", &(line_number + 1).to_string())
+ };
+ Some(result)
+}
+
+fn run_process(executable: String, args: Vec<String>) -> io::Result<()> {
+ Command::new(executable)
+ .args(args)
+ .stdin(Stdio::null())
+ .stdout(Stdio::null())
+ .stderr(Stdio::null())
+ .status()?;
+ Ok(())
+}
diff --git a/support/texlab/src/features/highlight.rs b/support/texlab/src/features/highlight.rs
new file mode 100644
index 0000000000..e1aea6e2ad
--- /dev/null
+++ b/support/texlab/src/features/highlight.rs
@@ -0,0 +1,15 @@
+mod label;
+
+use cancellation::CancellationToken;
+use lsp_types::{DocumentHighlight, DocumentHighlightParams};
+
+use self::label::find_label_highlights;
+
+use super::FeatureRequest;
+
+pub fn find_document_highlights(
+ request: FeatureRequest<DocumentHighlightParams>,
+ token: &CancellationToken,
+) -> Option<Vec<DocumentHighlight>> {
+ find_label_highlights(&request, token)
+}
diff --git a/support/texlab/src/features/highlight/label.rs b/support/texlab/src/features/highlight/label.rs
new file mode 100644
index 0000000000..4c94180f2a
--- /dev/null
+++ b/support/texlab/src/features/highlight/label.rs
@@ -0,0 +1,167 @@
+use cancellation::CancellationToken;
+use lsp_types::{DocumentHighlight, DocumentHighlightKind, DocumentHighlightParams};
+
+use crate::{
+ features::FeatureRequest,
+ syntax::{latex, CstNode},
+ LineIndexExt,
+};
+
+pub fn find_label_highlights(
+ request: &FeatureRequest<DocumentHighlightParams>,
+ token: &CancellationToken,
+) -> Option<Vec<DocumentHighlight>> {
+ let main_document = request.main_document();
+
+ let offset = main_document
+ .line_index
+ .offset_lsp(request.params.text_document_position_params.position);
+
+ let data = main_document.data.as_latex()?;
+ let name = data.root.token_at_offset(offset).right_biased()?;
+ if name.kind() != latex::WORD {
+ return None;
+ }
+
+ if !matches!(
+ name.parent().parent()?.kind(),
+ latex::LABEL_DEFINITION | latex::LABEL_REFERENCE | latex::LABEL_REFERENCE_RANGE
+ ) {
+ return None;
+ }
+
+ let mut highlights = Vec::new();
+ for node in data.root.descendants() {
+ if token.is_canceled() {
+ return None;
+ }
+
+ if let Some(label_name) = latex::LabelDefinition::cast(node)
+ .and_then(|label| label.name())
+ .and_then(|label_name| label_name.word())
+ .filter(|label_name| label_name.text() == name.text())
+ {
+ let range = main_document
+ .line_index
+ .line_col_lsp_range(label_name.text_range());
+
+ highlights.push(DocumentHighlight {
+ range,
+ kind: Some(DocumentHighlightKind::Write),
+ });
+ } else if let Some(label) = latex::LabelReference::cast(node) {
+ for label_name in label
+ .name_list()
+ .into_iter()
+ .flat_map(|name| name.words())
+ .filter(|label_name| label_name.text() == name.text())
+ {
+ let range = main_document
+ .line_index
+ .line_col_lsp_range(label_name.text_range());
+
+ highlights.push(DocumentHighlight {
+ range,
+ kind: Some(DocumentHighlightKind::Read),
+ });
+ }
+ } else if let Some(label) = latex::LabelReferenceRange::cast(node) {
+ if let Some(label_name) = label
+ .from()
+ .and_then(|label_name| label_name.word())
+ .filter(|label_name| label_name.text() == name.text())
+ {
+ let range = main_document
+ .line_index
+ .line_col_lsp_range(label_name.text_range());
+
+ highlights.push(DocumentHighlight {
+ range,
+ kind: Some(DocumentHighlightKind::Read),
+ });
+ }
+
+ if let Some(label_name) = label
+ .to()
+ .and_then(|label_name| label_name.word())
+ .filter(|label_name| label_name.text() == name.text())
+ {
+ let range = main_document
+ .line_index
+ .line_col_lsp_range(label_name.text_range());
+
+ highlights.push(DocumentHighlight {
+ range,
+ kind: Some(DocumentHighlightKind::Read),
+ });
+ }
+ }
+ }
+
+ Some(highlights)
+}
+
+#[cfg(test)]
+mod tests {
+ use lsp_types::Range;
+
+ use crate::{features::testing::FeatureTester, RangeExt};
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .highlight();
+
+ let actual_links = find_label_highlights(&request, CancellationToken::none());
+
+ assert!(actual_links.is_none());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .highlight();
+
+ let actual_links = find_label_highlights(&request, CancellationToken::none());
+
+ assert!(actual_links.is_none());
+ }
+
+ #[test]
+ fn test_label() {
+ let tester = FeatureTester::builder()
+ .files(vec![("main.tex", "\\label{foo}\n\\ref{foo}\\label{bar}")])
+ .main("main.tex")
+ .line(0)
+ .character(7)
+ .build();
+ let request = tester.highlight();
+
+ let actual_highlights = find_label_highlights(&request, CancellationToken::none()).unwrap();
+
+ let expected_highlights = vec![
+ DocumentHighlight {
+ range: Range::new_simple(0, 7, 0, 10),
+ kind: Some(DocumentHighlightKind::Write),
+ },
+ DocumentHighlight {
+ range: Range::new_simple(1, 5, 1, 8),
+ kind: Some(DocumentHighlightKind::Read),
+ },
+ ];
+
+ assert_eq!(actual_highlights, expected_highlights);
+ }
+}
diff --git a/support/texlab/src/features/hover.rs b/support/texlab/src/features/hover.rs
new file mode 100644
index 0000000000..a78978bc33
--- /dev/null
+++ b/support/texlab/src/features/hover.rs
@@ -0,0 +1,43 @@
+#[cfg(feature = "citeproc")]
+mod citation;
+mod component;
+mod entry_type;
+mod field;
+mod label;
+mod string_ref;
+
+use cancellation::CancellationToken;
+use cfg_if::cfg_if;
+use lsp_types::{Hover, HoverParams};
+
+use crate::features::cursor::CursorContext;
+
+use self::{
+ component::find_component_hover, entry_type::find_entry_type_hover, field::find_field_hover,
+ label::find_label_hover, string_ref::find_string_reference_hover,
+};
+
+use super::FeatureRequest;
+
+pub fn find_hover(
+ request: FeatureRequest<HoverParams>,
+ cabcellation_token: &CancellationToken,
+) -> Option<Hover> {
+ let context = CursorContext::new(request);
+ log::debug!("[Hover] Cursor: {:?}", context.cursor);
+ let mut hover = find_label_hover(&context, cabcellation_token);
+
+ cfg_if! {
+ if #[cfg(feature = "citation")] {
+ hover = hover.or_else(|| self::citation::find_citation_hover(&context, cabcellation_token));
+ }
+ }
+
+ hover = hover
+ .or_else(|| find_component_hover(&context, cabcellation_token))
+ .or_else(|| find_string_reference_hover(&context, cabcellation_token))
+ .or_else(|| find_field_hover(&context, cabcellation_token))
+ .or_else(|| find_entry_type_hover(&context, cabcellation_token));
+
+ hover
+}
diff --git a/support/texlab/src/features/hover/citation.rs b/support/texlab/src/features/hover/citation.rs
new file mode 100644
index 0000000000..1fd6d6c06b
--- /dev/null
+++ b/support/texlab/src/features/hover/citation.rs
@@ -0,0 +1,133 @@
+use cancellation::CancellationToken;
+use lsp_types::{Hover, HoverContents, HoverParams};
+
+use crate::{citation, features::cursor::CursorContext, LineIndexExt};
+
+pub fn find_citation_hover(
+ context: &CursorContext<HoverParams>,
+ cancellation_token: &CancellationToken,
+) -> Option<Hover> {
+ cancellation_token.result().ok()?;
+ let main_document = context.request.main_document();
+
+ let (key_text, key_range) = context
+ .find_citation_key_word()
+ .or_else(|| context.find_citation_key_command())
+ .or_else(|| context.find_entry_key())?;
+
+ let contents = context
+ .request
+ .subset
+ .documents
+ .iter()
+ .find_map(|document| {
+ document
+ .data
+ .as_bibtex()
+ .and_then(|data| citation::render_citation(&data.root, &key_text))
+ })?;
+
+ Some(Hover {
+ range: Some(main_document.line_index.line_col_lsp_range(key_range)),
+ contents: HoverContents::Markup(contents),
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use lsp_types::{MarkupContent, MarkupKind, Range};
+
+ use crate::{features::testing::FeatureTester, RangeExt};
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_citation_hover(&context, CancellationToken::none());
+
+ assert_eq!(actual_hover, None);
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_citation_hover(&context, CancellationToken::none());
+
+ assert_eq!(actual_hover, None);
+ }
+
+ #[test]
+ fn test_inside_cite() {
+ let request = FeatureTester::builder()
+ .files(vec![
+ (
+ "main.bib",
+ "@article{foo, author = {Foo Bar}, title = {Baz Qux}, year = 1337}",
+ ),
+ ("main.tex", "\\addbibresource{main.bib}\n\\cite{foo}"),
+ ])
+ .main("main.tex")
+ .line(1)
+ .character(7)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_citation_hover(&context, CancellationToken::none()).unwrap();
+
+ let expected_hover = Hover {
+ contents: HoverContents::Markup(MarkupContent {
+ kind: MarkupKind::Markdown,
+ value: "Bar, F. (1337). *Baz Qux*.".into(),
+ }),
+ range: Some(Range::new_simple(1, 6, 1, 9)),
+ };
+ assert_eq!(actual_hover, expected_hover);
+ }
+
+ #[test]
+ fn test_inside_entry() {
+ let request = FeatureTester::builder()
+ .files(vec![
+ (
+ "main.bib",
+ "@article{foo, author = {Foo Bar}, title = {Baz Qux}, year = 1337}",
+ ),
+ ("main.tex", "\\addbibresource{main.bib}\n\\cite{foo}"),
+ ])
+ .main("main.bib")
+ .line(0)
+ .character(11)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_citation_hover(&context, CancellationToken::none()).unwrap();
+
+ let expected_hover = Hover {
+ contents: HoverContents::Markup(MarkupContent {
+ kind: MarkupKind::Markdown,
+ value: "Bar, F. (1337). *Baz Qux*.".into(),
+ }),
+ range: Some(Range::new_simple(0, 9, 0, 12)),
+ };
+ assert_eq!(actual_hover, expected_hover);
+ }
+}
diff --git a/support/texlab/src/features/hover/component.rs b/support/texlab/src/features/hover/component.rs
new file mode 100644
index 0000000000..abfdb781f1
--- /dev/null
+++ b/support/texlab/src/features/hover/component.rs
@@ -0,0 +1,105 @@
+use cancellation::CancellationToken;
+use lsp_types::{Hover, HoverContents, HoverParams};
+
+use crate::{
+ component_db::COMPONENT_DATABASE, features::cursor::CursorContext, syntax::latex, LineIndexExt,
+};
+
+pub fn find_component_hover(
+ context: &CursorContext<HoverParams>,
+ token: &CancellationToken,
+) -> Option<Hover> {
+ let main_document = context.request.main_document();
+ let data = main_document.data.as_latex()?;
+ for link in &data.extras.explicit_links {
+ if token.is_canceled() {
+ break;
+ }
+
+ if matches!(
+ link.kind,
+ latex::ExplicitLinkKind::Package | latex::ExplicitLinkKind::Class
+ ) && link.stem_range.contains_inclusive(context.offset)
+ {
+ let docs = COMPONENT_DATABASE.documentation(&link.stem)?;
+ return Some(Hover {
+ contents: HoverContents::Markup(docs),
+ range: Some(main_document.line_index.line_col_lsp_range(link.stem_range)),
+ });
+ }
+ }
+ None
+}
+
+#[cfg(test)]
+mod tests {
+ use lsp_types::Range;
+
+ use crate::{features::testing::FeatureTester, RangeExt};
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_component_hover(&context, CancellationToken::none());
+
+ assert_eq!(actual_hover, None);
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_component_hover(&context, CancellationToken::none());
+
+ assert_eq!(actual_hover, None);
+ }
+
+ #[test]
+ fn test_known_package() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", r#"\usepackage{amsmath}"#)])
+ .main("main.tex")
+ .line(0)
+ .character(15)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_component_hover(&context, CancellationToken::none()).unwrap();
+
+ assert_eq!(actual_hover.range.unwrap(), Range::new_simple(0, 12, 0, 19));
+ }
+
+ #[test]
+ fn test_unknown_class() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", r#"\documentclass{abcdefghijklmnop}"#)])
+ .main("main.tex")
+ .line(0)
+ .character(20)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_component_hover(&context, CancellationToken::none());
+
+ assert_eq!(actual_hover, None);
+ }
+}
diff --git a/support/texlab/src/features/hover/entry_type.rs b/support/texlab/src/features/hover/entry_type.rs
new file mode 100644
index 0000000000..e480b2f835
--- /dev/null
+++ b/support/texlab/src/features/hover/entry_type.rs
@@ -0,0 +1,128 @@
+use cancellation::CancellationToken;
+use lsp_types::{Hover, HoverContents, HoverParams, MarkupContent};
+
+use crate::{features::cursor::CursorContext, LineIndexExt, LANGUAGE_DATA};
+
+pub fn find_entry_type_hover(
+ context: &CursorContext<HoverParams>,
+ _token: &CancellationToken,
+) -> Option<Hover> {
+ let name = context
+ .cursor
+ .as_bibtex()
+ .filter(|token| token.kind().is_type())?;
+
+ let docs = LANGUAGE_DATA.entry_type_documentation(&name.text()[1..])?;
+ Some(Hover {
+ contents: HoverContents::Markup(MarkupContent {
+ kind: lsp_types::MarkupKind::Markdown,
+ value: docs.to_string(),
+ }),
+ range: Some(
+ context
+ .request
+ .main_document()
+ .line_index
+ .line_col_lsp_range(name.text_range()),
+ ),
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use lsp_types::{MarkupKind, Range};
+
+ use crate::{features::testing::FeatureTester, RangeExt};
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_entry_type_hover(&context, CancellationToken::none());
+
+ assert_eq!(actual_hover, None);
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_entry_type_hover(&context, CancellationToken::none());
+
+ assert_eq!(actual_hover, None);
+ }
+
+ #[test]
+ fn test_known_entry_type() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", r#"@article{foo,}"#)])
+ .main("main.bib")
+ .line(0)
+ .character(3)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_entry_type_hover(&context, CancellationToken::none()).unwrap();
+
+ let expected_hover = Hover {
+ contents: HoverContents::Markup(MarkupContent {
+ kind: MarkupKind::Markdown,
+ value: LANGUAGE_DATA
+ .entry_type_documentation("article")
+ .unwrap()
+ .into(),
+ }),
+ range: Some(Range::new_simple(0, 0, 0, 8)),
+ };
+ assert_eq!(actual_hover, expected_hover);
+ }
+
+ #[test]
+ fn test_unknown_field() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", r#"@foo{bar,}"#)])
+ .main("main.bib")
+ .line(0)
+ .character(3)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_entry_type_hover(&context, CancellationToken::none());
+
+ assert_eq!(actual_hover, None);
+ }
+
+ #[test]
+ fn test_entry_key() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", r#"@article{foo,}"#)])
+ .main("main.bib")
+ .line(0)
+ .character(11)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_entry_type_hover(&context, CancellationToken::none());
+
+ assert_eq!(actual_hover, None);
+ }
+}
diff --git a/support/texlab/src/features/hover/field.rs b/support/texlab/src/features/hover/field.rs
new file mode 100644
index 0000000000..54a8718d69
--- /dev/null
+++ b/support/texlab/src/features/hover/field.rs
@@ -0,0 +1,128 @@
+use cancellation::CancellationToken;
+use lsp_types::{Hover, HoverContents, HoverParams, MarkupContent};
+
+use crate::{
+ features::cursor::CursorContext,
+ syntax::{bibtex, CstNode},
+ LineIndexExt, LANGUAGE_DATA,
+};
+
+pub fn find_field_hover(
+ context: &CursorContext<HoverParams>,
+ _token: &CancellationToken,
+) -> Option<Hover> {
+ let main_document = context.request.main_document();
+
+ let name = context
+ .cursor
+ .as_bibtex()
+ .filter(|token| token.kind() == bibtex::WORD)?;
+
+ bibtex::Field::cast(name.parent())?;
+
+ let docs = LANGUAGE_DATA.field_documentation(&name.text())?;
+ Some(Hover {
+ contents: HoverContents::Markup(MarkupContent {
+ kind: lsp_types::MarkupKind::Markdown,
+ value: docs.to_string(),
+ }),
+ range: Some(
+ main_document
+ .line_index
+ .line_col_lsp_range(name.text_range()),
+ ),
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use lsp_types::{MarkupKind, Range};
+
+ use crate::{features::testing::FeatureTester, RangeExt};
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_field_hover(&context, CancellationToken::none());
+
+ assert_eq!(actual_hover, None);
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_field_hover(&context, CancellationToken::none());
+
+ assert_eq!(actual_hover, None);
+ }
+
+ #[test]
+ fn test_known_field() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", r#"@article{foo, author = bar}"#)])
+ .main("main.bib")
+ .line(0)
+ .character(15)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_field_hover(&context, CancellationToken::none()).unwrap();
+ let expected_hover = Hover {
+ contents: HoverContents::Markup(MarkupContent {
+ kind: MarkupKind::Markdown,
+ value: LANGUAGE_DATA.field_documentation("author").unwrap().into(),
+ }),
+ range: Some(Range::new_simple(0, 14, 0, 20)),
+ };
+ assert_eq!(actual_hover, expected_hover);
+ }
+
+ #[test]
+ fn test_unknown_field() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", r#"@article{foo, bar = baz}"#)])
+ .main("main.bib")
+ .line(0)
+ .character(15)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_field_hover(&context, CancellationToken::none());
+ assert_eq!(actual_hover, None);
+ }
+
+ #[test]
+ fn test_entry_key() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", r#"@article{foo, author = bar}"#)])
+ .main("main.bib")
+ .line(0)
+ .character(11)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_field_hover(&context, CancellationToken::none());
+ assert_eq!(actual_hover, None);
+ }
+}
diff --git a/support/texlab/src/features/hover/label.rs b/support/texlab/src/features/hover/label.rs
new file mode 100644
index 0000000000..79afe7d250
--- /dev/null
+++ b/support/texlab/src/features/hover/label.rs
@@ -0,0 +1,79 @@
+use cancellation::CancellationToken;
+use lsp_types::{Hover, HoverContents, HoverParams};
+
+use crate::{features::cursor::CursorContext, render_label, LineIndexExt};
+
+pub fn find_label_hover(
+ context: &CursorContext<HoverParams>,
+ _token: &CancellationToken,
+) -> Option<Hover> {
+ let main_document = context.request.main_document();
+
+ let (name_text, name_range) = context
+ .find_label_name_word()
+ .or_else(|| context.find_label_name_command())?;
+
+ let label = render_label(&context.request.subset, name_text, None)?;
+
+ Some(Hover {
+ range: Some(main_document.line_index.line_col_lsp_range(name_range)),
+ contents: HoverContents::Markup(label.documentation()),
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use lsp_types::Range;
+
+ use crate::{features::testing::FeatureTester, RangeExt};
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_label_hover(&context, CancellationToken::none());
+
+ assert_eq!(actual_hover, None);
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_label_hover(&context, CancellationToken::none());
+
+ assert_eq!(actual_hover, None);
+ }
+
+ #[test]
+ fn test_section() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", r#"\section{Foo}\label{sec:foo}"#)])
+ .main("main.tex")
+ .line(0)
+ .character(23)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_label_hover(&context, CancellationToken::none()).unwrap();
+
+ assert_eq!(actual_hover.range.unwrap(), Range::new_simple(0, 20, 0, 27));
+ }
+}
diff --git a/support/texlab/src/features/hover/string_ref.rs b/support/texlab/src/features/hover/string_ref.rs
new file mode 100644
index 0000000000..f6e7f1c873
--- /dev/null
+++ b/support/texlab/src/features/hover/string_ref.rs
@@ -0,0 +1,141 @@
+use cancellation::CancellationToken;
+use lsp_types::{Hover, HoverContents, HoverParams, MarkupContent, MarkupKind};
+
+use crate::{
+ features::cursor::CursorContext,
+ syntax::{bibtex, CstNode},
+ LineIndexExt,
+};
+
+pub fn find_string_reference_hover(
+ context: &CursorContext<HoverParams>,
+ cancellation_token: &CancellationToken,
+) -> Option<Hover> {
+ let main_document = context.request.main_document();
+ let data = main_document.data.as_bibtex()?;
+
+ let name = context
+ .cursor
+ .as_bibtex()
+ .filter(|token| token.kind() == bibtex::WORD)
+ .filter(|name| matches!(name.parent().kind(), bibtex::TOKEN | bibtex::STRING))?;
+
+ for string in data.root.children().filter_map(bibtex::String::cast) {
+ if cancellation_token.is_canceled() {
+ return None;
+ }
+
+ if string.name().filter(|n| n.text() == name.text()).is_some() {
+ let value = string.value()?.syntax().text().to_string();
+ return Some(Hover {
+ range: Some(
+ main_document
+ .line_index
+ .line_col_lsp_range(name.text_range()),
+ ),
+ contents: HoverContents::Markup(MarkupContent {
+ kind: MarkupKind::PlainText,
+ value,
+ }),
+ });
+ }
+ }
+
+ None
+}
+
+#[cfg(test)]
+mod tests {
+ use indoc::indoc;
+ use lsp_types::Range;
+
+ use crate::{features::testing::FeatureTester, RangeExt};
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_string_reference_hover(&context, CancellationToken::none());
+
+ assert_eq!(actual_hover, None);
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_string_reference_hover(&context, CancellationToken::none());
+
+ assert_eq!(actual_hover, None);
+ }
+
+ #[test]
+ fn test_inside_reference() {
+ let request = FeatureTester::builder()
+ .files(vec![(
+ "main.bib",
+ indoc! { r#"
+ @string{foo = "Foo"}
+ @string{bar = "Bar"}
+ @article{baz, author = bar}
+ "# },
+ )])
+ .main("main.bib")
+ .line(2)
+ .character(24)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover =
+ find_string_reference_hover(&context, CancellationToken::none()).unwrap();
+
+ let expected_hover = Hover {
+ contents: HoverContents::Markup(MarkupContent {
+ kind: MarkupKind::PlainText,
+ value: "\"Bar\"".into(),
+ }),
+ range: Some(Range::new_simple(2, 23, 2, 26)),
+ };
+
+ assert_eq!(actual_hover, expected_hover);
+ }
+
+ #[test]
+ fn test_inside_field() {
+ let request = FeatureTester::builder()
+ .files(vec![(
+ "main.bib",
+ indoc! { r#"
+ @string{foo = "Foo"}
+ @string{bar = "Bar"}
+ @article{baz, author = bar}
+ "# },
+ )])
+ .main("main.bib")
+ .line(2)
+ .character(20)
+ .build()
+ .hover();
+
+ let context = CursorContext::new(request);
+ let actual_hover = find_string_reference_hover(&context, CancellationToken::none());
+ assert_eq!(actual_hover, None);
+ }
+}
diff --git a/support/texlab/src/features/link.rs b/support/texlab/src/features/link.rs
new file mode 100644
index 0000000000..a6016c9f76
--- /dev/null
+++ b/support/texlab/src/features/link.rs
@@ -0,0 +1,115 @@
+use cancellation::CancellationToken;
+use lsp_types::{DocumentLink, DocumentLinkParams};
+
+use crate::LineIndexExt;
+
+use super::FeatureRequest;
+
+pub fn find_document_links(
+ request: FeatureRequest<DocumentLinkParams>,
+ cancellation_token: &CancellationToken,
+) -> Vec<DocumentLink> {
+ let mut links = Vec::new();
+ let main_document = request.main_document();
+ if let Some(data) = main_document.data.as_latex() {
+ for include in &data.extras.explicit_links {
+ for target in &include.targets {
+ if cancellation_token.is_canceled() {
+ return links;
+ }
+
+ if request
+ .subset
+ .documents
+ .iter()
+ .any(|document| document.uri.as_ref() == target.as_ref())
+ {
+ links.push(DocumentLink {
+ range: main_document
+ .line_index
+ .line_col_lsp_range(include.stem_range),
+ target: Some(target.as_ref().clone().into()),
+ tooltip: None,
+ data: None,
+ });
+ break;
+ }
+ }
+ }
+ }
+ links
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use lsp_types::Range;
+
+ use crate::{features::testing::FeatureTester, RangeExt};
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .build()
+ .link();
+
+ let items = find_document_links(request, CancellationToken::none());
+ assert!(items.is_empty());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .build()
+ .link();
+
+ let items = find_document_links(request, CancellationToken::none());
+ assert!(items.is_empty());
+ }
+
+ #[test]
+ fn test_includes() {
+ let tester = FeatureTester::builder()
+ .files(vec![("foo.tex", r#"\input{bar.tex}"#), ("bar.tex", r#""#)])
+ .main("foo.tex")
+ .build();
+ let target = tester.uri("bar.tex");
+
+ let actual_items = find_document_links(tester.link(), CancellationToken::none());
+
+ let expected_items = vec![DocumentLink {
+ range: Range::new_simple(0, 7, 0, 14),
+ target: Some(target.as_ref().clone().into()),
+ tooltip: None,
+ data: None,
+ }];
+ assert_eq!(actual_items, expected_items);
+ }
+
+ #[test]
+ fn test_imports() {
+ let tester = FeatureTester::builder()
+ .files(vec![
+ ("foo.tex", r#"\import{bar/}{baz}"#),
+ ("bar/baz.tex", r#""#),
+ ])
+ .main("foo.tex")
+ .build();
+ let target = tester.uri("bar/baz.tex");
+
+ let actual_items = find_document_links(tester.link(), CancellationToken::none());
+
+ let expected_items = vec![DocumentLink {
+ range: Range::new_simple(0, 14, 0, 17),
+ target: Some(target.as_ref().clone().into()),
+ tooltip: None,
+ data: None,
+ }];
+ assert_eq!(actual_items, expected_items);
+ }
+}
diff --git a/support/texlab/src/syntax/lsp_kind.rs b/support/texlab/src/features/lsp_kinds.rs
index 7d92fd4ee4..fbfc50cace 100644
--- a/support/texlab/src/syntax/lsp_kind.rs
+++ b/support/texlab/src/features/lsp_kinds.rs
@@ -1,5 +1,6 @@
-use super::lang_data::BibtexEntryTypeCategory;
-use crate::protocol::{CompletionItemKind, SymbolKind};
+use lsp_types::{CompletionItemKind, SymbolKind};
+
+use crate::BibtexEntryTypeCategory;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Structure {
diff --git a/support/texlab/src/features/reference.rs b/support/texlab/src/features/reference.rs
new file mode 100644
index 0000000000..14cf3bc297
--- /dev/null
+++ b/support/texlab/src/features/reference.rs
@@ -0,0 +1,25 @@
+mod entry;
+mod label;
+mod string;
+
+use cancellation::CancellationToken;
+use lsp_types::{Location, ReferenceParams};
+
+use self::{
+ entry::find_entry_references, label::find_label_references, string::find_string_references,
+};
+
+use super::{cursor::CursorContext, FeatureRequest};
+
+pub fn find_all_references(
+ request: FeatureRequest<ReferenceParams>,
+ cancellation_token: &CancellationToken,
+) -> Option<Vec<Location>> {
+ let mut references = Vec::new();
+ let context = CursorContext::new(request);
+ log::debug!("[References] Cursor: {:?}", context.cursor);
+ find_label_references(&context, cancellation_token, &mut references);
+ find_entry_references(&context, cancellation_token, &mut references);
+ find_string_references(&context, cancellation_token, &mut references);
+ Some(references)
+}
diff --git a/support/texlab/src/features/reference/entry.rs b/support/texlab/src/features/reference/entry.rs
new file mode 100644
index 0000000000..fbbcbdb4cc
--- /dev/null
+++ b/support/texlab/src/features/reference/entry.rs
@@ -0,0 +1,198 @@
+use cancellation::CancellationToken;
+use lsp_types::{Location, ReferenceParams};
+
+use crate::{
+ features::cursor::CursorContext,
+ syntax::{bibtex, latex, CstNode},
+ DocumentData, LineIndexExt,
+};
+
+pub fn find_entry_references(
+ context: &CursorContext<ReferenceParams>,
+ cancellation_token: &CancellationToken,
+ references: &mut Vec<Location>,
+) -> Option<()> {
+ let (key_text, _) = context
+ .find_citation_key_word()
+ .or_else(|| context.find_citation_key_command())
+ .or_else(|| context.find_entry_key())?;
+
+ for document in &context.request.subset.documents {
+ cancellation_token.result().ok()?;
+
+ match &document.data {
+ DocumentData::Latex(data) => {
+ data.root
+ .descendants()
+ .filter_map(latex::Citation::cast)
+ .filter_map(|citation| citation.key_list())
+ .flat_map(|keys| keys.words())
+ .filter(|key| key.text() == key_text)
+ .map(|key| document.line_index.line_col_lsp_range(key.text_range()))
+ .for_each(|range| {
+ references.push(Location::new(document.uri.as_ref().clone().into(), range));
+ });
+ }
+ DocumentData::Bibtex(data) if context.request.params.context.include_declaration => {
+ data.root
+ .children()
+ .filter_map(bibtex::Entry::cast)
+ .filter_map(|entry| entry.key())
+ .filter(|key| key.text() == key_text)
+ .map(|key| document.line_index.line_col_lsp_range(key.text_range()))
+ .for_each(|range| {
+ references.push(Location::new(document.uri.as_ref().clone().into(), range));
+ });
+ }
+ DocumentData::Bibtex(_) | DocumentData::BuildLog(_) => {}
+ }
+ }
+ Some(())
+}
+
+#[cfg(test)]
+mod tests {
+ use lsp_types::Range;
+
+ use crate::{features::testing::FeatureTester, RangeExt};
+
+ use super::*;
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .reference();
+
+ let mut actual_references = Vec::new();
+ let context = CursorContext::new(request);
+ find_entry_references(&context, CancellationToken::none(), &mut actual_references);
+
+ assert!(actual_references.is_empty());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .reference();
+
+ let mut actual_references = Vec::new();
+ let context = CursorContext::new(request);
+ find_entry_references(&context, CancellationToken::none(), &mut actual_references);
+
+ assert!(actual_references.is_empty());
+ }
+
+ #[test]
+ fn test_definition() {
+ let tester = FeatureTester::builder()
+ .files(vec![
+ ("foo.bib", r#"@article{foo,}"#),
+ ("bar.tex", r#"\cite{foo}\addbibresource{foo.bib}"#),
+ ])
+ .main("foo.bib")
+ .line(0)
+ .character(11)
+ .build();
+ let uri = tester.uri("bar.tex");
+ let mut actual_references = Vec::new();
+
+ let request = tester.reference();
+ let context = CursorContext::new(request);
+ find_entry_references(&context, CancellationToken::none(), &mut actual_references);
+
+ let expected_references = vec![Location::new(
+ uri.as_ref().clone().into(),
+ Range::new_simple(0, 6, 0, 9),
+ )];
+ assert_eq!(actual_references, expected_references);
+ }
+
+ #[test]
+ fn test_definition_include_declaration() {
+ let tester = FeatureTester::builder()
+ .files(vec![
+ ("foo.bib", r#"@article{foo,}"#),
+ ("bar.tex", r#"\cite{foo}\addbibresource{foo.bib}"#),
+ ])
+ .main("foo.bib")
+ .line(0)
+ .character(11)
+ .include_declaration(true)
+ .build();
+ let uri1 = tester.uri("foo.bib");
+ let uri2 = tester.uri("bar.tex");
+ let mut actual_references = Vec::new();
+
+ let request = tester.reference();
+ let context = CursorContext::new(request);
+ find_entry_references(&context, CancellationToken::none(), &mut actual_references);
+
+ let expected_references = vec![
+ Location::new(uri1.as_ref().clone().into(), Range::new_simple(0, 9, 0, 12)),
+ Location::new(uri2.as_ref().clone().into(), Range::new_simple(0, 6, 0, 9)),
+ ];
+ assert_eq!(actual_references, expected_references);
+ }
+
+ #[test]
+ fn test_reference() {
+ let tester = FeatureTester::builder()
+ .files(vec![
+ ("foo.bib", r#"@article{foo,}"#),
+ ("bar.tex", r#"\cite{foo}\addbibresource{foo.bib}"#),
+ ])
+ .main("bar.tex")
+ .line(0)
+ .character(8)
+ .build();
+ let uri = tester.uri("bar.tex");
+ let mut actual_references = Vec::new();
+
+ let request = tester.reference();
+ let context = CursorContext::new(request);
+ find_entry_references(&context, CancellationToken::none(), &mut actual_references);
+
+ let expected_references = vec![Location::new(
+ uri.as_ref().clone().into(),
+ Range::new_simple(0, 6, 0, 9),
+ )];
+ assert_eq!(actual_references, expected_references);
+ }
+
+ #[test]
+ fn test_reference_include_declaration() {
+ let tester = FeatureTester::builder()
+ .files(vec![
+ ("foo.bib", r#"@article{foo,}"#),
+ ("bar.tex", r#"\cite{foo}\addbibresource{foo.bib}"#),
+ ])
+ .main("bar.tex")
+ .line(0)
+ .character(6)
+ .include_declaration(true)
+ .build();
+ let uri1 = tester.uri("foo.bib");
+ let uri2 = tester.uri("bar.tex");
+ let mut actual_references = Vec::new();
+
+ let request = tester.reference();
+ let context = CursorContext::new(request);
+ find_entry_references(&context, CancellationToken::none(), &mut actual_references);
+
+ let expected_references = vec![
+ Location::new(uri2.as_ref().clone().into(), Range::new_simple(0, 6, 0, 9)),
+ Location::new(uri1.as_ref().clone().into(), Range::new_simple(0, 9, 0, 12)),
+ ];
+ assert_eq!(actual_references, expected_references);
+ }
+}
diff --git a/support/texlab/src/features/reference/label.rs b/support/texlab/src/features/reference/label.rs
new file mode 100644
index 0000000000..e9aa3a8510
--- /dev/null
+++ b/support/texlab/src/features/reference/label.rs
@@ -0,0 +1,187 @@
+use cancellation::CancellationToken;
+use lsp_types::{Location, ReferenceParams};
+
+use crate::{features::cursor::CursorContext, LineIndexExt};
+
+pub fn find_label_references(
+ context: &CursorContext<ReferenceParams>,
+ cancellation_token: &CancellationToken,
+ references: &mut Vec<Location>,
+) -> Option<()> {
+ cancellation_token.result().ok()?;
+
+ let (name_text, _) = context
+ .find_label_name_word()
+ .or_else(|| context.find_label_name_command())?;
+
+ for document in &context.request.subset.documents {
+ if let Some(data) = document.data.as_latex() {
+ cancellation_token.result().ok()?;
+ for name in data
+ .extras
+ .label_names
+ .iter()
+ .filter(|name| name.text == name_text)
+ .filter(|name| {
+ !name.is_definition || context.request.params.context.include_declaration
+ })
+ {
+ references.push(Location::new(
+ document.uri.as_ref().clone().into(),
+ document.line_index.line_col_lsp_range(name.range),
+ ));
+ }
+ }
+ }
+
+ Some(())
+}
+
+#[cfg(test)]
+mod tests {
+ use lsp_types::Range;
+
+ use crate::{features::testing::FeatureTester, RangeExt};
+
+ use super::*;
+
+ #[test]
+ fn test_definition() {
+ let tester = FeatureTester::builder()
+ .files(vec![
+ ("foo.tex", r#"\label{foo}"#),
+ ("bar.tex", r#"\ref{foo}\input{foo.tex}"#),
+ ])
+ .main("foo.tex")
+ .line(0)
+ .character(8)
+ .build();
+ let uri = tester.uri("bar.tex");
+ let mut actual_references = Vec::new();
+
+ let request = tester.reference();
+ let context = CursorContext::new(request);
+ find_label_references(&context, CancellationToken::none(), &mut actual_references);
+
+ let expected_references = vec![Location::new(
+ uri.as_ref().clone().into(),
+ Range::new_simple(0, 5, 0, 8),
+ )];
+ assert_eq!(actual_references, expected_references);
+ }
+
+ #[test]
+ fn test_definition_include_declaration() {
+ let tester = FeatureTester::builder()
+ .files(vec![
+ ("foo.tex", r#"\label{foo}\input{bar.tex}"#),
+ ("bar.tex", r#"\ref{foo}"#),
+ ])
+ .main("foo.tex")
+ .line(0)
+ .character(9)
+ .include_declaration(true)
+ .build();
+ let uri1 = tester.uri("foo.tex");
+ let uri2 = tester.uri("bar.tex");
+ let mut actual_references = Vec::new();
+
+ let request = tester.reference();
+ let context = CursorContext::new(request);
+ find_label_references(&context, CancellationToken::none(), &mut actual_references);
+
+ let expected_references = vec![
+ Location::new(uri1.as_ref().clone().into(), Range::new_simple(0, 7, 0, 10)),
+ Location::new(uri2.as_ref().clone().into(), Range::new_simple(0, 5, 0, 8)),
+ ];
+ assert_eq!(actual_references, expected_references);
+ }
+
+ #[test]
+ fn test_reference() {
+ let tester = FeatureTester::builder()
+ .files(vec![
+ ("foo.tex", r#"\label{foo}\input{bar.tex}"#),
+ ("bar.tex", r#"\ref{foo}"#),
+ ("baz.tex", r#"\ref{foo}\input{bar.tex}"#),
+ ])
+ .main("bar.tex")
+ .line(0)
+ .character(7)
+ .build();
+ let uri1 = tester.uri("bar.tex");
+ let uri2 = tester.uri("baz.tex");
+ let mut actual_references = Vec::new();
+
+ let request = tester.reference();
+ let context = CursorContext::new(request);
+ find_label_references(&context, CancellationToken::none(), &mut actual_references);
+
+ let expected_references = vec![
+ Location::new(uri1.as_ref().clone().into(), Range::new_simple(0, 5, 0, 8)),
+ Location::new(uri2.as_ref().clone().into(), Range::new_simple(0, 5, 0, 8)),
+ ];
+ assert_eq!(actual_references, expected_references);
+ }
+
+ #[test]
+ fn test_reference_include_declaration() {
+ let tester = FeatureTester::builder()
+ .files(vec![
+ ("foo.tex", r#"\label{foo}"#),
+ ("bar.tex", r#"\ref{foo}\input{foo.tex}"#),
+ ])
+ .main("bar.tex")
+ .line(0)
+ .character(7)
+ .include_declaration(true)
+ .build();
+ let uri1 = tester.uri("foo.tex");
+ let uri2 = tester.uri("bar.tex");
+ let mut actual_references = Vec::new();
+
+ let request = tester.reference();
+ let context = CursorContext::new(request);
+ find_label_references(&context, CancellationToken::none(), &mut actual_references);
+
+ let expected_references = vec![
+ Location::new(uri2.as_ref().clone().into(), Range::new_simple(0, 5, 0, 8)),
+ Location::new(uri1.as_ref().clone().into(), Range::new_simple(0, 7, 0, 10)),
+ ];
+ assert_eq!(actual_references, expected_references);
+ }
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .reference();
+ let mut actual_references = Vec::new();
+
+ let context = CursorContext::new(request);
+ find_label_references(&context, CancellationToken::none(), &mut actual_references);
+
+ assert!(actual_references.is_empty());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .reference();
+ let mut actual_references = Vec::new();
+
+ let context = CursorContext::new(request);
+ find_label_references(&context, CancellationToken::none(), &mut actual_references);
+
+ assert!(actual_references.is_empty());
+ }
+}
diff --git a/support/texlab/src/features/reference/string.rs b/support/texlab/src/features/reference/string.rs
new file mode 100644
index 0000000000..b7dd4e039a
--- /dev/null
+++ b/support/texlab/src/features/reference/string.rs
@@ -0,0 +1,206 @@
+use cancellation::CancellationToken;
+use lsp_types::{Location, ReferenceParams};
+
+use crate::{
+ features::cursor::CursorContext,
+ syntax::{bibtex, CstNode},
+ LineIndexExt,
+};
+
+pub fn find_string_references(
+ context: &CursorContext<ReferenceParams>,
+ cancellation_token: &CancellationToken,
+ items: &mut Vec<Location>,
+) -> Option<()> {
+ let name_text = context
+ .cursor
+ .as_bibtex()
+ .filter(|token| token.kind() == bibtex::WORD)
+ .filter(|token| matches!(token.parent().kind(), bibtex::TOKEN | bibtex::STRING))?
+ .text();
+
+ let document = context.request.main_document();
+ let data = document.data.as_bibtex()?;
+ for node in data.root.descendants() {
+ cancellation_token.result().ok()?;
+ if let Some(name) = bibtex::String::cast(node)
+ .and_then(|string| string.name())
+ .filter(|name| {
+ context.request.params.context.include_declaration && name.text() == name_text
+ })
+ .or_else(|| {
+ bibtex::Token::cast(node)
+ .and_then(|token| token.syntax().first_token())
+ .filter(|name| name.text() == name_text)
+ })
+ {
+ items.push(Location::new(
+ document.uri.as_ref().clone().into(),
+ document.line_index.line_col_lsp_range(name.text_range()),
+ ));
+ }
+ }
+ Some(())
+}
+
+#[cfg(test)]
+mod tests {
+ use indoc::indoc;
+ use lsp_types::Range;
+
+ use crate::{features::testing::FeatureTester, RangeExt};
+
+ use super::*;
+
+ #[test]
+ fn test_definition() {
+ let tester = FeatureTester::builder()
+ .files(vec![(
+ "main.bib",
+ indoc! { r#"
+ @string{foo = {Foo}}
+ @string{bar = {Bar}}
+ @article{baz, author = foo}
+ "# },
+ )])
+ .main("main.bib")
+ .line(2)
+ .character(24)
+ .build();
+ let uri = tester.uri("main.bib");
+
+ let mut actual_references = Vec::new();
+ let request = tester.reference();
+ let context = CursorContext::new(request);
+ find_string_references(&context, CancellationToken::none(), &mut actual_references);
+
+ let expected_references = vec![Location::new(
+ uri.as_ref().clone().into(),
+ Range::new_simple(2, 23, 2, 26),
+ )];
+ assert_eq!(actual_references, expected_references);
+ }
+
+ #[test]
+ fn test_definition_include_declaration() {
+ let tester = FeatureTester::builder()
+ .files(vec![(
+ "main.bib",
+ indoc! { r#"
+ @string{foo = {Foo}}
+ @string{bar = {Bar}}
+ @article{baz, author = foo}
+ "# },
+ )])
+ .main("main.bib")
+ .line(2)
+ .character(24)
+ .include_declaration(true)
+ .build();
+ let uri = tester.uri("main.bib");
+
+ let mut actual_references = Vec::new();
+ let request = tester.reference();
+ let context = CursorContext::new(request);
+ find_string_references(&context, CancellationToken::none(), &mut actual_references);
+
+ let expected_references = vec![
+ Location::new(uri.as_ref().clone().into(), Range::new_simple(0, 8, 0, 11)),
+ Location::new(uri.as_ref().clone().into(), Range::new_simple(2, 23, 2, 26)),
+ ];
+ assert_eq!(actual_references, expected_references);
+ }
+
+ #[test]
+ fn test_reference() {
+ let tester = FeatureTester::builder()
+ .files(vec![(
+ "main.bib",
+ indoc! { r#"
+ @string{foo = {Foo}}
+ @string{bar = {Bar}}
+ @article{baz, author = foo}
+ "# },
+ )])
+ .main("main.bib")
+ .line(0)
+ .character(10)
+ .build();
+ let uri = tester.uri("main.bib");
+
+ let mut actual_references = Vec::new();
+ let request = tester.reference();
+ let context = CursorContext::new(request);
+ find_string_references(&context, CancellationToken::none(), &mut actual_references);
+
+ let expected_references = vec![Location::new(
+ uri.as_ref().clone().into(),
+ Range::new_simple(2, 23, 2, 26),
+ )];
+ assert_eq!(actual_references, expected_references);
+ }
+
+ #[test]
+ fn test_reference_include_declaration() {
+ let tester = FeatureTester::builder()
+ .files(vec![(
+ "main.bib",
+ indoc! { r#"
+ @string{foo = {Foo}}
+ @string{bar = {Bar}}
+ @article{baz, author = foo}
+ "# },
+ )])
+ .main("main.bib")
+ .line(0)
+ .character(10)
+ .include_declaration(true)
+ .build();
+ let uri = tester.uri("main.bib");
+
+ let mut actual_references = Vec::new();
+ let request = tester.reference();
+ let context = CursorContext::new(request);
+ find_string_references(&context, CancellationToken::none(), &mut actual_references);
+
+ let expected_references = vec![
+ Location::new(uri.as_ref().clone().into(), Range::new_simple(0, 8, 0, 11)),
+ Location::new(uri.as_ref().clone().into(), Range::new_simple(2, 23, 2, 26)),
+ ];
+ assert_eq!(actual_references, expected_references);
+ }
+
+ #[test]
+ fn test_empty_latex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.tex", "")])
+ .main("main.tex")
+ .line(0)
+ .character(0)
+ .build()
+ .reference();
+
+ let mut actual_references = Vec::new();
+ let context = CursorContext::new(request);
+ find_string_references(&context, CancellationToken::none(), &mut actual_references);
+
+ assert!(actual_references.is_empty());
+ }
+
+ #[test]
+ fn test_empty_bibtex_document() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "")])
+ .main("main.bib")
+ .line(0)
+ .character(0)
+ .build()
+ .reference();
+
+ let mut actual_references = Vec::new();
+ let context = CursorContext::new(request);
+ find_string_references(&context, CancellationToken::none(), &mut actual_references);
+
+ assert!(actual_references.is_empty());
+ }
+}
diff --git a/support/texlab/src/features/rename.rs b/support/texlab/src/features/rename.rs
new file mode 100644
index 0000000000..ba09fa268c
--- /dev/null
+++ b/support/texlab/src/features/rename.rs
@@ -0,0 +1,34 @@
+mod command;
+mod entry;
+mod label;
+
+use cancellation::CancellationToken;
+use lsp_types::{Range, RenameParams, TextDocumentPositionParams, WorkspaceEdit};
+
+use self::{
+ command::{prepare_command_rename, rename_command},
+ entry::{prepare_entry_rename, rename_entry},
+ label::{prepare_label_rename, rename_label},
+};
+
+use super::{cursor::CursorContext, FeatureRequest};
+
+pub fn prepare_rename_all(
+ request: FeatureRequest<TextDocumentPositionParams>,
+ cancellation_token: &CancellationToken,
+) -> Option<Range> {
+ let context = CursorContext::new(request);
+ prepare_entry_rename(&context, cancellation_token)
+ .or_else(|| prepare_label_rename(&context, cancellation_token))
+ .or_else(|| prepare_command_rename(&context, cancellation_token))
+}
+
+pub fn rename_all(
+ request: FeatureRequest<RenameParams>,
+ cancellation_token: &CancellationToken,
+) -> Option<WorkspaceEdit> {
+ let context = CursorContext::new(request);
+ rename_entry(&context, cancellation_token)
+ .or_else(|| rename_label(&context, cancellation_token))
+ .or_else(|| rename_command(&context, cancellation_token))
+}
diff --git a/support/texlab/src/features/rename/command.rs b/support/texlab/src/features/rename/command.rs
new file mode 100644
index 0000000000..402783f3ae
--- /dev/null
+++ b/support/texlab/src/features/rename/command.rs
@@ -0,0 +1,98 @@
+use std::collections::HashMap;
+
+use cancellation::CancellationToken;
+use cstree::{TextRange, TextSize};
+use lsp_types::{Range, RenameParams, TextEdit, WorkspaceEdit};
+
+use crate::{
+ features::cursor::{CursorContext, HasPosition},
+ LineIndexExt,
+};
+
+pub fn prepare_command_rename<P: HasPosition>(
+ context: &CursorContext<P>,
+ _cancellation_token: &CancellationToken,
+) -> Option<Range> {
+ Some(
+ context
+ .request
+ .main_document()
+ .line_index
+ .line_col_lsp_range(context.cursor.command_range(context.offset)?),
+ )
+}
+
+pub fn rename_command(
+ context: &CursorContext<RenameParams>,
+ cancellation_token: &CancellationToken,
+) -> Option<WorkspaceEdit> {
+ cancellation_token.result().ok()?;
+ prepare_command_rename(context, cancellation_token)?;
+ let name = context.cursor.as_latex()?.text();
+ let mut changes = HashMap::new();
+ for document in &context.request.subset.documents {
+ cancellation_token.result().ok()?;
+
+ if let Some(data) = document.data.as_latex() {
+ let edits = data
+ .root
+ .descendants_with_tokens()
+ .filter_map(|element| element.into_token())
+ .filter(|token| token.kind().is_command_name() && token.text() == name)
+ .map(|token| {
+ let range = token.text_range();
+ let range = document.line_index.line_col_lsp_range(TextRange::new(
+ range.start() + TextSize::from(1),
+ range.end(),
+ ));
+ TextEdit::new(range, context.request.params.new_name.clone())
+ })
+ .collect();
+
+ changes.insert(document.uri.as_ref().clone().into(), edits);
+ }
+ }
+
+ Some(WorkspaceEdit::new(changes))
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::{features::testing::FeatureTester, RangeExt};
+
+ use super::*;
+
+ #[test]
+ fn test_command() {
+ let tester = FeatureTester::builder()
+ .files(vec![
+ ("foo.tex", r#"\baz\include{bar.tex}"#),
+ ("bar.tex", r#"\baz"#),
+ ])
+ .main("foo.tex")
+ .line(0)
+ .character(2)
+ .new_name("qux")
+ .build();
+
+ let uri1 = tester.uri("foo.tex");
+ let uri2 = tester.uri("bar.tex");
+ let req = tester.rename();
+
+ let context = CursorContext::new(req);
+ let actual_edit = rename_command(&context, CancellationToken::none()).unwrap();
+
+ let mut expected_changes = HashMap::new();
+ expected_changes.insert(
+ uri1.as_ref().clone().into(),
+ vec![TextEdit::new(Range::new_simple(0, 1, 0, 4), "qux".into())],
+ );
+ expected_changes.insert(
+ uri2.as_ref().clone().into(),
+ vec![TextEdit::new(Range::new_simple(0, 1, 0, 4), "qux".into())],
+ );
+ let expected_edit = WorkspaceEdit::new(expected_changes);
+
+ assert_eq!(actual_edit, expected_edit);
+ }
+}
diff --git a/support/texlab/src/features/rename/entry.rs b/support/texlab/src/features/rename/entry.rs
new file mode 100644
index 0000000000..14fbfd42d7
--- /dev/null
+++ b/support/texlab/src/features/rename/entry.rs
@@ -0,0 +1,161 @@
+use std::collections::HashMap;
+
+use cancellation::CancellationToken;
+use lsp_types::{Range, RenameParams, TextEdit, WorkspaceEdit};
+
+use crate::{
+ features::cursor::{Cursor, CursorContext, HasPosition},
+ syntax::{bibtex, latex, CstNode},
+ DocumentData, LineIndexExt,
+};
+
+pub fn prepare_entry_rename<P: HasPosition>(
+ context: &CursorContext<P>,
+ _cancellation_token: &CancellationToken,
+) -> Option<Range> {
+ let range = match &context.cursor {
+ Cursor::Latex(token) if token.kind() == latex::WORD => {
+ let group = latex::CurlyGroupWordList::cast(token.parent())?;
+ latex::Citation::cast(group.syntax().parent()?)?;
+ token.text_range()
+ }
+ Cursor::Bibtex(token) if token.kind() == bibtex::WORD => {
+ bibtex::Entry::cast(token.parent())?;
+ token.text_range()
+ }
+ _ => return None,
+ };
+
+ Some(
+ context
+ .request
+ .main_document()
+ .line_index
+ .line_col_lsp_range(range),
+ )
+}
+
+pub fn rename_entry(
+ context: &CursorContext<RenameParams>,
+ cancellation_token: &CancellationToken,
+) -> Option<WorkspaceEdit> {
+ cancellation_token.result().ok()?;
+ prepare_entry_rename(context, cancellation_token)?;
+ let key_text = context
+ .cursor
+ .as_latex()
+ .map(|token| token.text())
+ .or_else(|| context.cursor.as_bibtex().map(|token| token.text()))?;
+
+ let mut changes = HashMap::new();
+ for document in &context.request.subset.documents {
+ cancellation_token.result().ok()?;
+ match &document.data {
+ DocumentData::Latex(data) => {
+ let edits: Vec<_> = data
+ .root
+ .descendants()
+ .filter_map(latex::Citation::cast)
+ .filter_map(|citation| citation.key_list())
+ .flat_map(|keys| keys.words())
+ .filter(|key| key.text() == key_text)
+ .map(|key| document.line_index.line_col_lsp_range(key.text_range()))
+ .map(|range| TextEdit::new(range, context.request.params.new_name.clone()))
+ .collect();
+ changes.insert(document.uri.as_ref().clone().into(), edits);
+ }
+ DocumentData::Bibtex(data) => {
+ let edits: Vec<_> = data
+ .root
+ .descendants()
+ .filter_map(bibtex::Entry::cast)
+ .filter_map(|entry| entry.key())
+ .filter(|key| key.text() == key_text)
+ .map(|key| document.line_index.line_col_lsp_range(key.text_range()))
+ .map(|range| TextEdit::new(range, context.request.params.new_name.clone()))
+ .collect();
+ changes.insert(document.uri.as_ref().clone().into(), edits);
+ }
+ DocumentData::BuildLog(_) => {}
+ }
+ }
+
+ Some(WorkspaceEdit::new(changes))
+}
+
+#[cfg(test)]
+mod tests {
+ use lsp_types::TextEdit;
+
+ use crate::{features::testing::FeatureTester, RangeExt};
+
+ use super::*;
+
+ #[test]
+ fn test_entry() {
+ let tester = FeatureTester::builder()
+ .files(vec![
+ ("main.bib", r#"@article{foo, bar = baz}"#),
+ ("main.tex", "\\addbibresource{main.bib}\n\\cite{foo}"),
+ ])
+ .main("main.bib")
+ .line(0)
+ .character(9)
+ .new_name("qux")
+ .build();
+
+ let uri1 = tester.uri("main.bib");
+ let uri2 = tester.uri("main.tex");
+ let request = tester.rename();
+
+ let context = CursorContext::new(request);
+ let actual_edit = rename_entry(&context, CancellationToken::none()).unwrap();
+
+ let mut expected_changes = HashMap::new();
+ expected_changes.insert(
+ uri1.as_ref().clone().into(),
+ vec![TextEdit::new(Range::new_simple(0, 9, 0, 12), "qux".into())],
+ );
+ expected_changes.insert(
+ uri2.as_ref().clone().into(),
+ vec![TextEdit::new(Range::new_simple(1, 6, 1, 9), "qux".into())],
+ );
+ let expected_edit = WorkspaceEdit::new(expected_changes);
+
+ assert_eq!(actual_edit, expected_edit);
+ }
+
+ #[test]
+ fn test_citation() {
+ let tester = FeatureTester::builder()
+ .files(vec![
+ ("main.bib", r#"@article{foo, bar = baz}"#),
+ ("main.tex", "\\addbibresource{main.bib}\n\\cite{foo}"),
+ ])
+ .main("main.tex")
+ .line(1)
+ .character(6)
+ .new_name("qux")
+ .build();
+
+ let uri1 = tester.uri("main.bib");
+ let uri2 = tester.uri("main.tex");
+ let request = tester.rename();
+
+ let context = CursorContext::new(request);
+ let actual_edit = rename_entry(&context, CancellationToken::none()).unwrap();
+
+ let mut expected_changes = HashMap::new();
+ expected_changes.insert(
+ uri1.as_ref().clone().into(),
+ vec![TextEdit::new(Range::new_simple(0, 9, 0, 12), "qux".into())],
+ );
+ expected_changes.insert(
+ uri2.as_ref().clone().into(),
+ vec![TextEdit::new(Range::new_simple(1, 6, 1, 9), "qux".into())],
+ );
+ let expected_edit = WorkspaceEdit::new(expected_changes);
+
+ assert_eq!(actual_edit, expected_edit);
+ }
+}
diff --git a/support/texlab/src/features/rename/label.rs b/support/texlab/src/features/rename/label.rs
new file mode 100644
index 0000000000..11b414ccfa
--- /dev/null
+++ b/support/texlab/src/features/rename/label.rs
@@ -0,0 +1,142 @@
+use std::collections::HashMap;
+
+use cancellation::CancellationToken;
+use lsp_types::{Range, RenameParams, TextEdit, WorkspaceEdit};
+
+use crate::{
+ features::cursor::{CursorContext, HasPosition},
+ syntax::{latex, CstNode},
+ LineIndexExt,
+};
+
+pub fn prepare_label_rename<P: HasPosition>(
+ context: &CursorContext<P>,
+ _cancellation_token: &CancellationToken,
+) -> Option<Range> {
+ let name = context.cursor.as_latex()?;
+ name.parent().parent().filter(|node| {
+ matches!(
+ node.kind(),
+ latex::LABEL_DEFINITION | latex::LABEL_REFERENCE | latex::LABEL_REFERENCE_RANGE
+ )
+ })?;
+
+ Some(
+ context
+ .request
+ .main_document()
+ .line_index
+ .line_col_lsp_range(name.text_range()),
+ )
+}
+
+pub fn rename_label(
+ context: &CursorContext<RenameParams>,
+ cancellation_token: &CancellationToken,
+) -> Option<WorkspaceEdit> {
+ prepare_label_rename(context, cancellation_token)?;
+ let name_text = context.cursor.as_latex()?.text();
+ let mut changes = HashMap::new();
+ for document in &context.request.subset.documents {
+ cancellation_token.result().ok()?;
+ if let Some(data) = document.data.as_latex() {
+ let mut edits = Vec::new();
+ for node in data.root.descendants() {
+ if let Some(range) = latex::LabelDefinition::cast(node)
+ .and_then(|label| label.name())
+ .and_then(|name| name.word())
+ .filter(|name| name.text() == name_text)
+ .map(|name| document.line_index.line_col_lsp_range(name.text_range()))
+ {
+ edits.push(TextEdit::new(
+ range,
+ context.request.params.new_name.clone(),
+ ));
+ }
+
+ latex::LabelReference::cast(node)
+ .and_then(|label| label.name_list())
+ .into_iter()
+ .flat_map(|label| label.words())
+ .filter(|name| name.text() == name_text)
+ .map(|name| document.line_index.line_col_lsp_range(name.text_range()))
+ .for_each(|range| {
+ edits.push(TextEdit::new(
+ range,
+ context.request.params.new_name.clone(),
+ ));
+ });
+
+ if let Some(label) = latex::LabelReferenceRange::cast(node) {
+ if let Some(name1) = label
+ .from()
+ .and_then(|name| name.word())
+ .filter(|name| name.text() == name_text)
+ {
+ edits.push(TextEdit::new(
+ document.line_index.line_col_lsp_range(name1.text_range()),
+ context.request.params.new_name.clone(),
+ ));
+ }
+
+ if let Some(name2) = label
+ .from()
+ .and_then(|name| name.word())
+ .filter(|name| name.text() == name_text)
+ {
+ edits.push(TextEdit::new(
+ document.line_index.line_col_lsp_range(name2.text_range()),
+ context.request.params.new_name.clone(),
+ ));
+ }
+ }
+ }
+
+ changes.insert(document.uri.as_ref().clone().into(), edits);
+ }
+ }
+
+ Some(WorkspaceEdit::new(changes))
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::{features::testing::FeatureTester, RangeExt};
+
+ use super::*;
+
+ #[test]
+ fn test_label() {
+ let tester = FeatureTester::builder()
+ .files(vec![
+ ("foo.tex", r#"\label{foo}\include{bar}"#),
+ ("bar.tex", r#"\ref{foo}"#),
+ ("baz.tex", r#"\ref{foo}"#),
+ ])
+ .main("foo.tex")
+ .line(0)
+ .character(7)
+ .new_name("bar")
+ .build();
+
+ let uri1 = tester.uri("foo.tex");
+ let uri2 = tester.uri("bar.tex");
+ let request = tester.rename();
+
+ let context = CursorContext::new(request);
+ let actual_edit = rename_label(&context, CancellationToken::none()).unwrap();
+
+ let mut expected_changes = HashMap::new();
+ expected_changes.insert(
+ uri1.as_ref().clone().into(),
+ vec![TextEdit::new(Range::new_simple(0, 7, 0, 10), "bar".into())],
+ );
+ expected_changes.insert(
+ uri2.as_ref().clone().into(),
+ vec![TextEdit::new(Range::new_simple(0, 5, 0, 8), "bar".into())],
+ );
+ let expected_edit = WorkspaceEdit::new(expected_changes);
+
+ assert_eq!(actual_edit, expected_edit);
+ }
+}
diff --git a/support/texlab/src/features/semantic.rs b/support/texlab/src/features/semantic.rs
new file mode 100644
index 0000000000..8dd2674601
--- /dev/null
+++ b/support/texlab/src/features/semantic.rs
@@ -0,0 +1,21 @@
+mod bibtex;
+pub mod legend;
+
+use cancellation::CancellationToken;
+use lsp_types::{SemanticTokens, SemanticTokensRangeParams};
+
+use self::bibtex::find_bibtex_semantic_tokens_range;
+
+use super::FeatureRequest;
+
+pub fn find_semantic_tokens_range(
+ request: FeatureRequest<SemanticTokensRangeParams>,
+ cancellation_token: &CancellationToken,
+) -> Option<SemanticTokens> {
+ let mut data = Vec::new();
+ find_bibtex_semantic_tokens_range(&request, &mut data, cancellation_token);
+ Some(SemanticTokens {
+ result_id: None,
+ data,
+ })
+}
diff --git a/support/texlab/src/features/semantic/bibtex.rs b/support/texlab/src/features/semantic/bibtex.rs
new file mode 100644
index 0000000000..6d6a46b6ce
--- /dev/null
+++ b/support/texlab/src/features/semantic/bibtex.rs
@@ -0,0 +1,289 @@
+use cancellation::CancellationToken;
+use cstree::{NodeOrToken, TextRange};
+use lsp_types::{
+ SemanticToken, SemanticTokenModifier, SemanticTokenType, SemanticTokensRangeParams,
+};
+
+use crate::{
+ features::FeatureRequest,
+ syntax::{bibtex, CstNode},
+ BibtexEntryTypeCategory, LineIndex, LineIndexExt, LANGUAGE_DATA,
+};
+
+use super::legend::*;
+
+struct Context<'a> {
+ line_index: &'a LineIndex,
+ previous_line: u32,
+ previous_character: u32,
+ tokens: &'a mut Vec<SemanticToken>,
+ cancellation_token: &'a CancellationToken,
+}
+
+impl<'a> Context<'a> {
+ pub fn insert(
+ &mut self,
+ range: TextRange,
+ token_type: SemanticTokenType,
+ token_modifiers_bitset: ModifierSet,
+ ) {
+ let range = self.line_index.line_col_lsp_range(range);
+
+ let mut delta_line = range.start.line;
+ let mut delta_start = range.start.character;
+ if !self.tokens.is_empty() {
+ delta_line -= self.previous_line;
+ if delta_line == 0 {
+ delta_start -= self.previous_character;
+ }
+ }
+ self.tokens.push(SemanticToken {
+ delta_line,
+ delta_start,
+ length: range.end.character - range.start.character,
+ token_type: type_index(token_type),
+ token_modifiers_bitset: token_modifiers_bitset.0,
+ });
+
+ self.previous_line = range.start.line;
+ self.previous_character = range.start.character;
+ }
+}
+
+pub fn find_bibtex_semantic_tokens_range(
+ request: &FeatureRequest<SemanticTokensRangeParams>,
+ tokens: &mut Vec<SemanticToken>,
+ cancellation_token: &CancellationToken,
+) -> Option<()> {
+ let document = request.main_document();
+ let data = document.data.as_bibtex()?;
+
+ let mut context = Context {
+ line_index: &document.line_index,
+ previous_line: 0,
+ previous_character: 0,
+ tokens,
+ cancellation_token,
+ };
+
+ let range = document.line_index.offset_lsp_range(request.params.range);
+
+ for node in data
+ .root
+ .children()
+ .filter(|node| node.text_range().intersect(range).is_some())
+ {
+ cancellation_token.result().ok()?;
+ visit_junk(&mut context, node)
+ .or_else(|| visit_preamble(&mut context, node))
+ .or_else(|| visit_string(&mut context, node))
+ .or_else(|| visit_entry(&mut context, node));
+ }
+
+ Some(())
+}
+
+fn visit_junk(context: &mut Context, node: &bibtex::SyntaxNode) -> Option<()> {
+ let junk = bibtex::Junk::cast(node)?;
+ for token in junk
+ .syntax()
+ .children_with_tokens()
+ .filter_map(|element| element.into_token())
+ .filter(|token| token.kind() != bibtex::WHITESPACE)
+ {
+ context.insert(token.text_range(), JUNK, ModifierSet::default());
+ }
+ Some(())
+}
+
+fn visit_preamble(context: &mut Context, node: &bibtex::SyntaxNode) -> Option<()> {
+ let preamble = bibtex::Preamble::cast(node)?;
+ for element in preamble.syntax().children_with_tokens() {
+ match element {
+ NodeOrToken::Node(value) => {
+ visit_value(context, value);
+ }
+ NodeOrToken::Token(token) => match token.kind() {
+ bibtex::PREAMBLE_TYPE => {
+ context.insert(token.text_range(), PREAMBLE_TYPE, ModifierSet::default());
+ }
+ bibtex::L_CURLY | bibtex::R_CURLY => {
+ context.insert(token.text_range(), CURLY, ModifierSet::default());
+ }
+ bibtex::L_PAREN | bibtex::R_PAREN => {
+ context.insert(token.text_range(), PAREN, ModifierSet::default());
+ }
+ _ => {}
+ },
+ }
+ }
+
+ Some(())
+}
+
+fn visit_string(context: &mut Context, node: &bibtex::SyntaxNode) -> Option<()> {
+ let string = bibtex::String::cast(node)?;
+ for element in string.syntax().children_with_tokens() {
+ match element {
+ NodeOrToken::Node(value) => {
+ visit_value(context, value);
+ }
+ NodeOrToken::Token(token) => match token.kind() {
+ bibtex::STRING_TYPE => {
+ context.insert(token.text_range(), STRING_TYPE, ModifierSet::default());
+ }
+ bibtex::L_CURLY | bibtex::R_CURLY => {
+ context.insert(token.text_range(), CURLY, ModifierSet::default());
+ }
+ bibtex::L_PAREN | bibtex::R_PAREN => {
+ context.insert(token.text_range(), PAREN, ModifierSet::default());
+ }
+ bibtex::WORD => {
+ context.insert(
+ token.text_range(),
+ STRING_NAME,
+ ModifierSet::default() | SemanticTokenModifier::DEFINITION,
+ );
+ }
+ bibtex::EQUALITY_SIGN => {
+ context.insert(token.text_range(), EQUALITY_SIGN, ModifierSet::default());
+ }
+ _ => {}
+ },
+ }
+ }
+
+ Some(())
+}
+
+fn visit_entry(context: &mut Context, node: &bibtex::SyntaxNode) -> Option<()> {
+ let entry = bibtex::Entry::cast(node)?;
+
+ for element in entry.syntax().children_with_tokens() {
+ match element {
+ NodeOrToken::Node(field) => {
+ visit_field(context, field);
+ }
+ NodeOrToken::Token(token) => match token.kind() {
+ bibtex::ENTRY_TYPE => {
+ context.insert(
+ token.text_range(),
+ match LANGUAGE_DATA.find_entry_type(&token.text()[1..]) {
+ Some(ty) => match ty.category {
+ BibtexEntryTypeCategory::Article => ARTICLE_TYPE,
+ BibtexEntryTypeCategory::Misc => MISC_TYPE,
+ BibtexEntryTypeCategory::String => STRING_TYPE,
+ BibtexEntryTypeCategory::Book => BOOK_TYPE,
+ BibtexEntryTypeCategory::Collection => COLLECTION_TYPE,
+ BibtexEntryTypeCategory::Part => PART_TYPE,
+ BibtexEntryTypeCategory::Thesis => THESIS_TYPE,
+ },
+ None => UNKNOWN_TYPE,
+ },
+ ModifierSet::default(),
+ );
+ }
+ bibtex::L_CURLY | bibtex::R_CURLY => {
+ context.insert(token.text_range(), CURLY, ModifierSet::default());
+ }
+ bibtex::L_PAREN | bibtex::R_PAREN => {
+ context.insert(token.text_range(), PAREN, ModifierSet::default());
+ }
+ bibtex::WORD => {
+ context.insert(
+ token.text_range(),
+ ENTRY_KEY,
+ ModifierSet::default()
+ | SemanticTokenModifier::DEFINITION
+ | SemanticTokenModifier::READONLY,
+ );
+ }
+ bibtex::COMMA => {
+ context.insert(token.text_range(), COMMA, ModifierSet::default());
+ }
+ _ => {}
+ },
+ }
+ }
+
+ Some(())
+}
+
+fn visit_field(context: &mut Context, node: &bibtex::SyntaxNode) -> Option<()> {
+ context.cancellation_token.result().ok()?;
+ let field = bibtex::Field::cast(node)?;
+ context.insert(field.name()?.text_range(), FIELD, ModifierSet::default());
+ if let Some(token) = field.equality_sign() {
+ context.insert(token.text_range(), EQUALITY_SIGN, ModifierSet::default());
+ }
+
+ if let Some(value) = field.value() {
+ visit_value(context, value.syntax());
+ }
+ Some(())
+}
+
+fn visit_value(context: &mut Context, node: &bibtex::SyntaxNode) -> Option<()> {
+ context.cancellation_token.result().ok()?;
+ let value = bibtex::Value::cast(node)?;
+ for child in value.syntax().children_with_tokens() {
+ match child {
+ NodeOrToken::Token(token) if token.kind() == bibtex::HASH => {
+ context.insert(token.text_range(), HASH, ModifierSet::default());
+ }
+ NodeOrToken::Token(_) => {}
+ NodeOrToken::Node(token) => {
+ if let Some(word) = token
+ .first_child_or_token()
+ .and_then(|child| child.into_token())
+ {
+ if word.text().chars().all(|c| c.is_ascii_digit()) {
+ context.insert(
+ word.text_range(),
+ SemanticTokenType::NUMBER,
+ ModifierSet::default(),
+ );
+ } else {
+ context.insert(word.text_range(), STRING_NAME, ModifierSet::default());
+ }
+ } else {
+ visit_content(context, token);
+ }
+ }
+ }
+ }
+ Some(())
+}
+
+fn visit_content(context: &mut Context, node: &bibtex::SyntaxNode) -> Option<()> {
+ for token in node
+ .descendants_with_tokens()
+ .filter_map(|element| element.into_token())
+ {
+ match token.kind() {
+ bibtex::WORD => {
+ context.insert(token.text_range(), TEXT, ModifierSet::default());
+ }
+ bibtex::COMMA => {
+ context.insert(token.text_range(), COMMA, ModifierSet::default());
+ }
+ bibtex::L_CURLY | bibtex::R_CURLY => {
+ context.insert(token.text_range(), CURLY, ModifierSet::default());
+ }
+ bibtex::L_PAREN | bibtex::R_PAREN => {
+ context.insert(token.text_range(), PAREN, ModifierSet::default());
+ }
+ bibtex::SyntaxKind::QUOTE => {
+ context.insert(token.text_range(), QUOTE, ModifierSet::default());
+ }
+ bibtex::SyntaxKind::EQUALITY_SIGN => {
+ context.insert(token.text_range(), EQUALITY_SIGN, ModifierSet::default());
+ }
+ bibtex::SyntaxKind::COMMAND_NAME => {
+ context.insert(token.text_range(), GENERIC_COMMAND, ModifierSet::default());
+ }
+ _ => {}
+ }
+ }
+ Some(())
+}
diff --git a/support/texlab/src/features/semantic/legend.rs b/support/texlab/src/features/semantic/legend.rs
new file mode 100644
index 0000000000..0b2a53cd38
--- /dev/null
+++ b/support/texlab/src/features/semantic/legend.rs
@@ -0,0 +1,83 @@
+use std::ops;
+
+use lsp_types::{SemanticTokenModifier, SemanticTokenType};
+
+macro_rules! define_semantic_token_types {
+ ($(($ident:ident, $string:literal)),*$(,)?) => {
+ $(pub const $ident: SemanticTokenType = SemanticTokenType::new($string);)*
+
+ pub const SUPPORTED_TYPES: &[SemanticTokenType] = &[
+ SemanticTokenType::COMMENT,
+ SemanticTokenType::NUMBER,
+ $($ident),*
+ ];
+ };
+}
+
+define_semantic_token_types![
+ (JUNK, "junk"),
+ (PREAMBLE_TYPE, "preambleType"),
+ (STRING_TYPE, "stringType"),
+ (COMMENT_TYPE, "commentType"),
+ (ARTICLE_TYPE, "articleType"),
+ (BOOK_TYPE, "bookType"),
+ (COLLECTION_TYPE, "collectionType"),
+ (PART_TYPE, "partType"),
+ (THESIS_TYPE, "thesisType"),
+ (MISC_TYPE, "miscType"),
+ (UNKNOWN_TYPE, "unknownType"),
+ (CURLY, "curly"),
+ (BRACK, "brack"),
+ (PAREN, "paren"),
+ (COMMA, "comma"),
+ (ENTRY_KEY, "entryKey"),
+ (FIELD, "field"),
+ (TEXT, "text"),
+ (STRING_NAME, "string"),
+ (EQUALITY_SIGN, "equalitySign"),
+ (HASH, "hash"),
+ (QUOTE, "quote"),
+ (GENERIC_COMMAND, "genericCommand"),
+];
+
+macro_rules! define_semantic_token_modifiers {
+ ($(($ident:ident, $string:literal)),*$(,)?) => {
+ $(pub const $ident: SemanticTokenModifier = SemanticTokenModifier::new($string);)*
+
+ pub const SUPPORTED_MODIFIERS: &[SemanticTokenModifier] = &[
+ SemanticTokenModifier::DEFAULT_LIBRARY,
+ SemanticTokenModifier::DECLARATION,
+ SemanticTokenModifier::DEFINITION,
+ SemanticTokenModifier::READONLY,
+ $($ident),*
+ ];
+ };
+}
+
+define_semantic_token_modifiers![(ITALIC, "italic"),];
+
+pub fn type_index(ty: SemanticTokenType) -> u32 {
+ SUPPORTED_TYPES.iter().position(|t| *t == ty).unwrap() as u32
+}
+
+#[derive(Default)]
+pub struct ModifierSet(pub u32);
+
+impl ops::BitOrAssign<SemanticTokenModifier> for ModifierSet {
+ fn bitor_assign(&mut self, rhs: SemanticTokenModifier) {
+ let index = SUPPORTED_MODIFIERS
+ .iter()
+ .position(|modifier| modifier == &rhs)
+ .unwrap();
+ self.0 |= 1 << index;
+ }
+}
+
+impl ops::BitOr<SemanticTokenModifier> for ModifierSet {
+ type Output = ModifierSet;
+
+ fn bitor(mut self, rhs: SemanticTokenModifier) -> Self::Output {
+ self |= rhs;
+ self
+ }
+}
diff --git a/support/texlab/src/features/symbol.rs b/support/texlab/src/features/symbol.rs
new file mode 100644
index 0000000000..349b6d0a37
--- /dev/null
+++ b/support/texlab/src/features/symbol.rs
@@ -0,0 +1,125 @@
+mod bibtex;
+mod latex;
+mod project_order;
+mod types;
+
+use std::{cmp::Reverse, sync::Arc};
+
+use cancellation::CancellationToken;
+use lsp_types::{
+ DocumentSymbolParams, DocumentSymbolResponse, SymbolInformation, WorkspaceSymbolParams,
+};
+
+use crate::{ClientCapabilitiesExt, Uri, Workspace};
+
+use self::{
+ bibtex::find_bibtex_symbols, latex::find_latex_symbols, project_order::ProjectOrdering,
+};
+
+use super::FeatureRequest;
+
+pub fn find_document_symbols(
+ req: FeatureRequest<DocumentSymbolParams>,
+ token: &CancellationToken,
+) -> DocumentSymbolResponse {
+ let mut buf = Vec::new();
+ find_latex_symbols(&req.subset, &mut buf, token);
+ find_bibtex_symbols(&req.subset, &mut buf, token);
+ if req
+ .context
+ .client_capabilities
+ .lock()
+ .unwrap()
+ .has_hierarchical_document_symbol_support()
+ {
+ DocumentSymbolResponse::Nested(
+ buf.into_iter()
+ .map(|symbol| symbol.into_document_symbol())
+ .collect(),
+ )
+ } else {
+ let mut new_buf = Vec::new();
+ for symbol in buf {
+ symbol.flatten(&mut new_buf);
+ }
+ let mut new_buf = new_buf
+ .into_iter()
+ .map(|symbol| symbol.into_symbol_info(req.main_document().uri.as_ref().clone()))
+ .collect();
+ sort_symbols(req.workspace.as_ref(), &mut new_buf);
+ DocumentSymbolResponse::Flat(new_buf)
+ }
+}
+
+#[derive(Debug, Clone)]
+struct WorkspaceSymbol {
+ info: SymbolInformation,
+ search_text: String,
+}
+
+pub fn find_workspace_symbols(
+ workspace: &dyn Workspace,
+ params: &WorkspaceSymbolParams,
+ token: &CancellationToken,
+) -> Vec<SymbolInformation> {
+ let mut symbols = Vec::new();
+
+ for document in workspace.documents() {
+ if let Some(subset) = workspace.subset(Arc::clone(&document.uri)) {
+ let mut buf = Vec::new();
+ find_latex_symbols(&subset, &mut buf, token);
+ find_bibtex_symbols(&subset, &mut buf, token);
+ let mut new_buf = Vec::new();
+
+ for symbol in buf {
+ symbol.flatten(&mut new_buf);
+ }
+
+ for symbol in new_buf {
+ symbols.push(WorkspaceSymbol {
+ search_text: symbol.search_text(),
+ info: symbol.into_symbol_info(document.uri.as_ref().clone()),
+ });
+ }
+ }
+ }
+
+ let query_words: Vec<_> = params
+ .query
+ .split_whitespace()
+ .map(str::to_lowercase)
+ .collect();
+ let mut filtered = Vec::new();
+ for symbol in symbols {
+ let mut included = true;
+ for word in &query_words {
+ if !symbol.search_text.contains(word) {
+ included = false;
+ break;
+ }
+ }
+
+ if included {
+ filtered.push(symbol.info);
+ }
+ }
+ sort_symbols(workspace, &mut filtered);
+ filtered
+}
+
+fn sort_symbols(workspace: &dyn Workspace, symbols: &mut Vec<SymbolInformation>) {
+ let ordering = ProjectOrdering::from(workspace);
+ symbols.sort_by(|left, right| {
+ let left_key = (
+ ordering.get(&Uri::from(left.location.uri.clone())),
+ left.location.range.start,
+ Reverse(left.location.range.end),
+ );
+ let right_key = (
+ ordering.get(&Uri::from(right.location.uri.clone())),
+ right.location.range.start,
+ Reverse(right.location.range.end),
+ );
+ left_key.cmp(&right_key)
+ });
+}
diff --git a/support/texlab/src/features/symbol/bibtex.rs b/support/texlab/src/features/symbol/bibtex.rs
new file mode 100644
index 0000000000..e030195880
--- /dev/null
+++ b/support/texlab/src/features/symbol/bibtex.rs
@@ -0,0 +1,88 @@
+use cancellation::CancellationToken;
+
+use crate::{
+ syntax::{
+ bibtex::{self, HasType},
+ CstNode,
+ },
+ BibtexEntryTypeCategory, LineIndexExt, WorkspaceSubset, LANGUAGE_DATA,
+};
+
+use super::types::{InternalSymbol, InternalSymbolKind};
+
+pub fn find_bibtex_symbols(
+ subset: &WorkspaceSubset,
+ buf: &mut Vec<InternalSymbol>,
+ token: &CancellationToken,
+) -> Option<()> {
+ let main_document = subset.documents.first()?;
+ let data = main_document.data.as_bibtex()?;
+
+ for node in data.root.children() {
+ if token.is_canceled() {
+ return None;
+ }
+
+ if let Some(string) = bibtex::String::cast(node) {
+ if let Some(name) = string.name() {
+ buf.push(InternalSymbol {
+ name: name.text().into(),
+ label: None,
+ kind: InternalSymbolKind::String,
+ deprecated: false,
+ full_range: main_document
+ .line_index
+ .line_col_lsp_range(string.small_range()),
+ selection_range: main_document
+ .line_index
+ .line_col_lsp_range(name.text_range()),
+ children: Vec::new(),
+ })
+ }
+ } else if let Some(entry) = bibtex::Entry::cast(node) {
+ if let Some(ty) = entry.ty() {
+ if let Some(key) = entry.key() {
+ let mut children = Vec::new();
+ for field in entry.fields() {
+ if let Some(name) = field.name() {
+ let symbol = InternalSymbol {
+ name: name.text().to_string(),
+ label: None,
+ kind: InternalSymbolKind::Field,
+ deprecated: false,
+ full_range: main_document
+ .line_index
+ .line_col_lsp_range(field.small_range()),
+ selection_range: main_document
+ .line_index
+ .line_col_lsp_range(name.text_range()),
+ children: Vec::new(),
+ };
+ children.push(symbol);
+ }
+ }
+
+ let category = LANGUAGE_DATA
+ .find_entry_type(&ty.text()[1..])
+ .map(|ty| ty.category)
+ .unwrap_or(BibtexEntryTypeCategory::Misc);
+
+ buf.push(InternalSymbol {
+ name: key.text().to_string(),
+ label: None,
+ kind: InternalSymbolKind::Entry(category),
+ deprecated: false,
+ full_range: main_document
+ .line_index
+ .line_col_lsp_range(entry.small_range()),
+ selection_range: main_document
+ .line_index
+ .line_col_lsp_range(key.text_range()),
+ children,
+ });
+ }
+ }
+ }
+ }
+ Some(())
+}
diff --git a/support/texlab/src/features/symbol/latex.rs b/support/texlab/src/features/symbol/latex.rs
new file mode 100644
index 0000000000..ce584c0c76
--- /dev/null
+++ b/support/texlab/src/features/symbol/latex.rs
@@ -0,0 +1,488 @@
+use std::str::FromStr;
+
+use cancellation::CancellationToken;
+use lsp_types::Range;
+use smol_str::SmolStr;
+use titlecase::titlecase;
+
+use crate::{
+ find_caption_by_parent, find_label_number,
+ syntax::{
+ latex::{self, HasBrack, HasCurly},
+ CstNode,
+ },
+ LabelledFloatKind, LatexDocumentData, LineIndexExt, WorkspaceSubset, LANGUAGE_DATA,
+};
+
+use super::types::{InternalSymbol, InternalSymbolKind};
+
+pub fn find_latex_symbols(
+ subset: &WorkspaceSubset,
+ buf: &mut Vec<InternalSymbol>,
+ token: &CancellationToken,
+) -> Option<()> {
+ let main_document = subset
+ .documents
+ .first()
+ .filter(|document| document.uri.as_str().ends_with(".tex"))?;
+
+ let data = main_document.data.as_latex()?;
+ let mut context = Context {
+ subset,
+ data,
+ token,
+ };
+
+ let root = &context.data.root;
+ let mut symbols = visit(&mut context, root);
+ buf.append(&mut symbols);
+ Some(())
+}
+
+struct Context<'a> {
+ subset: &'a WorkspaceSubset,
+ data: &'a LatexDocumentData,
+ token: &'a CancellationToken,
+}
+
+fn visit(context: &mut Context, node: &latex::SyntaxNode) -> Vec<InternalSymbol> {
+ let symbol = match node.kind() {
+ latex::PART
+ | latex::CHAPTER
+ | latex::SECTION
+ | latex::SUBSECTION
+ | latex::SUBSUBSECTION
+ | latex::PARAGRAPH
+ | latex::SUBPARAGRAPH => visit_section(context, node),
+ latex::ENUM_ITEM => visit_enum_item(context, node),
+ latex::EQUATION => visit_equation(context, node),
+ latex::ENVIRONMENT => latex::Environment::cast(node)
+ .and_then(|env| env.begin())
+ .and_then(|begin| begin.name())
+ .and_then(|name| name.word())
+ .map(|name| name.text())
+ .and_then(|name| {
+ if LANGUAGE_DATA
+ .math_environments
+ .iter()
+ .any(|env| env == name)
+ {
+ visit_equation_environment(context, node)
+ } else if LANGUAGE_DATA
+ .enum_environments
+ .iter()
+ .any(|env| env == name)
+ {
+ visit_enumeration(context, node, name)
+ } else if let Ok(float_kind) = LabelledFloatKind::from_str(name) {
+ visit_float(context, node, float_kind)
+ } else {
+ visit_theorem(context, node, name)
+ }
+ }),
+ _ => None,
+ };
+
+ match symbol {
+ Some(mut parent) => {
+ for child in node.children() {
+ if context.token.is_canceled() {
+ break;
+ }
+
+ parent.children.append(&mut visit(context, child));
+ }
+ vec![parent]
+ }
+ None => {
+ let mut symbols = Vec::new();
+ for child in node.children() {
+ if context.token.is_canceled() {
+ break;
+ }
+ symbols.append(&mut visit(context, child));
+ }
+ symbols
+ }
+ }
+}
+
+fn visit_section(context: &mut Context, node: &latex::SyntaxNode) -> Option<InternalSymbol> {
+ let section = latex::Section::cast(node)?;
+ let full_range = context
+ .subset
+ .documents
+ .first()?
+ .line_index
+ .line_col_lsp_range(section.small_range());
+
+ let group = section.name()?;
+ let group_text = group.content_text()?;
+
+ let symbol = match find_label_by_parent(context, section.syntax()) {
+ Some(NumberedLabel {
+ name: label,
+ range: selection_range,
+ number,
+ }) => {
+ let name = match number {
+ Some(number) => format!("{} {}", number, group_text),
+ None => group_text.to_string(),
+ };
+
+ InternalSymbol {
+ name,
+ label: Some(label),
+ kind: InternalSymbolKind::Section,
+ deprecated: false,
+ full_range,
+ selection_range,
+ children: Vec::new(),
+ }
+ }
+ None => InternalSymbol {
+ name: group_text.to_string(),
+ label: None,
+ kind: InternalSymbolKind::Section,
+ deprecated: false,
+ full_range,
+ selection_range: full_range,
+ children: Vec::new(),
+ },
+ };
+ Some(symbol)
+}
+
+fn visit_enum_item(context: &mut Context, node: &latex::SyntaxNode) -> Option<InternalSymbol> {
+ let enum_item = latex::EnumItem::cast(node)?;
+ if !enum_item
+ .syntax()
+ .ancestors()
+ .filter_map(latex::Environment::cast)
+ .filter_map(|environment| environment.begin())
+ .filter_map(|begin| begin.name())
+ .filter_map(|name| name.word())
+ .any(|name| {
+ LANGUAGE_DATA
+ .enum_environments
+ .iter()
+ .any(|e| e == name.text())
+ })
+ {
+ return None;
+ }
+
+ let full_range = context
+ .subset
+ .documents
+ .first()?
+ .line_index
+ .line_col_lsp_range(enum_item.small_range());
+
+ let name = enum_item
+ .label()
+ .and_then(|label| label.content_text())
+ .unwrap_or_else(|| "Item".to_string());
+
+ let symbol = match find_label_by_parent(context, node) {
+ Some(NumberedLabel {
+ name: label,
+ range: selection_range,
+ number,
+ }) => InternalSymbol {
+ name: number.map(Into::into).unwrap_or_else(|| name.to_string()),
+ label: Some(label),
+ kind: InternalSymbolKind::EnumerationItem,
+ deprecated: false,
+ full_range,
+ selection_range,
+ children: Vec::new(),
+ },
+ None => InternalSymbol {
+ name: name.to_string(),
+ label: None,
+ kind: InternalSymbolKind::EnumerationItem,
+ deprecated: false,
+ full_range,
+ selection_range: full_range,
+ children: Vec::new(),
+ },
+ };
+ Some(symbol)
+}
+
+fn visit_equation(context: &mut Context, node: &latex::SyntaxNode) -> Option<InternalSymbol> {
+ let equation = latex::Equation::cast(node)?;
+
+ let full_range = context
+ .subset
+ .documents
+ .first()?
+ .line_index
+ .line_col_lsp_range(equation.small_range());
+
+ make_equation_symbol(context, node, full_range)
+}
+
+fn visit_equation_environment(
+ context: &mut Context,
+ node: &latex::SyntaxNode,
+) -> Option<InternalSymbol> {
+ let environment = latex::Environment::cast(node)?;
+
+ let full_range = context
+ .subset
+ .documents
+ .first()?
+ .line_index
+ .line_col_lsp_range(environment.small_range());
+
+ make_equation_symbol(context, node, full_range)
+}
+
+fn make_equation_symbol(
+ context: &mut Context,
+ node: &cstree::ResolvedNode<latex::Language>,
+ full_range: Range,
+) -> Option<InternalSymbol> {
+ let symbol = match find_label_by_parent(context, node) {
+ Some(NumberedLabel {
+ name: label,
+ range: selection_range,
+ number,
+ }) => {
+ let name = match number {
+ Some(number) => format!("Equation ({})", number),
+ None => "Equation".to_string(),
+ };
+
+ InternalSymbol {
+ name,
+ label: Some(label),
+ kind: InternalSymbolKind::Equation,
+ deprecated: false,
+ full_range,
+ selection_range,
+ children: Vec::new(),
+ }
+ }
+ None => InternalSymbol {
+ name: "Equation".to_string(),
+ label: None,
+ kind: InternalSymbolKind::Equation,
+ deprecated: false,
+ full_range,
+ selection_range: full_range,
+ children: Vec::new(),
+ },
+ };
+ Some(symbol)
+}
+
+fn visit_enumeration(
+ context: &mut Context,
+ node: &latex::SyntaxNode,
+ env_name: &str,
+) -> Option<InternalSymbol> {
+ let environment = latex::Environment::cast(node)?;
+ let full_range = context
+ .subset
+ .documents
+ .first()?
+ .line_index
+ .line_col_lsp_range(environment.small_range());
+
+ let name = titlecase(env_name);
+ let symbol = match find_label_by_parent(context, environment.syntax()) {
+ Some(NumberedLabel {
+ name: label,
+ range: selection_range,
+ number,
+ }) => {
+ let name = match number {
+ Some(number) => format!("{} {}", name, number),
+ None => name,
+ };
+
+ InternalSymbol {
+ name,
+ label: Some(label),
+ kind: InternalSymbolKind::Enumeration,
+ deprecated: false,
+ full_range,
+ selection_range,
+ children: Vec::new(),
+ }
+ }
+ None => InternalSymbol {
+ name,
+ label: None,
+ kind: InternalSymbolKind::Enumeration,
+ deprecated: false,
+ full_range,
+ selection_range: full_range,
+ children: Vec::new(),
+ },
+ };
+ Some(symbol)
+}
+
+fn visit_float(
+ context: &mut Context,
+ node: &latex::SyntaxNode,
+ float_kind: LabelledFloatKind,
+) -> Option<InternalSymbol> {
+ let environment = latex::Environment::cast(node)?;
+ let full_range = context
+ .subset
+ .documents
+ .first()?
+ .line_index
+ .line_col_lsp_range(environment.small_range());
+
+ let (float_kind, symbol_kind) = match float_kind {
+ LabelledFloatKind::Algorithm => ("Algorithm", InternalSymbolKind::Algorithm),
+ LabelledFloatKind::Figure => ("Figure", InternalSymbolKind::Figure),
+ LabelledFloatKind::Listing => ("Listing", InternalSymbolKind::Listing),
+ LabelledFloatKind::Table => ("Table", InternalSymbolKind::Table),
+ };
+
+ let caption = find_caption_by_parent(environment.syntax())?;
+ let symbol = match find_label_by_parent(context, environment.syntax()) {
+ Some(NumberedLabel {
+ name: label,
+ range: selection_range,
+ number,
+ }) => {
+ let name = match number {
+ Some(number) => format!("{} {}: {}", float_kind, number, caption),
+ None => format!("{}: {}", float_kind, caption),
+ };
+
+ InternalSymbol {
+ name,
+ label: Some(label),
+ kind: symbol_kind,
+ deprecated: false,
+ full_range,
+ selection_range,
+ children: Vec::new(),
+ }
+ }
+ None => InternalSymbol {
+ name: format!("{}: {}", float_kind, caption),
+ label: None,
+ kind: symbol_kind,
+ deprecated: false,
+ full_range,
+ selection_range: full_range,
+ children: Vec::new(),
+ },
+ };
+
+ Some(symbol)
+}
+
+fn visit_theorem(
+ context: &mut Context,
+ node: &latex::SyntaxNode,
+ environment_name: &str,
+) -> Option<InternalSymbol> {
+ let definition = context
+ .subset
+ .documents
+ .iter()
+ .filter_map(|document| document.data.as_latex())
+ .find_map(|data| {
+ data.extras
+ .theorem_environments
+ .iter()
+ .find(|environment| environment.name == environment_name)
+ .cloned()
+ })?;
+
+ let node = latex::Environment::cast(node)?;
+ let theorem_description = node
+ .begin()?
+ .options()
+ .and_then(|option| option.content_text());
+
+ let full_range = context
+ .subset
+ .documents
+ .first()?
+ .line_index
+ .line_col_lsp_range(node.small_range());
+
+ let symbol = match find_label_by_parent(context, node.syntax()) {
+ Some(NumberedLabel {
+ name: label,
+ range: selection_range,
+ number,
+ }) => {
+ let name = match (number, theorem_description) {
+ (Some(number), Some(desc)) => {
+ format!("{} {} ({})", definition.description, number, desc)
+ }
+ (Some(number), None) => format!("{} {}", definition.description, number),
+ (None, Some(desc)) => format!("{} ({})", definition.description, desc),
+ (None, None) => definition.description.clone(),
+ };
+
+ InternalSymbol {
+ name,
+ label: Some(label),
+ kind: InternalSymbolKind::Theorem,
+ deprecated: false,
+ full_range,
+ selection_range,
+ children: Vec::new(),
+ }
+ }
+ None => {
+ let name = match theorem_description {
+ Some(desc) => format!("{} ({})", definition.description, desc),
+ None => definition.description.to_string(),
+ };
+ InternalSymbol {
+ name,
+ label: None,
+ kind: InternalSymbolKind::Theorem,
+ deprecated: false,
+ full_range,
+ selection_range: full_range,
+ children: Vec::new(),
+ }
+ }
+ };
+ Some(symbol)
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+struct NumberedLabel {
+ name: String,
+ range: Range,
+ number: Option<SmolStr>,
+}
+
+fn find_label_by_parent(
+ context: &mut Context,
+ parent: &latex::SyntaxNode,
+) -> Option<NumberedLabel> {
+ let node = parent.children().find_map(latex::LabelDefinition::cast)?;
+
+ let name = node.name()?.word()?.text();
+ let range = context
+ .subset
+ .documents
+ .first()?
+ .line_index
+ .line_col_lsp_range(node.small_range());
+
+ let number = find_label_number(&context.subset, name);
+ Some(NumberedLabel {
+ name: name.to_string(),
+ range,
+ number: number.map(Into::into),
+ })
+}
diff --git a/support/texlab/src/features/symbol/project_order.rs b/support/texlab/src/features/symbol/project_order.rs
new file mode 100644
index 0000000000..08b43ad50c
--- /dev/null
+++ b/support/texlab/src/features/symbol/project_order.rs
@@ -0,0 +1,223 @@
+use std::{sync::Arc, usize};
+
+use petgraph::{algo::tarjan_scc, Directed, Graph};
+use rustc_hash::FxHashSet;
+
+use crate::{Uri, Workspace, WorkspaceSubset};
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct ProjectOrdering {
+ ordering: Vec<Arc<Uri>>,
+}
+
+impl ProjectOrdering {
+ pub fn get(&self, uri: &Uri) -> usize {
+ self.ordering
+ .iter()
+ .position(|u| u.as_ref() == uri)
+ .unwrap_or(usize::MAX)
+ }
+}
+
+impl From<&dyn Workspace> for ProjectOrdering {
+ fn from(workspace: &dyn Workspace) -> Self {
+ let mut ordering = Vec::new();
+ let uris: FxHashSet<Arc<Uri>> = workspace
+ .documents()
+ .into_iter()
+ .map(|document| Arc::clone(&document.uri))
+ .collect();
+
+ let comps = connected_components(workspace);
+ for comp in comps {
+ let graph = build_dependency_graph(&comp);
+
+ let mut visited = FxHashSet::default();
+ let root_index = *graph.node_weight(tarjan_scc(&graph)[0][0]).unwrap();
+ let mut stack = vec![Arc::clone(&comp.documents[root_index].uri)];
+
+ while let Some(uri) = stack.pop() {
+ if !visited.insert(Arc::clone(&uri)) {
+ continue;
+ }
+
+ ordering.push(Arc::clone(&uri));
+ if let Some(document) = workspace.get(&uri) {
+ if let Some(data) = document.data.as_latex() {
+ for link in data.extras.explicit_links.iter().rev() {
+ for target in &link.targets {
+ if uris.contains(target.as_ref()) {
+ stack.push(Arc::clone(target));
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Self { ordering }
+ }
+}
+
+fn connected_components(workspace: &dyn Workspace) -> Vec<WorkspaceSubset> {
+ let mut components = Vec::new();
+ let mut visited = FxHashSet::default();
+ for root_document in workspace.documents() {
+ if !visited.insert(Arc::clone(&root_document.uri)) {
+ continue;
+ }
+
+ let subset = workspace.subset(Arc::clone(&root_document.uri)).unwrap();
+ for document in &subset.documents {
+ visited.insert(Arc::clone(&document.uri));
+ }
+ components.push(subset);
+ }
+ components
+}
+
+fn build_dependency_graph(subset: &WorkspaceSubset) -> Graph<usize, (), Directed> {
+ let mut graph = Graph::new();
+ let nodes: Vec<_> = (0..subset.documents.len())
+ .map(|i| graph.add_node(i))
+ .collect();
+
+ for (i, document) in subset.documents.iter().enumerate() {
+ if let Some(data) = document.data.as_latex() {
+ for link in &data.extras.explicit_links {
+ for target in &link.targets {
+ if let Some(j) = subset
+ .documents
+ .iter()
+ .position(|document| document.uri.as_ref() == target.as_ref())
+ {
+ graph.add_edge(nodes[j], nodes[i], ());
+ break;
+ }
+ }
+ }
+ }
+ }
+ graph
+}
+
+#[cfg(test)]
+mod tests {
+ use anyhow::Result;
+
+ use crate::{create_workspace_fast, DocumentLanguage, ServerContext, WorkspaceSource};
+
+ use super::*;
+
+ #[test]
+ fn test_no_cycles() -> Result<()> {
+ let workspace: Arc<dyn Workspace> = Arc::new(create_workspace_fast(Arc::new(
+ ServerContext::new(std::env::temp_dir()),
+ ))?);
+
+ let a = workspace.open(
+ Arc::new(Uri::parse("http://example.com/a.tex")?),
+ "".into(),
+ DocumentLanguage::Latex,
+ WorkspaceSource::Client,
+ );
+
+ let b = workspace.open(
+ Arc::new(Uri::parse("http://example.com/b.tex")?),
+ "".into(),
+ DocumentLanguage::Latex,
+ WorkspaceSource::Client,
+ );
+
+ let c = workspace.open(
+ Arc::new(Uri::parse("http://example.com/c.tex")?),
+ r#"\include{b}\include{a}"#.into(),
+ DocumentLanguage::Latex,
+ WorkspaceSource::Client,
+ );
+
+ let ordering = ProjectOrdering::from(workspace.as_ref());
+
+ assert_eq!(ordering.get(&a.uri), 2);
+ assert_eq!(ordering.get(&b.uri), 1);
+ assert_eq!(ordering.get(&c.uri), 0);
+ Ok(())
+ }
+
+ #[test]
+ fn test_cycles() -> Result<()> {
+ let workspace: Arc<dyn Workspace> = Arc::new(create_workspace_fast(Arc::new(
+ ServerContext::new(std::env::temp_dir()),
+ ))?);
+
+ let a = workspace.open(
+ Arc::new(Uri::parse("http://example.com/a.tex")?),
+ r#"\include{b}"#.into(),
+ DocumentLanguage::Latex,
+ WorkspaceSource::Client,
+ );
+
+ let b = workspace.open(
+ Arc::new(Uri::parse("http://example.com/b.tex")?),
+ r#"\include{a}"#.into(),
+ DocumentLanguage::Latex,
+ WorkspaceSource::Client,
+ );
+
+ let c = workspace.open(
+ Arc::new(Uri::parse("http://example.com/c.tex")?),
+ r#"\include{a}"#.into(),
+ DocumentLanguage::Latex,
+ WorkspaceSource::Client,
+ );
+
+ let ordering = ProjectOrdering::from(workspace.as_ref());
+
+ assert_eq!(ordering.get(&a.uri), 1);
+ assert_eq!(ordering.get(&b.uri), 2);
+ assert_eq!(ordering.get(&c.uri), 0);
+ Ok(())
+ }
+
+ #[test]
+ fn test_multiple_roots() -> Result<()> {
+ let workspace: Arc<dyn Workspace> = Arc::new(create_workspace_fast(Arc::new(
+ ServerContext::new(std::env::temp_dir()),
+ ))?);
+
+ let a = workspace.open(
+ Arc::new(Uri::parse("http://example.com/a.tex")?),
+ r#"\include{b}"#.into(),
+ DocumentLanguage::Latex,
+ WorkspaceSource::Client,
+ );
+
+ let b = workspace.open(
+ Arc::new(Uri::parse("http://example.com/b.tex")?),
+ r#""#.into(),
+ DocumentLanguage::Latex,
+ WorkspaceSource::Client,
+ );
+
+ let c = workspace.open(
+ Arc::new(Uri::parse("http://example.com/c.tex")?),
+ r#""#.into(),
+ DocumentLanguage::Latex,
+ WorkspaceSource::Client,
+ );
+
+ let d = workspace.open(
+ Arc::new(Uri::parse("http://example.com/d.tex")?),
+ r#"\include{c}"#.into(),
+ DocumentLanguage::Latex,
+ WorkspaceSource::Client,
+ );
+
+ let ordering = ProjectOrdering::from(workspace.as_ref());
+
+ assert!(ordering.get(&a.uri) < ordering.get(&b.uri));
+ assert!(ordering.get(&d.uri) < ordering.get(&c.uri));
+ Ok(())
+ }
+}
diff --git a/support/texlab/src/symbol/types.rs b/support/texlab/src/features/symbol/types.rs
index f10f43c637..bb00a932d8 100644
--- a/support/texlab/src/symbol/types.rs
+++ b/support/texlab/src/features/symbol/types.rs
@@ -1,10 +1,9 @@
-use crate::{
- protocol::{DocumentSymbol, Location, Range, SymbolInformation, SymbolKind, Uri},
- syntax::{BibtexEntryTypeCategory, Structure},
-};
+use lsp_types::{DocumentSymbol, Location, Range, SymbolInformation, SymbolKind};
+
+use crate::{features::lsp_kinds::Structure, BibtexEntryTypeCategory, Uri};
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum LatexSymbolKind {
+pub enum InternalSymbolKind {
Section,
Figure,
Algorithm,
@@ -19,8 +18,8 @@ pub enum LatexSymbolKind {
String,
}
-impl Into<SymbolKind> for LatexSymbolKind {
- fn into(self) -> SymbolKind {
+impl InternalSymbolKind {
+ pub fn into_symbol_kind(self) -> SymbolKind {
match self {
Self::Section => Structure::Section.symbol_kind(),
Self::Figure | Self::Algorithm | Self::Table | Self::Listing => {
@@ -38,37 +37,37 @@ impl Into<SymbolKind> for LatexSymbolKind {
}
#[derive(Debug, PartialEq, Eq, Clone)]
-pub struct LatexSymbol {
+pub struct InternalSymbol {
pub name: String,
pub label: Option<String>,
- pub kind: LatexSymbolKind,
+ pub kind: InternalSymbolKind,
pub deprecated: bool,
pub full_range: Range,
pub selection_range: Range,
- pub children: Vec<LatexSymbol>,
+ pub children: Vec<InternalSymbol>,
}
-impl LatexSymbol {
+impl InternalSymbol {
pub fn search_text(&self) -> String {
let kind = match self.kind {
- LatexSymbolKind::Section => "latex section",
- LatexSymbolKind::Figure => "latex float figure",
- LatexSymbolKind::Algorithm => "latex float algorithm",
- LatexSymbolKind::Table => "latex float table",
- LatexSymbolKind::Listing => "latex float listing",
- LatexSymbolKind::Enumeration => "latex enumeration",
- LatexSymbolKind::EnumerationItem => "latex enumeration item",
- LatexSymbolKind::Theorem => "latex math",
- LatexSymbolKind::Equation => "latex math equation",
- LatexSymbolKind::Entry(_) => "bibtex entry",
- LatexSymbolKind::Field => "bibtex field",
- LatexSymbolKind::String => "bibtex string",
+ InternalSymbolKind::Section => "latex section",
+ InternalSymbolKind::Figure => "latex float figure",
+ InternalSymbolKind::Algorithm => "latex float algorithm",
+ InternalSymbolKind::Table => "latex float table",
+ InternalSymbolKind::Listing => "latex float listing",
+ InternalSymbolKind::Enumeration => "latex enumeration",
+ InternalSymbolKind::EnumerationItem => "latex enumeration item",
+ InternalSymbolKind::Theorem => "latex math",
+ InternalSymbolKind::Equation => "latex math equation",
+ InternalSymbolKind::Entry(_) => "bibtex entry",
+ InternalSymbolKind::Field => "bibtex field",
+ InternalSymbolKind::String => "bibtex string",
};
format!("{} {}", kind, self.name).to_lowercase()
}
pub fn flatten(mut self, buffer: &mut Vec<Self>) {
- if self.kind == LatexSymbolKind::Field {
+ if self.kind == InternalSymbolKind::Field {
return;
}
for symbol in self.children.drain(..) {
@@ -77,28 +76,34 @@ impl LatexSymbol {
buffer.push(self);
}
- pub fn into_symbol_info(self, uri: Uri) -> SymbolInformation {
- SymbolInformation {
+ pub fn into_document_symbol(self) -> DocumentSymbol {
+ let children = self
+ .children
+ .into_iter()
+ .map(|child| child.into_document_symbol())
+ .collect();
+ #[allow(deprecated)]
+ DocumentSymbol {
name: self.name,
+ detail: self.label,
+ kind: self.kind.into_symbol_kind(),
deprecated: Some(self.deprecated),
- kind: self.kind.into(),
- container_name: None,
- location: Location::new(uri.into(), self.full_range),
+ range: self.full_range,
+ selection_range: self.selection_range,
+ children: Some(children),
+ tags: None,
}
}
-}
-impl Into<DocumentSymbol> for LatexSymbol {
- fn into(self) -> DocumentSymbol {
- let children = self.children.into_iter().map(Into::into).collect();
- DocumentSymbol {
+ pub fn into_symbol_info(self, uri: Uri) -> SymbolInformation {
+ #[allow(deprecated)]
+ SymbolInformation {
name: self.name,
+ kind: self.kind.into_symbol_kind(),
deprecated: Some(self.deprecated),
- detail: self.label,
- kind: self.kind.into(),
- selection_range: self.selection_range,
- range: self.full_range,
- children: Some(children),
+ location: Location::new(uri.into(), self.full_range),
+ container_name: None,
+ tags: None,
}
}
}
diff --git a/support/texlab/src/folding/bibtex_decl.rs b/support/texlab/src/folding/bibtex_decl.rs
deleted file mode 100644
index f0ffa787e1..0000000000
--- a/support/texlab/src/folding/bibtex_decl.rs
+++ /dev/null
@@ -1,159 +0,0 @@
-use crate::{
- feature::{FeatureProvider, FeatureRequest},
- protocol::{FoldingRange, FoldingRangeKind, FoldingRangeParams},
- syntax::{bibtex, SyntaxNode},
- workspace::DocumentContent,
-};
-use async_trait::async_trait;
-use petgraph::graph::NodeIndex;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct BibtexDeclarationFoldingProvider;
-
-#[async_trait]
-impl FeatureProvider for BibtexDeclarationFoldingProvider {
- type Params = FoldingRangeParams;
- type Output = Vec<FoldingRange>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- if let DocumentContent::Bibtex(tree) = &req.current().content {
- tree.children(tree.root)
- .filter_map(|decl| Self::fold(tree, decl))
- .collect()
- } else {
- Vec::new()
- }
- }
-}
-
-impl BibtexDeclarationFoldingProvider {
- fn fold(tree: &bibtex::Tree, decl: NodeIndex) -> Option<FoldingRange> {
- let (ty, right) = match &tree.graph[decl] {
- bibtex::Node::Preamble(preamble) => (Some(&preamble.ty), preamble.right.as_ref()),
- bibtex::Node::String(string) => (Some(&string.ty), string.right.as_ref()),
- bibtex::Node::Entry(entry) => (Some(&entry.ty), entry.right.as_ref()),
- bibtex::Node::Root(_)
- | bibtex::Node::Comment(_)
- | bibtex::Node::Field(_)
- | bibtex::Node::Word(_)
- | bibtex::Node::Command(_)
- | bibtex::Node::QuotedContent(_)
- | bibtex::Node::BracedContent(_)
- | bibtex::Node::Concat(_) => (None, None),
- };
-
- Some(FoldingRange {
- start_line: ty?.start().line,
- start_character: Some(ty?.start().character),
- end_line: right?.end().line,
- end_character: Some(right?.end().character),
- kind: Some(FoldingRangeKind::Region),
- })
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::feature::FeatureTester;
- use indoc::indoc;
-
- #[tokio::test]
- async fn preamble() {
- let actual_foldings = FeatureTester::new()
- .file("main.bib", r#"@preamble{"foo"}"#)
- .main("main.bib")
- .test_folding(BibtexDeclarationFoldingProvider)
- .await;
-
- let expected_foldings = vec![FoldingRange {
- start_line: 0,
- start_character: Some(0),
- end_line: 0,
- end_character: Some(16),
- kind: Some(FoldingRangeKind::Region),
- }];
-
- assert_eq!(actual_foldings, expected_foldings);
- }
-
- #[tokio::test]
- async fn string() {
- let actual_foldings = FeatureTester::new()
- .file("main.bib", r#"@string{foo = "bar"}"#)
- .main("main.bib")
- .test_folding(BibtexDeclarationFoldingProvider)
- .await;
-
- let expected_foldings = vec![FoldingRange {
- start_line: 0,
- start_character: Some(0),
- end_line: 0,
- end_character: Some(20),
- kind: Some(FoldingRangeKind::Region),
- }];
-
- assert_eq!(actual_foldings, expected_foldings);
- }
-
- #[tokio::test]
- async fn entry() {
- let actual_foldings = FeatureTester::new()
- .file(
- "main.bib",
- indoc!(
- r#"
- @article{foo,
- bar = baz
- }
- "#
- ),
- )
- .main("main.bib")
- .test_folding(BibtexDeclarationFoldingProvider)
- .await;
-
- let expected_foldings = vec![FoldingRange {
- start_line: 0,
- start_character: Some(0),
- end_line: 2,
- end_character: Some(1),
- kind: Some(FoldingRangeKind::Region),
- }];
-
- assert_eq!(actual_foldings, expected_foldings);
- }
-
- #[tokio::test]
- async fn comment() {
- let actual_foldings = FeatureTester::new()
- .file("main.bib", "foo")
- .main("main.bib")
- .test_folding(BibtexDeclarationFoldingProvider)
- .await;
-
- assert!(actual_foldings.is_empty());
- }
-
- #[tokio::test]
- async fn entry_invalid() {
- let actual_foldings = FeatureTester::new()
- .file("main.bib", "@article{foo,")
- .main("main.bib")
- .test_folding(BibtexDeclarationFoldingProvider)
- .await;
-
- assert!(actual_foldings.is_empty());
- }
-
- #[tokio::test]
- async fn latex() {
- let actual_foldings = FeatureTester::new()
- .file("main.tex", "foo")
- .main("main.tex")
- .test_folding(BibtexDeclarationFoldingProvider)
- .await;
-
- assert!(actual_foldings.is_empty());
- }
-}
diff --git a/support/texlab/src/folding/latex_env.rs b/support/texlab/src/folding/latex_env.rs
deleted file mode 100644
index ced2567b41..0000000000
--- a/support/texlab/src/folding/latex_env.rs
+++ /dev/null
@@ -1,80 +0,0 @@
-use crate::{
- feature::{FeatureProvider, FeatureRequest},
- protocol::{FoldingRange, FoldingRangeKind, FoldingRangeParams},
- syntax::SyntaxNode,
- workspace::DocumentContent,
-};
-use async_trait::async_trait;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct LatexEnvironmentFoldingProvider;
-
-#[async_trait]
-impl FeatureProvider for LatexEnvironmentFoldingProvider {
- type Params = FoldingRangeParams;
- type Output = Vec<FoldingRange>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let mut foldings = Vec::new();
- if let DocumentContent::Latex(table) = &req.current().content {
- for env in &table.environments {
- let left_node = &table[env.left.parent];
- let right_node = &table[env.right.parent];
- let folding = FoldingRange {
- start_line: left_node.end().line,
- start_character: Some(left_node.end().character),
- end_line: right_node.start().line,
- end_character: Some(right_node.start().character),
- kind: Some(FoldingRangeKind::Region),
- };
- foldings.push(folding);
- }
- }
- foldings
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::feature::FeatureTester;
- use indoc::indoc;
-
- #[tokio::test]
- async fn multiline() {
- let actual_foldings = FeatureTester::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \begin{foo}
- \end{foo}
- "#
- ),
- )
- .main("main.tex")
- .test_folding(LatexEnvironmentFoldingProvider)
- .await;
-
- let expected_foldings = vec![FoldingRange {
- start_line: 0,
- start_character: Some(11),
- end_line: 1,
- end_character: Some(0),
- kind: Some(FoldingRangeKind::Region),
- }];
-
- assert_eq!(actual_foldings, expected_foldings);
- }
-
- #[tokio::test]
- async fn bibtex() {
- let actual_foldings = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .test_folding(LatexEnvironmentFoldingProvider)
- .await;
-
- assert!(actual_foldings.is_empty());
- }
-}
diff --git a/support/texlab/src/folding/latex_section.rs b/support/texlab/src/folding/latex_section.rs
deleted file mode 100644
index a37efe1a7a..0000000000
--- a/support/texlab/src/folding/latex_section.rs
+++ /dev/null
@@ -1,111 +0,0 @@
-use crate::{
- feature::{FeatureProvider, FeatureRequest},
- protocol::{FoldingRange, FoldingRangeKind, FoldingRangeParams},
- syntax::SyntaxNode,
- workspace::DocumentContent,
-};
-use async_trait::async_trait;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct LatexSectionFoldingProvider;
-
-#[async_trait]
-impl FeatureProvider for LatexSectionFoldingProvider {
- type Params = FoldingRangeParams;
- type Output = Vec<FoldingRange>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let mut foldings = Vec::new();
- if let DocumentContent::Latex(table) = &req.current().content {
- let sections = &table.sections;
- for i in 0..sections.len() {
- let current = &sections[i];
- if let Some(next) = sections
- .iter()
- .skip(i + 1)
- .find(|sec| current.level >= sec.level)
- {
- let next_node = &table[next.parent];
- if next_node.start().line > 0 {
- let current_node = &table[current.parent];
- let folding = FoldingRange {
- start_line: current_node.end().line,
- start_character: Some(current_node.end().character),
- end_line: next_node.start().line - 1,
- end_character: Some(0),
- kind: Some(FoldingRangeKind::Region),
- };
- foldings.push(folding);
- }
- }
- }
- }
- foldings
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::feature::FeatureTester;
- use indoc::indoc;
-
- #[tokio::test]
- async fn nested() {
- let actual_foldings = FeatureTester::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \section{Foo}
- foo
- \subsection{Bar}
- bar
- \section{Baz}
- baz
- \section{Qux}
- "#
- ),
- )
- .main("main.tex")
- .test_folding(LatexSectionFoldingProvider)
- .await;
-
- let expected_foldings = vec![
- FoldingRange {
- start_line: 0,
- start_character: Some(13),
- end_line: 3,
- end_character: Some(0),
- kind: Some(FoldingRangeKind::Region),
- },
- FoldingRange {
- start_line: 2,
- start_character: Some(16),
- end_line: 3,
- end_character: Some(0),
- kind: Some(FoldingRangeKind::Region),
- },
- FoldingRange {
- start_line: 4,
- start_character: Some(13),
- end_line: 5,
- end_character: Some(0),
- kind: Some(FoldingRangeKind::Region),
- },
- ];
-
- assert_eq!(actual_foldings, expected_foldings);
- }
-
- #[tokio::test]
- async fn bibtex() {
- let actual_foldings = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .test_folding(LatexSectionFoldingProvider)
- .await;
-
- assert!(actual_foldings.is_empty());
- }
-}
diff --git a/support/texlab/src/folding/mod.rs b/support/texlab/src/folding/mod.rs
deleted file mode 100644
index 448e3d26fc..0000000000
--- a/support/texlab/src/folding/mod.rs
+++ /dev/null
@@ -1,45 +0,0 @@
-mod bibtex_decl;
-mod latex_env;
-mod latex_section;
-
-use self::{
- bibtex_decl::BibtexDeclarationFoldingProvider, latex_env::LatexEnvironmentFoldingProvider,
- latex_section::LatexSectionFoldingProvider,
-};
-use crate::{
- feature::{ConcatProvider, FeatureProvider, FeatureRequest},
- protocol::{FoldingRange, FoldingRangeParams},
-};
-use async_trait::async_trait;
-
-pub struct FoldingProvider {
- provider: ConcatProvider<FoldingRangeParams, FoldingRange>,
-}
-
-impl FoldingProvider {
- pub fn new() -> Self {
- Self {
- provider: ConcatProvider::new(vec![
- Box::new(BibtexDeclarationFoldingProvider),
- Box::new(LatexEnvironmentFoldingProvider),
- Box::new(LatexSectionFoldingProvider),
- ]),
- }
- }
-}
-
-impl Default for FoldingProvider {
- fn default() -> Self {
- Self::new()
- }
-}
-
-#[async_trait]
-impl FeatureProvider for FoldingProvider {
- type Params = FoldingRangeParams;
- type Output = Vec<FoldingRange>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- self.provider.execute(req).await
- }
-}
diff --git a/support/texlab/src/forward_search.rs b/support/texlab/src/forward_search.rs
deleted file mode 100644
index a3911ed9f3..0000000000
--- a/support/texlab/src/forward_search.rs
+++ /dev/null
@@ -1,90 +0,0 @@
-use crate::{
- protocol::{ForwardSearchResult, ForwardSearchStatus, Options, Uri},
- workspace::Snapshot,
-};
-use log::error;
-use std::{io, path::Path, process::Stdio};
-use tokio::process::Command;
-
-pub async fn search<'a>(
- snapshot: &'a Snapshot,
- tex_uri: &'a Uri,
- line_number: u64,
- options: &Options,
- current_dir: &'a Path,
-) -> Option<ForwardSearchResult> {
- let pdf_path = snapshot
- .resolve_aux_targets(
- &snapshot.parent_subfile(tex_uri, options, current_dir)?.uri,
- options,
- current_dir,
- "pdf",
- )?
- .into_iter()
- .filter(|uri| uri.scheme() == "file")
- .filter_map(|uri| uri.to_file_path().ok())
- .find(|path| path.exists())?;
-
- if tex_uri.scheme() != "file" {
- return Some(ForwardSearchResult {
- status: ForwardSearchStatus::Failure,
- });
- }
-
- let search_options = options
- .latex
- .as_ref()
- .and_then(|opts| opts.forward_search.as_ref())
- .map(Clone::clone)
- .unwrap_or_default();
-
- if search_options.executable.is_none() || search_options.args.is_none() {
- return Some(ForwardSearchResult {
- status: ForwardSearchStatus::Unconfigured,
- });
- }
-
- let tex_path = tex_uri.to_file_path().ok()?;
- let args: Vec<String> = search_options
- .args
- .unwrap()
- .into_iter()
- .flat_map(|arg| replace_placeholder(&tex_path, &pdf_path, line_number, arg))
- .collect();
-
- let status = match spawn_process(search_options.executable.unwrap(), args).await {
- Ok(()) => ForwardSearchStatus::Success,
- Err(why) => {
- error!("Unable to execute forward search: {}", why);
- ForwardSearchStatus::Failure
- }
- };
- Some(ForwardSearchResult { status })
-}
-
-fn replace_placeholder(
- tex_file: &Path,
- pdf_file: &Path,
- line_number: u64,
- argument: String,
-) -> Option<String> {
- let result = if argument.starts_with('"') || argument.ends_with('"') {
- argument
- } else {
- argument
- .replace("%f", tex_file.to_str()?)
- .replace("%p", pdf_file.to_str()?)
- .replace("%l", &(line_number + 1).to_string())
- };
- Some(result)
-}
-
-async fn spawn_process(executable: String, args: Vec<String>) -> io::Result<()> {
- Command::new(executable)
- .args(args)
- .stdout(Stdio::null())
- .stderr(Stdio::null())
- .spawn()?
- .await?;
- Ok(())
-}
diff --git a/support/texlab/src/highlight/latex_label.rs b/support/texlab/src/highlight/latex_label.rs
deleted file mode 100644
index c6d4aab6ee..0000000000
--- a/support/texlab/src/highlight/latex_label.rs
+++ /dev/null
@@ -1,109 +0,0 @@
-use crate::{
- feature::{FeatureProvider, FeatureRequest},
- protocol::{DocumentHighlight, DocumentHighlightKind, RangeExt, TextDocumentPositionParams},
- syntax::{latex, LatexLabelKind, SyntaxNode},
- workspace::DocumentContent,
-};
-use async_trait::async_trait;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct LatexLabelHighlightProvider;
-
-#[async_trait]
-impl FeatureProvider for LatexLabelHighlightProvider {
- type Params = TextDocumentPositionParams;
- type Output = Vec<DocumentHighlight>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let mut highlights = Vec::new();
- if let DocumentContent::Latex(table) = &req.current().content {
- if let Some(name) = table
- .labels
- .iter()
- .flat_map(|label| label.names(&table))
- .find(|label| label.range().contains(req.params.position))
- .map(latex::Token::text)
- {
- for label_group in &table.labels {
- for label in label_group.names(&table) {
- if label.text() == name {
- let kind = match label_group.kind {
- LatexLabelKind::Definition => DocumentHighlightKind::Write,
- LatexLabelKind::Reference(_) => DocumentHighlightKind::Read,
- };
-
- let highlight = DocumentHighlight {
- range: label.range(),
- kind: Some(kind),
- };
- highlights.push(highlight);
- }
- }
- }
- }
- }
- highlights
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{feature::FeatureTester, protocol::Range};
- use indoc::indoc;
-
- #[tokio::test]
- async fn has_label() {
- let actual_highlights = FeatureTester::new()
- .file(
- "foo.tex",
- indoc!(
- r#"
- \label{foo}
- \ref{foo}
- "#
- ),
- )
- .main("foo.tex")
- .position(0, 7)
- .test_position(LatexLabelHighlightProvider)
- .await;
-
- let expected_highlights = vec![
- DocumentHighlight {
- range: Range::new_simple(0, 7, 0, 10),
- kind: Some(DocumentHighlightKind::Write),
- },
- DocumentHighlight {
- range: Range::new_simple(1, 5, 1, 8),
- kind: Some(DocumentHighlightKind::Read),
- },
- ];
-
- assert_eq!(actual_highlights, expected_highlights);
- }
-
- #[tokio::test]
- async fn no_label_latex() {
- let actual_highlights = FeatureTester::new()
- .file("foo.tex", "")
- .main("foo.tex")
- .position(0, 0)
- .test_position(LatexLabelHighlightProvider)
- .await;
-
- assert!(actual_highlights.is_empty());
- }
-
- #[tokio::test]
- async fn no_label_bibtex() {
- let actual_highlights = FeatureTester::new()
- .file("foo.bib", "")
- .main("foo.bib")
- .position(0, 0)
- .test_position(LatexLabelHighlightProvider)
- .await;
-
- assert!(actual_highlights.is_empty());
- }
-}
diff --git a/support/texlab/src/highlight/mod.rs b/support/texlab/src/highlight/mod.rs
deleted file mode 100644
index e47a7f99c2..0000000000
--- a/support/texlab/src/highlight/mod.rs
+++ /dev/null
@@ -1,36 +0,0 @@
-mod latex_label;
-
-use self::latex_label::LatexLabelHighlightProvider;
-use crate::{
- feature::{ConcatProvider, FeatureProvider, FeatureRequest},
- protocol::{DocumentHighlight, TextDocumentPositionParams},
-};
-use async_trait::async_trait;
-
-pub struct HighlightProvider {
- provider: ConcatProvider<TextDocumentPositionParams, DocumentHighlight>,
-}
-
-impl HighlightProvider {
- pub fn new() -> Self {
- Self {
- provider: ConcatProvider::new(vec![Box::new(LatexLabelHighlightProvider)]),
- }
- }
-}
-
-impl Default for HighlightProvider {
- fn default() -> Self {
- Self::new()
- }
-}
-
-#[async_trait]
-impl FeatureProvider for HighlightProvider {
- type Params = TextDocumentPositionParams;
- type Output = Vec<DocumentHighlight>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- self.provider.execute(req).await
- }
-}
diff --git a/support/texlab/src/hover/bibtex/entry_type.rs b/support/texlab/src/hover/bibtex/entry_type.rs
deleted file mode 100644
index 1e009565d7..0000000000
--- a/support/texlab/src/hover/bibtex/entry_type.rs
+++ /dev/null
@@ -1,116 +0,0 @@
-use crate::{
- feature::{FeatureProvider, FeatureRequest},
- protocol::{
- Hover, HoverContents, MarkupContent, MarkupKind, RangeExt, TextDocumentPositionParams,
- },
- syntax::{SyntaxNode, LANGUAGE_DATA},
-};
-use async_trait::async_trait;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct BibtexEntryTypeHoverProvider;
-
-#[async_trait]
-impl FeatureProvider for BibtexEntryTypeHoverProvider {
- type Params = TextDocumentPositionParams;
- type Output = Option<Hover>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let tree = req.current().content.as_bibtex()?;
- for entry in tree
- .children(tree.root)
- .filter_map(|node| tree.as_entry(node))
- {
- if entry.ty.range().contains(req.params.position) {
- let ty = &entry.ty.text()[1..];
- let docs = LANGUAGE_DATA.entry_type_documentation(ty)?;
- return Some(Hover {
- contents: HoverContents::Markup(MarkupContent {
- kind: MarkupKind::Markdown,
- value: docs.into(),
- }),
- range: Some(entry.ty.range()),
- });
- }
- }
- None
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{feature::FeatureTester, protocol::Range};
-
- #[tokio::test]
- async fn empty_latex_document() {
- let actual_hover = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_position(BibtexEntryTypeHoverProvider)
- .await;
-
- assert_eq!(actual_hover, None);
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let actual_hover = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_position(BibtexEntryTypeHoverProvider)
- .await;
-
- assert_eq!(actual_hover, None);
- }
-
- #[tokio::test]
- async fn known_entry_type() {
- let actual_hover = FeatureTester::new()
- .file("main.bib", "@article{foo,}")
- .main("main.bib")
- .position(0, 3)
- .test_position(BibtexEntryTypeHoverProvider)
- .await
- .unwrap();
-
- let expected_hover = Hover {
- contents: HoverContents::Markup(MarkupContent {
- kind: MarkupKind::Markdown,
- value: LANGUAGE_DATA
- .entry_type_documentation("article")
- .unwrap()
- .into(),
- }),
- range: Some(Range::new_simple(0, 0, 0, 8)),
- };
-
- assert_eq!(actual_hover, expected_hover);
- }
-
- #[tokio::test]
- async fn unknown_entry_type() {
- let actual_hover = FeatureTester::new()
- .file("main.bib", "@foo{bar,}")
- .main("main.bib")
- .position(0, 3)
- .test_position(BibtexEntryTypeHoverProvider)
- .await;
-
- assert_eq!(actual_hover, None);
- }
-
- #[tokio::test]
- async fn entry_key() {
- let actual_hover = FeatureTester::new()
- .file("main.bib", "@article{foo,}")
- .main("main.bib")
- .position(0, 11)
- .test_position(BibtexEntryTypeHoverProvider)
- .await;
-
- assert_eq!(actual_hover, None);
- }
-}
diff --git a/support/texlab/src/hover/bibtex/field.rs b/support/texlab/src/hover/bibtex/field.rs
deleted file mode 100644
index 4b00f3364c..0000000000
--- a/support/texlab/src/hover/bibtex/field.rs
+++ /dev/null
@@ -1,111 +0,0 @@
-use crate::{
- feature::{FeatureProvider, FeatureRequest},
- protocol::{
- Hover, HoverContents, MarkupContent, MarkupKind, RangeExt, TextDocumentPositionParams,
- },
- syntax::{SyntaxNode, LANGUAGE_DATA},
-};
-use async_trait::async_trait;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct BibtexFieldHoverProvider;
-
-#[async_trait]
-impl FeatureProvider for BibtexFieldHoverProvider {
- type Params = TextDocumentPositionParams;
- type Output = Option<Hover>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let tree = req.current().content.as_bibtex()?;
- let name = tree
- .find(req.params.position)
- .into_iter()
- .filter_map(|node| tree.as_field(node))
- .map(|field| &field.name)
- .find(|name| name.range().contains(req.params.position))?;
-
- let docs = LANGUAGE_DATA.field_documentation(name.text())?;
- Some(Hover {
- contents: HoverContents::Markup(MarkupContent {
- kind: MarkupKind::Markdown,
- value: docs.into(),
- }),
- range: Some(name.range()),
- })
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{feature::FeatureTester, protocol::Range};
-
- #[tokio::test]
- async fn empty_latex_document() {
- let actual_hover = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_position(BibtexFieldHoverProvider)
- .await;
-
- assert_eq!(actual_hover, None);
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let actual_hover = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_position(BibtexFieldHoverProvider)
- .await;
-
- assert_eq!(actual_hover, None);
- }
-
- #[tokio::test]
- async fn known_field() {
- let actual_hover = FeatureTester::new()
- .file("main.bib", "@article{foo, author = bar}")
- .main("main.bib")
- .position(0, 15)
- .test_position(BibtexFieldHoverProvider)
- .await
- .unwrap();
-
- let expected_hover = Hover {
- contents: HoverContents::Markup(MarkupContent {
- kind: MarkupKind::Markdown,
- value: LANGUAGE_DATA.field_documentation("author").unwrap().into(),
- }),
- range: Some(Range::new_simple(0, 14, 0, 20)),
- };
-
- assert_eq!(actual_hover, expected_hover);
- }
-
- #[tokio::test]
- async fn unknown_field() {
- let actual_hover = FeatureTester::new()
- .file("main.bib", "@article{foo, bar = baz}")
- .main("main.bib")
- .position(0, 15)
- .test_position(BibtexFieldHoverProvider)
- .await;
-
- assert_eq!(actual_hover, None);
- }
-
- #[tokio::test]
- async fn entry_key() {
- let actual_hover = FeatureTester::new()
- .file("main.bib", "@article{foo, bar = baz}")
- .main("main.bib")
- .position(0, 11)
- .test_position(BibtexFieldHoverProvider)
- .await;
-
- assert_eq!(actual_hover, None);
- }
-}
diff --git a/support/texlab/src/hover/bibtex/mod.rs b/support/texlab/src/hover/bibtex/mod.rs
deleted file mode 100644
index 59ff356fa0..0000000000
--- a/support/texlab/src/hover/bibtex/mod.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-pub mod entry_type;
-pub mod field;
-pub mod string_reference;
diff --git a/support/texlab/src/hover/bibtex/string_reference.rs b/support/texlab/src/hover/bibtex/string_reference.rs
deleted file mode 100644
index 46724a4f60..0000000000
--- a/support/texlab/src/hover/bibtex/string_reference.rs
+++ /dev/null
@@ -1,165 +0,0 @@
-use crate::{
- feature::{FeatureProvider, FeatureRequest},
- protocol::{
- BibtexFormattingOptions, Hover, HoverContents, MarkupContent, MarkupKind, Position,
- TextDocumentPositionParams,
- },
- syntax::{bibtex, SyntaxNode},
-};
-use async_trait::async_trait;
-use petgraph::graph::NodeIndex;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct BibtexStringReferenceHoverProvider;
-
-#[async_trait]
-impl FeatureProvider for BibtexStringReferenceHoverProvider {
- type Params = TextDocumentPositionParams;
- type Output = Option<Hover>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let tree = req.current().content.as_bibtex()?;
- let reference = Self::find_reference(tree, req.params.position)?;
- for string_node in tree.children(tree.root) {
- let hover = Self::find_definition(tree, string_node, reference);
- if hover.is_some() {
- return hover;
- }
- }
- None
- }
-}
-
-impl BibtexStringReferenceHoverProvider {
- fn find_reference(tree: &bibtex::Tree, pos: Position) -> Option<&bibtex::Token> {
- let mut results = tree.find(pos);
- results.reverse();
- match (
- &tree.graph[results[0]],
- results.get(1).map(|node| &tree.graph[*node]),
- ) {
- (bibtex::Node::Word(reference), Some(bibtex::Node::Concat(_))) => {
- Some(&reference.token)
- }
- (bibtex::Node::Word(reference), Some(bibtex::Node::Field(_))) => Some(&reference.token),
- _ => None,
- }
- }
-
- fn find_definition(
- tree: &bibtex::Tree,
- string_node: NodeIndex,
- reference: &bibtex::Token,
- ) -> Option<Hover> {
- let string = tree.as_string(string_node)?;
- if string.name.as_ref()?.text() != reference.text() {
- return None;
- }
-
- let options = BibtexFormattingOptions {
- line_length: None,
- formatter: None,
- };
- let text = bibtex::format(
- tree,
- tree.children(string_node).next()?,
- bibtex::FormattingParams {
- tab_size: 4,
- insert_spaces: true,
- options: &options,
- },
- );
- Some(Hover {
- contents: HoverContents::Markup(MarkupContent {
- kind: MarkupKind::PlainText,
- value: text,
- }),
- range: Some(reference.range()),
- })
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- feature::FeatureTester,
- protocol::{Range, RangeExt},
- };
- use indoc::indoc;
-
- #[tokio::test]
- async fn empty_latex_document() {
- let actual_hover = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_position(BibtexStringReferenceHoverProvider)
- .await;
-
- assert_eq!(actual_hover, None);
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let actual_hover = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_position(BibtexStringReferenceHoverProvider)
- .await;
-
- assert_eq!(actual_hover, None);
- }
-
- #[tokio::test]
- async fn inside_reference() {
- let actual_hover = FeatureTester::new()
- .file(
- "main.bib",
- indoc!(
- r#"
- @string{foo = "Foo"}
- @string{bar = "Bar"}
- @article{baz, author = bar}
- "#
- ),
- )
- .main("main.bib")
- .position(2, 24)
- .test_position(BibtexStringReferenceHoverProvider)
- .await
- .unwrap();
-
- let expected_hover = Hover {
- contents: HoverContents::Markup(MarkupContent {
- kind: MarkupKind::PlainText,
- value: "\"Bar\"".into(),
- }),
- range: Some(Range::new_simple(2, 23, 2, 26)),
- };
-
- assert_eq!(actual_hover, expected_hover);
- }
-
- #[tokio::test]
- async fn outside_reference() {
- let actual_hover = FeatureTester::new()
- .file(
- "main.bib",
- indoc!(
- r#"
- @string{foo = "Foo"}
- @string{bar = "Bar"}
- @article{baz, author = bar}
- "#
- ),
- )
- .main("main.bib")
- .position(2, 20)
- .test_position(BibtexStringReferenceHoverProvider)
- .await;
-
- assert_eq!(actual_hover, None);
- }
-}
diff --git a/support/texlab/src/hover/latex/citation.rs b/support/texlab/src/hover/latex/citation.rs
deleted file mode 100644
index 8b5468e52e..0000000000
--- a/support/texlab/src/hover/latex/citation.rs
+++ /dev/null
@@ -1,179 +0,0 @@
-use crate::{
- citeproc::render_citation,
- feature::{FeatureProvider, FeatureRequest},
- protocol::{Hover, HoverContents, RangeExt, TextDocumentPositionParams},
- syntax::{bibtex, Span, SyntaxNode},
- workspace::DocumentContent,
-};
-use async_trait::async_trait;
-use log::warn;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct LatexCitationHoverProvider;
-
-#[async_trait]
-impl FeatureProvider for LatexCitationHoverProvider {
- type Params = TextDocumentPositionParams;
- type Output = Option<Hover>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let (tree, src_key, entry) = Self::get_entry(req)?;
- if entry.is_comment() {
- None
- } else {
- let key = entry.key.as_ref()?;
- match render_citation(&tree, key.text()) {
- Some(markdown) => Some(Hover {
- contents: HoverContents::Markup(markdown),
- range: Some(src_key.range()),
- }),
- None => {
- warn!("Failed to render entry: {}", key.text());
- None
- }
- }
- }
- }
-}
-
-impl LatexCitationHoverProvider {
- fn get_entry(
- req: &FeatureRequest<TextDocumentPositionParams>,
- ) -> Option<(&bibtex::Tree, &Span, &bibtex::Entry)> {
- let key = Self::get_key(req)?;
- for tree in req
- .related()
- .iter()
- .filter_map(|doc| doc.content.as_bibtex())
- {
- for entry in tree
- .children(tree.root)
- .filter_map(|node| tree.as_entry(node))
- {
- if let Some(current_key) = &entry.key {
- if current_key.text() == key.text {
- return Some((tree, key, entry));
- }
- }
- }
- }
- None
- }
-
- fn get_key(req: &FeatureRequest<TextDocumentPositionParams>) -> Option<&Span> {
- match &req.current().content {
- DocumentContent::Latex(table) => table
- .citations
- .iter()
- .flat_map(|citation| citation.keys(&table))
- .find(|key| key.range().contains(req.params.position))
- .map(|token| &token.span),
- DocumentContent::Bibtex(tree) => tree
- .children(tree.root)
- .filter_map(|node| tree.as_entry(node))
- .filter_map(|entry| entry.key.as_ref())
- .find(|key| key.range().contains(req.params.position))
- .map(|token| &token.span),
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- feature::FeatureTester,
- protocol::{MarkupContent, MarkupKind, Range, RangeExt},
- };
- use indoc::indoc;
-
- #[tokio::test]
- async fn empty_latex_document() {
- let actual_hover = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_position(LatexCitationHoverProvider)
- .await;
-
- assert_eq!(actual_hover, None);
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let actual_hover = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_position(LatexCitationHoverProvider)
- .await;
-
- assert_eq!(actual_hover, None);
- }
-
- #[tokio::test]
- async fn inside_label() {
- let actual_hover = FeatureTester::new()
- .file(
- "main.bib",
- "@article{foo, author = {Foo Bar}, title = {Baz Qux}, year = 1337}",
- )
- .file(
- "main.tex",
- indoc!(
- r#"
- \addbibresource{main.bib}
- \cite{foo}
- "#
- ),
- )
- .main("main.tex")
- .position(1, 7)
- .test_position(LatexCitationHoverProvider)
- .await
- .unwrap();
-
- let expected_hover = Hover {
- contents: HoverContents::Markup(MarkupContent {
- kind: MarkupKind::Markdown,
- value: "Bar, F. (1337). *Baz Qux*.".into(),
- }),
- range: Some(Range::new_simple(1, 6, 1, 9)),
- };
-
- assert_eq!(actual_hover, expected_hover);
- }
-
- #[tokio::test]
- async fn inside_entry() {
- let actual_hover = FeatureTester::new()
- .file(
- "main.bib",
- "@article{foo, author = {Foo Bar}, title = {Baz Qux}, year = 1337}",
- )
- .file(
- "main.tex",
- indoc!(
- r#"
- \addbibresource{main.bib}
- \cite{foo}
- "#
- ),
- )
- .main("main.bib")
- .position(0, 11)
- .test_position(LatexCitationHoverProvider)
- .await
- .unwrap();
-
- let expected_hover = Hover {
- contents: HoverContents::Markup(MarkupContent {
- kind: MarkupKind::Markdown,
- value: "Bar, F. (1337). *Baz Qux*.".into(),
- }),
- range: Some(Range::new_simple(0, 9, 0, 12)),
- };
-
- assert_eq!(actual_hover, expected_hover);
- }
-}
diff --git a/support/texlab/src/hover/latex/component.rs b/support/texlab/src/hover/latex/component.rs
deleted file mode 100644
index 5285ec795a..0000000000
--- a/support/texlab/src/hover/latex/component.rs
+++ /dev/null
@@ -1,95 +0,0 @@
-use crate::{
- components::COMPONENT_DATABASE,
- feature::{FeatureProvider, FeatureRequest},
- protocol::{Hover, HoverContents, RangeExt, TextDocumentPositionParams},
- syntax::{LatexIncludeKind, SyntaxNode},
-};
-use async_trait::async_trait;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct LatexComponentHoverProvider;
-
-#[async_trait]
-impl FeatureProvider for LatexComponentHoverProvider {
- type Params = TextDocumentPositionParams;
- type Output = Option<Hover>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let table = req.current().content.as_latex()?;
- for include in &table.includes {
- match include.kind {
- LatexIncludeKind::Package | LatexIncludeKind::Class => {
- for path in include.paths(&table) {
- if path.range().contains(req.params.position) {
- let docs = COMPONENT_DATABASE.documentation(path.text())?;
- return Some(Hover {
- contents: HoverContents::Markup(docs),
- range: Some(path.range()),
- });
- }
- }
- }
- _ => {}
- }
- }
- None
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- feature::FeatureTester,
- protocol::{Range, RangeExt},
- };
-
- #[tokio::test]
- async fn empty_latex_document() {
- let actual_hover = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_position(LatexComponentHoverProvider)
- .await;
-
- assert_eq!(actual_hover, None);
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let actual_hover = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_position(LatexComponentHoverProvider)
- .await;
-
- assert_eq!(actual_hover, None);
- }
-
- #[tokio::test]
- async fn known_package() {
- let actual_hover = FeatureTester::new()
- .file("main.tex", r#"\usepackage{amsmath}"#)
- .main("main.tex")
- .position(0, 15)
- .test_position(LatexComponentHoverProvider)
- .await
- .unwrap();
-
- assert_eq!(actual_hover.range.unwrap(), Range::new_simple(0, 12, 0, 19));
- }
-
- #[tokio::test]
- async fn unknown_class() {
- let actual_hover = FeatureTester::new()
- .file("main.tex", r#"\documentclass{abcdefghijklmnop}"#)
- .main("main.tex")
- .position(0, 20)
- .test_position(LatexComponentHoverProvider)
- .await;
-
- assert_eq!(actual_hover, None);
- }
-}
diff --git a/support/texlab/src/hover/latex/label.rs b/support/texlab/src/hover/latex/label.rs
deleted file mode 100644
index 66724d442b..0000000000
--- a/support/texlab/src/hover/latex/label.rs
+++ /dev/null
@@ -1,118 +0,0 @@
-use crate::{
- feature::{DocumentView, FeatureProvider, FeatureRequest},
- outline::{Outline, OutlineContext},
- protocol::{Hover, HoverContents, Position, RangeExt, TextDocumentPositionParams},
- syntax::{latex, LatexLabelKind, SyntaxNode},
- workspace::{Document, DocumentContent},
-};
-use async_trait::async_trait;
-use std::sync::Arc;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct LatexLabelHoverProvider;
-
-#[async_trait]
-impl FeatureProvider for LatexLabelHoverProvider {
- type Params = TextDocumentPositionParams;
- type Output = Option<Hover>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let table = req.current().content.as_latex()?;
- let reference = Self::find_reference(table, req.params.position)?;
- let (doc, def) = Self::find_definition(&req.view, reference)?;
-
- let snapshot = Arc::clone(&req.view.snapshot);
- let view = DocumentView::analyze(snapshot, doc, &req.options, &req.current_dir);
- let outline = Outline::analyze(&view, &req.options, &req.current_dir);
- let outline_ctx = OutlineContext::parse(&view, &outline, def)?;
- let markup = outline_ctx.documentation();
- Some(Hover {
- contents: HoverContents::Markup(markup),
- range: Some(reference.range()),
- })
- }
-}
-
-impl LatexLabelHoverProvider {
- fn find_reference(table: &latex::SymbolTable, pos: Position) -> Option<&latex::Token> {
- for label in &table.labels {
- let names = label.names(&table);
- if names.len() == 1 && table[label.parent].range().contains(pos) {
- return Some(&label.names(&table)[0]);
- }
-
- for name in &names {
- if name.range().contains(pos) {
- return Some(name);
- }
- }
- }
- None
- }
-
- fn find_definition(
- view: &DocumentView,
- reference: &latex::Token,
- ) -> Option<(Arc<Document>, latex::Label)> {
- for doc in &view.related {
- if let DocumentContent::Latex(table) = &doc.content {
- for label in &table.labels {
- if label.kind == LatexLabelKind::Definition {
- for name in label.names(&table) {
- if name.text() == reference.text() {
- return Some((Arc::clone(&doc), *label));
- }
- }
- }
- }
- }
- }
- None
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- feature::FeatureTester,
- protocol::{Range, RangeExt},
- };
-
- #[tokio::test]
- async fn empty_latex_document() {
- let actual_hover = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_position(LatexLabelHoverProvider)
- .await;
-
- assert_eq!(actual_hover, None);
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let actual_hover = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_position(LatexLabelHoverProvider)
- .await;
-
- assert_eq!(actual_hover, None);
- }
-
- #[tokio::test]
- async fn section() {
- let actual_hover = FeatureTester::new()
- .file("main.tex", r#"\section{Foo}\label{sec:foo}"#)
- .main("main.tex")
- .position(0, 23)
- .test_position(LatexLabelHoverProvider)
- .await
- .unwrap();
-
- assert_eq!(actual_hover.range.unwrap(), Range::new_simple(0, 20, 0, 27));
- }
-}
diff --git a/support/texlab/src/hover/latex/mod.rs b/support/texlab/src/hover/latex/mod.rs
deleted file mode 100644
index c119bfc832..0000000000
--- a/support/texlab/src/hover/latex/mod.rs
+++ /dev/null
@@ -1,6 +0,0 @@
-#[cfg(feature = "citation")]
-pub mod citation;
-
-pub mod component;
-pub mod label;
-pub mod preview;
diff --git a/support/texlab/src/hover/latex/preview.rs b/support/texlab/src/hover/latex/preview.rs
deleted file mode 100644
index 8d2ca00cb5..0000000000
--- a/support/texlab/src/hover/latex/preview.rs
+++ /dev/null
@@ -1,338 +0,0 @@
-use crate::{
- components::COMPONENT_DATABASE,
- feature::{FeatureProvider, FeatureRequest},
- protocol::{
- ClientCapabilitiesExt, Hover, HoverContents, MarkupContent, MarkupKind, Range, RangeExt,
- TextDocumentPositionParams,
- },
- syntax::{latex, CharStream, LatexIncludeKind, SyntaxNode},
- tex::{CompileError, CompileParams, DistributionKind, Format},
- workspace::DocumentContent,
-};
-use async_trait::async_trait;
-use image::{png::PNGEncoder, ColorType, DynamicImage, GenericImageView, ImageBuffer, RgbaImage};
-use io::Cursor;
-use log::warn;
-use std::{io, process::Stdio, time::Duration};
-use tempfile::TempDir;
-use thiserror::Error;
-use tokio::process::Command;
-
-const PREVIEW_ENVIRONMENTS: &[&str] = &[
- "align",
- "alignat",
- "aligned",
- "alignedat",
- "algorithmic",
- "array",
- "Bmatrix",
- "bmatrix",
- "cases",
- "CD",
- "eqnarray",
- "equation",
- "gather",
- "gathered",
- "matrix",
- "multline",
- "pmatrix",
- "smallmatrix",
- "split",
- "subarray",
- "Vmatrix",
- "vmatrix",
-];
-
-const IGNORED_PACKAGES: &[&str] = &["biblatex", "pgf", "tikz"];
-
-#[derive(Debug, Clone, Copy)]
-enum MathElement {
- Environment(latex::Environment),
- Equation(latex::Equation),
- Inline(latex::Inline),
-}
-
-impl MathElement {
- fn range(&self, tree: &latex::Tree) -> Range {
- match self {
- Self::Environment(env) => env.range(tree),
- Self::Equation(eq) => eq.range(tree),
- Self::Inline(inline) => inline.range(tree),
- }
- }
-}
-
-#[derive(Debug, Error)]
-enum RenderError {
- #[error("an I/O error occurred: `{0}`")]
- IO(#[from] io::Error),
- #[error("a compile error occurred: `{0}`")]
- Compile(#[from] CompileError),
- #[error("compilation failed")]
- DviNotFound,
- #[error("dvipng is not installed")]
- DviPngNotInstalled,
- #[error("calling dvipng failed")]
- DviPngFaulty,
- #[error("failed to decode image")]
- DecodeImage,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct LatexPreviewHoverProvider;
-
-impl LatexPreviewHoverProvider {
- fn is_preview_environment(
- req: &FeatureRequest<TextDocumentPositionParams>,
- table: &latex::SymbolTable,
- environment: latex::Environment,
- ) -> bool {
- let canonical_name = environment
- .left
- .name(&table)
- .map(latex::Token::text)
- .unwrap_or_default()
- .replace('*', "");
-
- PREVIEW_ENVIRONMENTS.contains(&canonical_name.as_ref())
- || Self::theorem_environments(req).contains(&canonical_name.as_ref())
- }
-
- fn theorem_environments(req: &FeatureRequest<TextDocumentPositionParams>) -> Vec<&str> {
- let mut names = Vec::new();
- for doc in req.related() {
- if let DocumentContent::Latex(table) = &doc.content {
- table
- .theorem_definitions
- .iter()
- .map(|thm| thm.name(&table).text())
- .for_each(|thm| names.push(thm));
- }
- }
- names
- }
-
- async fn render(
- req: &FeatureRequest<TextDocumentPositionParams>,
- range: Range,
- ) -> Result<Hover, RenderError> {
- let code = Self::generate_code(req, range);
- let params = CompileParams {
- file_name: "preview.tex",
- code: &code,
- format: Format::Latex,
- timeout: Duration::from_secs(10),
- };
- let dir = req.distro.compile(params).await?.dir;
- if !dir.path().join("preview.dvi").exists() {
- return Err(RenderError::DviNotFound);
- }
-
- let img = Self::add_margin(Self::dvipng(&dir).await?);
- let base64 = Self::encode_image(img);
- let markdown = format!("![preview](data:image/png;base64,{})", base64);
- dir.close()?;
- Ok(Hover {
- contents: HoverContents::Markup(MarkupContent {
- kind: MarkupKind::Markdown,
- value: markdown,
- }),
- range: Some(range),
- })
- }
-
- fn generate_code(req: &FeatureRequest<TextDocumentPositionParams>, range: Range) -> String {
- let mut code = String::new();
- code.push_str("\\documentclass{article}\n");
- code.push_str("\\thispagestyle{empty}\n");
- Self::generate_includes(req, &mut code);
- Self::generate_command_definitions(req, &mut code);
- Self::generate_math_operators(req, &mut code);
- Self::generate_theorem_definitions(req, &mut code);
- code.push_str("\\begin{document}\n");
- code.push_str(&CharStream::extract(&req.current().text, range));
- code.push('\n');
- code.push_str("\\end{document}\n");
- code
- }
-
- fn generate_includes(req: &FeatureRequest<TextDocumentPositionParams>, code: &mut String) {
- for doc in req.related() {
- if let DocumentContent::Latex(table) = &doc.content {
- let text = &doc.text;
- for include in &table.includes {
- if include.kind == LatexIncludeKind::Package {
- if include
- .paths(&table)
- .iter()
- .all(|path| IGNORED_PACKAGES.contains(&path.text()))
- {
- continue;
- }
-
- if include
- .paths(&table)
- .iter()
- .map(|path| format!("{}.sty", path.text()))
- .any(|name| !COMPONENT_DATABASE.exists(&name))
- {
- continue;
- }
-
- code.push_str(&CharStream::extract(&text, table[include.parent].range()));
- code.push('\n');
- }
- }
- }
- }
- }
-
- fn generate_command_definitions(
- req: &FeatureRequest<TextDocumentPositionParams>,
- code: &mut String,
- ) {
- for doc in req.related() {
- if let DocumentContent::Latex(table) = &doc.content {
- table
- .command_definitions
- .iter()
- .filter(|def| !def.definition_name(&table).contains("@"))
- .map(|def| CharStream::extract(&doc.text, table[def.parent].range()))
- .for_each(|def| {
- code.push_str(&def);
- code.push('\n');
- });
- }
- }
- }
-
- fn generate_math_operators(
- req: &FeatureRequest<TextDocumentPositionParams>,
- code: &mut String,
- ) {
- for doc in req.related() {
- if let DocumentContent::Latex(table) = &doc.content {
- table
- .math_operators
- .iter()
- .filter(|op| !op.definition_name(&table).contains("@"))
- .map(|op| CharStream::extract(&doc.text, table[op.parent].range()))
- .for_each(|op| {
- code.push_str(&op);
- code.push('\n');
- });
- }
- }
- }
-
- fn generate_theorem_definitions(
- req: &FeatureRequest<TextDocumentPositionParams>,
- code: &mut String,
- ) {
- for doc in req.related() {
- if let DocumentContent::Latex(table) = &doc.content {
- table
- .theorem_definitions
- .iter()
- .map(|thm| CharStream::extract(&doc.text, table[thm.parent].range()))
- .for_each(|thm| {
- code.push_str(&thm);
- code.push('\n');
- })
- }
- }
- }
-
- async fn dvipng(dir: &TempDir) -> Result<DynamicImage, RenderError> {
- let process = Command::new("dvipng")
- .args(&["-D", "175", "-T", "tight", "preview.dvi"])
- .current_dir(dir.path())
- .stdout(Stdio::null())
- .stderr(Stdio::null())
- .spawn()
- .map_err(|_| RenderError::DviPngNotInstalled)?;
-
- process.await.map_err(|_| RenderError::DviPngFaulty)?;
-
- let png_file = dir.path().join("preview1.png");
- let png = image::open(png_file).map_err(|_| RenderError::DecodeImage)?;
- Ok(png)
- }
-
- fn add_margin(image: DynamicImage) -> RgbaImage {
- let margin = 5;
- let width = image.width() + 2 * margin;
- let height = image.height() + 2 * margin;
- let mut result =
- ImageBuffer::from_pixel(width, height, image::Rgba([0xFF, 0xFF, 0xFF, 0xFF]));
-
- for x in 0..image.width() {
- for y in 0..image.height() {
- let pixel = image.get_pixel(x, y);
- result.put_pixel(x + margin, y + margin, pixel);
- }
- }
- result
- }
-
- fn encode_image(image: RgbaImage) -> String {
- let mut image_buf = Cursor::new(Vec::new());
- let png_encoder = PNGEncoder::new(&mut image_buf);
- let width = image.width();
- let height = image.height();
- png_encoder
- .encode(&image.into_raw(), width, height, ColorType::Rgba8)
- .unwrap();
- base64::encode(&image_buf.into_inner())
- }
-}
-
-#[async_trait]
-impl FeatureProvider for LatexPreviewHoverProvider {
- type Params = TextDocumentPositionParams;
- type Output = Option<Hover>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- if !req.client_capabilities.has_hover_markdown_support()
- || req.distro.kind() == DistributionKind::Tectonic
- {
- return None;
- }
-
- if let DocumentContent::Latex(table) = &req.current().content {
- let mut elements = Vec::new();
- table
- .inlines
- .iter()
- .map(|inline| MathElement::Inline(*inline))
- .for_each(|inline| elements.push(inline));
-
- table
- .equations
- .iter()
- .map(|eq| MathElement::Equation(*eq))
- .for_each(|eq| elements.push(eq));
-
- table
- .environments
- .iter()
- .filter(|env| Self::is_preview_environment(req, table, **env))
- .map(|env| MathElement::Environment(*env))
- .for_each(|env| elements.push(env));
-
- let range = elements
- .iter()
- .map(|elem| elem.range(&table))
- .find(|range| range.contains(req.params.position))?;
-
- return match Self::render(req, range).await {
- Ok(hover) => Some(hover),
- Err(why) => {
- warn!("Preview failed: {}", why);
- None
- }
- };
- }
- None
- }
-}
diff --git a/support/texlab/src/hover/mod.rs b/support/texlab/src/hover/mod.rs
deleted file mode 100644
index e740657add..0000000000
--- a/support/texlab/src/hover/mod.rs
+++ /dev/null
@@ -1,58 +0,0 @@
-mod bibtex;
-mod latex;
-
-#[cfg(feature = "citation")]
-use self::latex::citation::LatexCitationHoverProvider;
-
-use self::{
- bibtex::{
- entry_type::BibtexEntryTypeHoverProvider, field::BibtexFieldHoverProvider,
- string_reference::BibtexStringReferenceHoverProvider,
- },
- latex::{
- component::LatexComponentHoverProvider, label::LatexLabelHoverProvider,
- preview::LatexPreviewHoverProvider,
- },
-};
-use crate::{
- feature::{ChoiceProvider, FeatureProvider, FeatureRequest},
- protocol::{Hover, TextDocumentPositionParams},
-};
-use async_trait::async_trait;
-
-pub struct HoverProvider {
- provider: ChoiceProvider<TextDocumentPositionParams, Hover>,
-}
-
-impl HoverProvider {
- pub fn new() -> Self {
- Self {
- provider: ChoiceProvider::new(vec![
- Box::new(BibtexEntryTypeHoverProvider),
- Box::new(BibtexStringReferenceHoverProvider),
- Box::new(BibtexFieldHoverProvider),
- #[cfg(feature = "citation")]
- Box::new(LatexCitationHoverProvider),
- Box::new(LatexComponentHoverProvider),
- Box::new(LatexLabelHoverProvider),
- Box::new(LatexPreviewHoverProvider),
- ]),
- }
- }
-}
-
-impl Default for HoverProvider {
- fn default() -> Self {
- Self::new()
- }
-}
-
-#[async_trait]
-impl FeatureProvider for HoverProvider {
- type Params = TextDocumentPositionParams;
- type Output = Option<Hover>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- self.provider.execute(req).await
- }
-}
diff --git a/support/texlab/src/label.rs b/support/texlab/src/label.rs
new file mode 100644
index 0000000000..eead07814e
--- /dev/null
+++ b/support/texlab/src/label.rs
@@ -0,0 +1,291 @@
+use std::str::FromStr;
+
+use cstree::TextRange;
+use lsp_types::{MarkupContent, MarkupKind};
+
+use crate::{
+ syntax::{
+ latex::{self, HasBrack, HasCurly},
+ CstNode,
+ },
+ WorkspaceSubset, LANGUAGE_DATA,
+};
+
+use self::LabelledObject::*;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum LabelledFloatKind {
+ Figure,
+ Table,
+ Listing,
+ Algorithm,
+}
+
+impl LabelledFloatKind {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Figure => "Figure",
+ Self::Table => "Table",
+ Self::Listing => "Listing",
+ Self::Algorithm => "Algorithm",
+ }
+ }
+}
+
+impl FromStr for LabelledFloatKind {
+ type Err = ();
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ "figure" | "subfigure" => Ok(Self::Figure),
+ "table" | "subtable" => Ok(Self::Table),
+ "listing" | "lstlisting" => Ok(Self::Listing),
+ "algorithm" => Ok(Self::Algorithm),
+ _ => Err(()),
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum LabelledObject {
+ Section {
+ prefix: &'static str,
+ text: String,
+ },
+ Float {
+ kind: LabelledFloatKind,
+ caption: String,
+ },
+ Theorem {
+ kind: String,
+ description: Option<String>,
+ },
+ Equation,
+ EnumItem,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct RenderedLabel {
+ pub range: TextRange,
+ pub number: Option<String>,
+ pub object: LabelledObject,
+}
+
+impl RenderedLabel {
+ pub fn reference(&self) -> String {
+ match &self.number {
+ Some(number) => match &self.object {
+ Section { prefix, text } => format!("{} {} ({})", prefix, number, text),
+ Float { kind, caption } => format!("{} {}: {}", kind.as_str(), number, caption),
+ Theorem {
+ kind,
+ description: None,
+ } => format!("{} {}", kind, number),
+ Theorem {
+ kind,
+ description: Some(description),
+ } => format!("{} {} ({})", kind, number, description),
+ Equation => format!("Equation ({})", number),
+ EnumItem => format!("Item {}", number),
+ },
+ None => match &self.object {
+ Section { prefix, text } => format!("{} ({})", prefix, text),
+ Float { kind, caption } => format!("{}: {}", kind.as_str(), caption),
+ Theorem {
+ kind,
+ description: None,
+ } => kind.into(),
+ Theorem {
+ kind,
+ description: Some(description),
+ } => format!("{} ({})", kind, description),
+ Equation => "Equation".into(),
+ EnumItem => "Item".into(),
+ },
+ }
+ }
+
+ pub fn detail(&self) -> Option<String> {
+ match &self.object {
+ Section { .. } | Theorem { .. } | Equation | EnumItem => Some(self.reference()),
+ Float { kind, .. } => {
+ let result = match &self.number {
+ Some(number) => format!("{} {}", kind.as_str(), number),
+ None => kind.as_str().to_owned(),
+ };
+ Some(result)
+ }
+ }
+ }
+
+ pub fn documentation(&self) -> MarkupContent {
+ MarkupContent {
+ kind: MarkupKind::PlainText,
+ value: self.reference(),
+ }
+ }
+}
+
+pub fn render_label<'a>(
+ subset: &'a WorkspaceSubset,
+ label_name: &str,
+ mut label: Option<latex::LabelDefinition<'a>>,
+) -> Option<RenderedLabel> {
+ let mut number = find_label_number(subset, label_name).map(ToString::to_string);
+
+ for document in &subset.documents {
+ if let Some(data) = document.data.as_latex() {
+ label = label.or_else(|| find_label_definition(&data.root, label_name));
+ }
+ }
+
+ label?.syntax().ancestors().find_map(|parent| {
+ render_label_float(parent, &mut number)
+ .or_else(|| render_label_section(parent, &mut number))
+ .or_else(|| render_label_enum_item(parent, &mut number))
+ .or_else(|| render_label_equation(parent, &mut number))
+ .or_else(|| render_label_theorem(subset, parent, &mut number))
+ })
+}
+
+pub fn find_label_definition<'a>(
+ root: &'a latex::SyntaxNode,
+ label_name: &str,
+) -> Option<latex::LabelDefinition<'a>> {
+ root.descendants()
+ .filter_map(latex::LabelDefinition::cast)
+ .find(|label| {
+ label
+ .name()
+ .and_then(|name| name.word())
+ .map(|name| name.text())
+ == Some(label_name)
+ })
+}
+
+pub fn find_label_number<'a>(subset: &'a WorkspaceSubset, label_name: &str) -> Option<&'a str> {
+ subset.documents.iter().find_map(|document| {
+ document
+ .data
+ .as_latex()
+ .and_then(|data| data.extras.label_numbers_by_name.get(label_name))
+ .map(|number| number.as_str())
+ })
+}
+
+fn render_label_float(
+ parent: &latex::SyntaxNode,
+ number: &mut Option<String>,
+) -> Option<RenderedLabel> {
+ let environment = latex::Environment::cast(parent)?;
+ let environment_name = environment.begin()?.name()?.word()?.text();
+ let kind = LabelledFloatKind::from_str(&environment_name).ok()?;
+ let caption = find_caption_by_parent(&parent)?;
+ Some(RenderedLabel {
+ range: environment.small_range(),
+ number: number.take(),
+ object: LabelledObject::Float { caption, kind },
+ })
+}
+
+fn render_label_section(
+ parent: &latex::SyntaxNode,
+ number: &mut Option<String>,
+) -> Option<RenderedLabel> {
+ let section = latex::Section::cast(parent)?;
+ let text_group = section.name()?;
+ let text = text_group.content_text()?;
+
+ Some(RenderedLabel {
+ range: section.small_range(),
+ number: number.take(),
+ object: LabelledObject::Section {
+ prefix: match section.syntax().kind() {
+ latex::PART => "Part",
+ latex::CHAPTER => "Chapter",
+ latex::SECTION => "Section",
+ latex::SUBSECTION => "Subsection",
+ latex::SUBSUBSECTION => "Subsubsection",
+ latex::PARAGRAPH => "Paragraph",
+ latex::SUBPARAGRAPH => "Subparagraph",
+ _ => unreachable!(),
+ },
+ text,
+ },
+ })
+}
+
+fn render_label_enum_item(
+ parent: &latex::SyntaxNode,
+ number: &mut Option<String>,
+) -> Option<RenderedLabel> {
+ let enum_item = latex::EnumItem::cast(parent)?;
+ Some(RenderedLabel {
+ range: enum_item.small_range(),
+ number: enum_item
+ .label()
+ .and_then(|number| number.content_text())
+ .or_else(|| number.take()),
+ object: LabelledObject::EnumItem,
+ })
+}
+
+fn render_label_equation(
+ parent: &latex::SyntaxNode,
+ number: &mut Option<String>,
+) -> Option<RenderedLabel> {
+ let environment = latex::Environment::cast(parent)?;
+ let environment_name = environment.begin()?.name()?.word()?.text();
+
+ if !LANGUAGE_DATA
+ .math_environments
+ .iter()
+ .any(|name| name == environment_name)
+ {
+ return None;
+ }
+
+ Some(RenderedLabel {
+ range: environment.small_range(),
+ number: number.take(),
+ object: LabelledObject::Equation,
+ })
+}
+
+fn render_label_theorem(
+ subset: &WorkspaceSubset,
+ parent: &latex::SyntaxNode,
+ number: &mut Option<String>,
+) -> Option<RenderedLabel> {
+ let environment = latex::Environment::cast(parent)?;
+ let begin = environment.begin()?;
+ let description = begin.options().and_then(|options| options.content_text());
+
+ let environment_name = begin.name()?.word()?.text();
+
+ let theorem = subset.documents.iter().find_map(|document| {
+ document.data.as_latex().and_then(|data| {
+ data.extras
+ .theorem_environments
+ .iter()
+ .find(|theorem| theorem.name.as_str() == environment_name)
+ })
+ })?;
+
+ Some(RenderedLabel {
+ range: environment.small_range(),
+ number: number.take(),
+ object: LabelledObject::Theorem {
+ kind: theorem.description.clone(),
+ description,
+ },
+ })
+}
+
+pub fn find_caption_by_parent(parent: &latex::SyntaxNode) -> Option<String> {
+ parent
+ .children()
+ .filter_map(latex::Caption::cast)
+ .find_map(|node| node.long())
+ .and_then(|node| node.content_text())
+}
diff --git a/support/texlab/src/lang_data.rs b/support/texlab/src/lang_data.rs
new file mode 100644
index 0000000000..9aa939c483
--- /dev/null
+++ b/support/texlab/src/lang_data.rs
@@ -0,0 +1,67 @@
+use once_cell::sync::Lazy;
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub enum BibtexEntryTypeCategory {
+ Misc,
+ String,
+ Article,
+ Book,
+ Collection,
+ Part,
+ Thesis,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct BibtexEntryTypeDoc {
+ pub name: String,
+ pub category: BibtexEntryTypeCategory,
+ pub documentation: Option<String>,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct BibtexFieldDoc {
+ pub name: String,
+ pub documentation: String,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct LanguageData {
+ pub colors: Vec<String>,
+ pub entry_types: Vec<BibtexEntryTypeDoc>,
+ pub fields: Vec<BibtexFieldDoc>,
+ pub pgf_libraries: Vec<String>,
+ pub tikz_libraries: Vec<String>,
+ pub math_environments: Vec<String>,
+ pub enum_environments: Vec<String>,
+}
+
+impl LanguageData {
+ pub fn find_entry_type(&self, name: &str) -> Option<&BibtexEntryTypeDoc> {
+ let name = name.to_lowercase();
+ self.entry_types
+ .iter()
+ .find(|ty| ty.name.to_lowercase() == name)
+ }
+
+ pub fn entry_type_documentation(&self, name: &str) -> Option<&str> {
+ self.find_entry_type(name)
+ .and_then(|ty| ty.documentation.as_ref().map(AsRef::as_ref))
+ }
+
+ pub fn field_documentation(&self, name: &str) -> Option<&str> {
+ self.fields
+ .iter()
+ .find(|field| field.name.to_lowercase() == name.to_lowercase())
+ .map(|field| field.documentation.as_ref())
+ }
+}
+
+pub static LANGUAGE_DATA: Lazy<LanguageData> = Lazy::new(|| {
+ const JSON: &str = include_str!("../data/lang_data.json");
+ serde_json::from_str(JSON).expect("Failed to deserialize language.json")
+});
diff --git a/support/texlab/src/language.rs b/support/texlab/src/language.rs
new file mode 100644
index 0000000000..45ef22daba
--- /dev/null
+++ b/support/texlab/src/language.rs
@@ -0,0 +1,33 @@
+use std::{ffi::OsStr, path::Path};
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
+pub enum DocumentLanguage {
+ Latex,
+ Bibtex,
+ BuildLog,
+}
+
+impl DocumentLanguage {
+ pub fn by_path(path: &Path) -> Option<Self> {
+ path.extension()
+ .and_then(OsStr::to_str)
+ .and_then(Self::by_extension)
+ }
+
+ pub fn by_extension(extension: &str) -> Option<Self> {
+ match extension.to_lowercase().as_str() {
+ "tex" | "sty" | "cls" | "def" | "lco" | "aux" | "rnw" => Some(Self::Latex),
+ "bib" | "bibtex" => Some(Self::Bibtex),
+ "log" => Some(Self::BuildLog),
+ _ => None,
+ }
+ }
+
+ pub fn by_language_id(language_id: &str) -> Option<Self> {
+ match language_id {
+ "latex" | "tex" => Some(Self::Latex),
+ "bibtex" | "bib" => Some(Self::Bibtex),
+ _ => None,
+ }
+ }
+}
diff --git a/support/texlab/src/lib.rs b/support/texlab/src/lib.rs
index 8644a40f10..0441c1a1d7 100644
--- a/support/texlab/src/lib.rs
+++ b/support/texlab/src/lib.rs
@@ -1,33 +1,38 @@
+mod capabilities;
#[cfg(feature = "citation")]
-pub mod citeproc;
-
-#[cfg(feature = "test")]
-pub mod test;
-
-cfg_if::cfg_if! {
- if #[cfg(feature = "server")] {
- mod config;
- mod build;
-
- pub mod server;
- }
-}
-
-pub mod completion;
-pub mod components;
-pub mod definition;
+pub mod citation;
+mod client;
+pub mod component_db;
+mod config;
+mod context;
pub mod diagnostics;
-pub mod feature;
-pub mod folding;
-pub mod forward_search;
-pub mod highlight;
-pub mod hover;
-pub mod link;
-pub mod outline;
-pub mod protocol;
-pub mod reference;
-pub mod rename;
-pub mod symbol;
+mod dispatch;
+pub mod distro;
+pub mod features;
+mod label;
+mod lang_data;
+mod language;
+mod line_index;
+mod line_index_ext;
+mod options;
+mod range;
+mod req_queue;
+mod server;
pub mod syntax;
-pub mod tex;
-pub mod workspace;
+mod uri;
+mod workspace;
+
+pub use self::{
+ capabilities::ClientCapabilitiesExt,
+ context::ServerContext,
+ label::*,
+ lang_data::*,
+ language::DocumentLanguage,
+ line_index::{LineCol, LineColUtf16, LineIndex},
+ line_index_ext::LineIndexExt,
+ options::*,
+ range::RangeExt,
+ server::Server,
+ uri::Uri,
+ workspace::*,
+};
diff --git a/support/texlab/src/line_index.rs b/support/texlab/src/line_index.rs
new file mode 100644
index 0000000000..868fe41232
--- /dev/null
+++ b/support/texlab/src/line_index.rs
@@ -0,0 +1,217 @@
+// The following code has been copied from rust-analyzer.
+
+//! `LineIndex` maps flat `TextSize` offsets into `(Line, Column)`
+//! representation.
+use std::iter;
+
+use cstree::{TextRange, TextSize};
+use rustc_hash::FxHashMap;
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct LineIndex {
+ /// Offset the the beginning of each line, zero-based
+ pub(crate) newlines: Vec<TextSize>,
+ /// List of non-ASCII characters on each line
+ pub(crate) utf16_lines: FxHashMap<u32, Vec<Utf16Char>>,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub struct LineColUtf16 {
+ /// Zero-based
+ pub line: u32,
+ /// Zero-based
+ pub col: u32,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub struct LineCol {
+ /// Zero-based
+ pub line: u32,
+ /// Zero-based utf8 offset
+ pub col: u32,
+}
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub(crate) struct Utf16Char {
+ /// Start offset of a character inside a line, zero-based
+ pub(crate) start: TextSize,
+ /// End offset of a character inside a line, zero-based
+ pub(crate) end: TextSize,
+}
+
+impl Utf16Char {
+ /// Returns the length in 8-bit UTF-8 code units.
+ fn len(&self) -> TextSize {
+ self.end - self.start
+ }
+
+ /// Returns the length in 16-bit UTF-16 code units.
+ fn len_utf16(&self) -> usize {
+ if self.len() == TextSize::from(4) {
+ 2
+ } else {
+ 1
+ }
+ }
+}
+
+impl LineIndex {
+ pub fn new(text: &str) -> LineIndex {
+ let mut utf16_lines = FxHashMap::default();
+ let mut utf16_chars = Vec::new();
+
+ let mut newlines = vec![0.into()];
+ let mut curr_row = 0.into();
+ let mut curr_col = 0.into();
+ let mut line = 0;
+ for c in text.chars() {
+ let c_len = TextSize::of(c);
+ curr_row += c_len;
+ if c == '\n' {
+ newlines.push(curr_row);
+
+ // Save any utf-16 characters seen in the previous line
+ if !utf16_chars.is_empty() {
+ utf16_lines.insert(line, utf16_chars);
+ utf16_chars = Vec::new();
+ }
+
+ // Prepare for processing the next line
+ curr_col = 0.into();
+ line += 1;
+ continue;
+ }
+
+ if !c.is_ascii() {
+ utf16_chars.push(Utf16Char {
+ start: curr_col,
+ end: curr_col + c_len,
+ });
+ }
+
+ curr_col += c_len;
+ }
+
+ // Save any utf-16 characters seen in the last line
+ if !utf16_chars.is_empty() {
+ utf16_lines.insert(line, utf16_chars);
+ }
+
+ LineIndex {
+ newlines,
+ utf16_lines,
+ }
+ }
+
+ pub fn line_col(&self, offset: TextSize) -> LineCol {
+ let line = partition_point(&self.newlines, |&it| it <= offset) - 1;
+ let line_start_offset = self.newlines[line];
+ let col = offset - line_start_offset;
+ LineCol {
+ line: line as u32,
+ col: col.into(),
+ }
+ }
+
+ pub fn offset(&self, line_col: LineCol) -> TextSize {
+ self.newlines[line_col.line as usize] + TextSize::from(line_col.col)
+ }
+
+ pub fn to_utf16(&self, line_col: LineCol) -> LineColUtf16 {
+ let col = self.utf8_to_utf16_col(line_col.line, line_col.col.into());
+ LineColUtf16 {
+ line: line_col.line,
+ col: col as u32,
+ }
+ }
+
+ pub fn to_utf8(&self, line_col: LineColUtf16) -> LineCol {
+ let col = self.utf16_to_utf8_col(line_col.line, line_col.col);
+ LineCol {
+ line: line_col.line,
+ col: col.into(),
+ }
+ }
+
+ pub fn lines(&self, range: TextRange) -> impl Iterator<Item = TextRange> + '_ {
+ let lo = partition_point(&self.newlines, |&it| it < range.start());
+ let hi = partition_point(&self.newlines, |&it| it <= range.end());
+ let all = iter::once(range.start())
+ .chain(self.newlines[lo..hi].iter().copied())
+ .chain(iter::once(range.end()));
+
+ all.clone()
+ .zip(all.skip(1))
+ .map(|(lo, hi)| TextRange::new(lo, hi))
+ .filter(|it| !it.is_empty())
+ }
+
+ fn utf8_to_utf16_col(&self, line: u32, col: TextSize) -> usize {
+ let mut res: usize = col.into();
+ if let Some(utf16_chars) = self.utf16_lines.get(&line) {
+ for c in utf16_chars {
+ if c.end <= col {
+ res -= usize::from(c.len()) - c.len_utf16();
+ } else {
+ // From here on, all utf16 characters come *after* the character we are mapping,
+ // so we don't need to take them into account
+ break;
+ }
+ }
+ }
+ res
+ }
+
+ fn utf16_to_utf8_col(&self, line: u32, mut col: u32) -> TextSize {
+ if let Some(utf16_chars) = self.utf16_lines.get(&line) {
+ for c in utf16_chars {
+ if col > u32::from(c.start) {
+ col += u32::from(c.len()) - c.len_utf16() as u32;
+ } else {
+ // From here on, all utf16 characters come *after* the character we are mapping,
+ // so we don't need to take them into account
+ break;
+ }
+ }
+ }
+
+ col.into()
+ }
+}
+
+/// Returns `idx` such that:
+///
+/// ```text
+/// ∀ x in slice[..idx]: pred(x)
+/// && ∀ x in slice[idx..]: !pred(x)
+/// ```
+///
+/// https://github.com/rust-lang/rust/issues/73831
+fn partition_point<T, P>(slice: &[T], mut pred: P) -> usize
+where
+ P: FnMut(&T) -> bool,
+{
+ let mut left = 0;
+ let mut right = slice.len();
+
+ while left != right {
+ let mid = left + (right - left) / 2;
+ // SAFETY:
+ // When left < right, left <= mid < right.
+ // Therefore left always increases and right always decreases,
+ // and either of them is selected.
+ // In both cases left <= right is satisfied.
+ // Therefore if left < right in a step,
+ // left <= right is satisfied in the next step.
+ // Therefore as long as left != right, 0 <= left < right <= len is satisfied
+ // and if this case 0 <= mid < len is satisfied too.
+ let value = unsafe { slice.get_unchecked(mid) };
+ if pred(value) {
+ left = mid + 1;
+ } else {
+ right = mid;
+ }
+ }
+
+ left
+} \ No newline at end of file
diff --git a/support/texlab/src/line_index_ext.rs b/support/texlab/src/line_index_ext.rs
new file mode 100644
index 0000000000..8e28207edd
--- /dev/null
+++ b/support/texlab/src/line_index_ext.rs
@@ -0,0 +1,42 @@
+use cstree::{TextRange, TextSize};
+use lsp_types::{Position, Range};
+
+use crate::{LineColUtf16, LineIndex};
+
+pub trait LineIndexExt {
+ fn offset_lsp(&self, line_col: Position) -> TextSize;
+
+ fn offset_lsp_range(&self, line_col: Range) -> TextRange;
+
+ fn line_col_lsp(&self, offset: TextSize) -> Position;
+
+ fn line_col_lsp_range(&self, offset: TextRange) -> Range;
+}
+
+impl LineIndexExt for LineIndex {
+ fn offset_lsp(&self, line_col: Position) -> TextSize {
+ let line_col = LineColUtf16 {
+ line: line_col.line,
+ col: line_col.character,
+ };
+ self.offset(self.to_utf8(line_col))
+ }
+
+ fn offset_lsp_range(&self, line_col: Range) -> TextRange {
+ let start = self.offset_lsp(line_col.start);
+ let end = self.offset_lsp(line_col.end);
+ TextRange::new(start, end)
+ }
+
+ fn line_col_lsp(&self, offset: TextSize) -> Position {
+ let position = self.line_col(offset);
+ let LineColUtf16 { line , col } = self.to_utf16(position);
+ Position::new(line, col)
+ }
+
+ fn line_col_lsp_range(&self, offset: TextRange) -> Range {
+ let start = self.line_col_lsp(offset.start());
+ let end = self.line_col_lsp(offset.end());
+ Range::new(start, end)
+ }
+}
diff --git a/support/texlab/src/link/latex_import.rs b/support/texlab/src/link/latex_import.rs
deleted file mode 100644
index 494a72b481..0000000000
--- a/support/texlab/src/link/latex_import.rs
+++ /dev/null
@@ -1,99 +0,0 @@
-use crate::{
- feature::{FeatureProvider, FeatureRequest},
- protocol::{DocumentLink, DocumentLinkParams},
- syntax::{latex, SyntaxNode},
- workspace::DocumentContent,
-};
-use async_trait::async_trait;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct LatexImportLinkProvider;
-
-#[async_trait]
-impl FeatureProvider for LatexImportLinkProvider {
- type Params = DocumentLinkParams;
- type Output = Vec<DocumentLink>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- if let DocumentContent::Latex(table) = &req.current().content {
- table
- .imports
- .iter()
- .flat_map(|import| Self::resolve(req, table, import))
- .collect()
- } else {
- Vec::new()
- }
- }
-}
-
-impl LatexImportLinkProvider {
- fn resolve(
- req: &FeatureRequest<DocumentLinkParams>,
- table: &latex::SymbolTable,
- import: &latex::Import,
- ) -> Vec<DocumentLink> {
- let mut links = Vec::new();
- let file = import.file(&table);
- for target in &import.targets {
- if let Some(link) = req.snapshot().find(target).map(|doc| DocumentLink {
- range: file.range(),
- target: doc.uri.clone().into(),
- tooltip: None,
- }) {
- links.push(link);
- break;
- }
- }
- links
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- feature::FeatureTester,
- protocol::{Range, RangeExt},
- };
-
- #[tokio::test]
- async fn empty_latex_document_command() {
- let actual_links = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .test_link(LatexImportLinkProvider)
- .await;
-
- assert!(actual_links.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document_command() {
- let actual_links = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .test_link(LatexImportLinkProvider)
- .await;
-
- assert!(actual_links.is_empty());
- }
-
- #[tokio::test]
- async fn has_links() {
- let actual_links = FeatureTester::new()
- .file("foo.tex", r#"\import{bar/}{baz}"#)
- .file("bar/baz.tex", r#""#)
- .main("foo.tex")
- .test_link(LatexImportLinkProvider)
- .await;
-
- let expected_links = vec![DocumentLink {
- range: Range::new_simple(0, 14, 0, 17),
- target: FeatureTester::uri("bar/baz.tex").into(),
- tooltip: None,
- }];
-
- assert_eq!(actual_links, expected_links);
- }
-}
diff --git a/support/texlab/src/link/latex_include.rs b/support/texlab/src/link/latex_include.rs
deleted file mode 100644
index 5f4f592f14..0000000000
--- a/support/texlab/src/link/latex_include.rs
+++ /dev/null
@@ -1,101 +0,0 @@
-use crate::{
- feature::{FeatureProvider, FeatureRequest},
- protocol::{DocumentLink, DocumentLinkParams},
- syntax::{latex, SyntaxNode},
- workspace::DocumentContent,
-};
-use async_trait::async_trait;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct LatexIncludeLinkProvider;
-
-#[async_trait]
-impl FeatureProvider for LatexIncludeLinkProvider {
- type Params = DocumentLinkParams;
- type Output = Vec<DocumentLink>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- if let DocumentContent::Latex(table) = &req.current().content {
- table
- .includes
- .iter()
- .flat_map(|include| Self::resolve(req, table, include))
- .collect()
- } else {
- Vec::new()
- }
- }
-}
-
-impl LatexIncludeLinkProvider {
- fn resolve(
- req: &FeatureRequest<DocumentLinkParams>,
- table: &latex::SymbolTable,
- include: &latex::Include,
- ) -> Vec<DocumentLink> {
- let mut links = Vec::new();
- let paths = include.paths(&table);
- for (i, targets) in include.all_targets.iter().enumerate() {
- for target in targets {
- if let Some(link) = req.snapshot().find(target).map(|doc| DocumentLink {
- range: paths[i].range(),
- target: doc.uri.clone().into(),
- tooltip: None,
- }) {
- links.push(link);
- break;
- }
- }
- }
- links
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- feature::FeatureTester,
- protocol::{Range, RangeExt},
- };
-
- #[tokio::test]
- async fn empty_latex_document_command() {
- let actual_links = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .test_link(LatexIncludeLinkProvider)
- .await;
-
- assert!(actual_links.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document_command() {
- let actual_links = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .test_link(LatexIncludeLinkProvider)
- .await;
-
- assert!(actual_links.is_empty());
- }
-
- #[tokio::test]
- async fn has_links() {
- let actual_links = FeatureTester::new()
- .file("foo.tex", r#"\input{bar.tex}"#)
- .file("bar.tex", r#""#)
- .main("foo.tex")
- .test_link(LatexIncludeLinkProvider)
- .await;
-
- let expected_links = vec![DocumentLink {
- range: Range::new_simple(0, 7, 0, 14),
- target: FeatureTester::uri("bar.tex").into(),
- tooltip: None,
- }];
-
- assert_eq!(actual_links, expected_links);
- }
-}
diff --git a/support/texlab/src/link/mod.rs b/support/texlab/src/link/mod.rs
deleted file mode 100644
index 934975f060..0000000000
--- a/support/texlab/src/link/mod.rs
+++ /dev/null
@@ -1,40 +0,0 @@
-mod latex_import;
-mod latex_include;
-
-use self::{latex_import::LatexImportLinkProvider, latex_include::LatexIncludeLinkProvider};
-use crate::{
- feature::{ConcatProvider, FeatureProvider, FeatureRequest},
- protocol::{DocumentLink, DocumentLinkParams},
-};
-use async_trait::async_trait;
-
-pub struct LinkProvider {
- provider: ConcatProvider<DocumentLinkParams, DocumentLink>,
-}
-
-impl LinkProvider {
- pub fn new() -> Self {
- Self {
- provider: ConcatProvider::new(vec![
- Box::new(LatexImportLinkProvider),
- Box::new(LatexIncludeLinkProvider),
- ]),
- }
- }
-}
-
-impl Default for LinkProvider {
- fn default() -> Self {
- Self::new()
- }
-}
-
-#[async_trait]
-impl FeatureProvider for LinkProvider {
- type Params = DocumentLinkParams;
- type Output = Vec<DocumentLink>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- self.provider.execute(req).await
- }
-}
diff --git a/support/texlab/src/main.rs b/support/texlab/src/main.rs
index 16c5be8f26..415de3b804 100644
--- a/support/texlab/src/main.rs
+++ b/support/texlab/src/main.rs
@@ -1,15 +1,10 @@
-use futures::{channel::mpsc, prelude::*};
-use jsonrpc::MessageHandler;
+use std::{env, fs::OpenOptions, io, path::PathBuf};
+
+use anyhow::Result;
use log::LevelFilter;
-use std::path::PathBuf;
-use std::{env, error, fs::OpenOptions, sync::Arc};
+use lsp_server::Connection;
use structopt::StructOpt;
-use texlab::{
- protocol::{LatexLspClient, LspCodec},
- server::LatexLspServer,
- tex::Distribution,
-};
-use tokio_util::codec::{FramedRead, FramedWrite};
+use texlab::Server;
/// An implementation of the Language Server Protocol for LaTeX
#[derive(Debug, StructOpt)]
@@ -27,37 +22,13 @@ struct Opts {
log_file: Option<PathBuf>,
}
-#[tokio::main]
-async fn main() -> Result<(), Box<dyn error::Error>> {
+fn main() -> Result<()> {
let opts = Opts::from_args();
setup_logger(opts);
- let mut stdin = FramedRead::new(tokio::io::stdin(), LspCodec);
- let (stdout_tx, mut stdout_rx) = mpsc::channel(0);
-
- let client = Arc::new(LatexLspClient::new(stdout_tx.clone()));
- let server = Arc::new(LatexLspServer::new(
- Distribution::detect().await,
- Arc::clone(&client),
- Arc::new(env::current_dir().expect("failed to get working directory")),
- ));
- let mut handler = MessageHandler {
- server,
- client,
- output: stdout_tx,
- };
-
- tokio::spawn(async move {
- let mut stdout = FramedWrite::new(tokio::io::stdout(), LspCodec);
- loop {
- let message = stdout_rx.next().await.unwrap();
- stdout.send(message).await.unwrap();
- }
- });
-
- while let Some(json) = stdin.next().await {
- handler.handle(&json.unwrap()).await;
- }
+ let (connection, threads) = Connection::stdio();
+ Server::with_connection(connection, env::current_dir()?, true)?.run()?;
+ threads.join()?;
Ok(())
}
@@ -78,8 +49,10 @@ fn setup_logger(opts: Opts) {
let logger = fern::Dispatch::new()
.format(|out, message, record| out.finish(format_args!("{} - {}", record.level(), message)))
.level(verbosity_level)
- .filter(|metadata| metadata.target() == "jsonrpc" || metadata.target().contains("texlab"))
- .chain(std::io::stderr());
+ .filter(|metadata| {
+ metadata.target().contains("texlab") || metadata.target().contains("lsp_server")
+ })
+ .chain(io::stderr());
let logger = match opts.log_file {
Some(log_file) => logger.chain(
diff --git a/support/texlab/src/options.rs b/support/texlab/src/options.rs
new file mode 100644
index 0000000000..09eea1110f
--- /dev/null
+++ b/support/texlab/src/options.rs
@@ -0,0 +1,86 @@
+use std::path::PathBuf;
+
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Options {
+ pub root_directory: Option<PathBuf>,
+
+ pub aux_directory: Option<PathBuf>,
+
+ #[serde(default)]
+ pub bibtex_formatter: BibtexFormatter,
+
+ pub formatter_line_length: Option<i32>,
+
+ pub diagnostics_delay: Option<u64>,
+
+ #[serde(default)]
+ pub build: BuildOptions,
+
+ #[serde(default)]
+ pub chktex: ChktexOptions,
+
+ pub forward_search: Option<ForwardSearchOptions>,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum BibtexFormatter {
+ Texlab,
+ Latexindent,
+}
+
+impl Default for BibtexFormatter {
+ fn default() -> Self {
+ Self::Texlab
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct BuildOptions {
+ pub executable: Option<String>,
+
+ pub args: Option<Vec<String>>,
+
+ #[serde(default)]
+ pub is_continuous: bool,
+}
+
+impl BuildOptions {
+ pub fn executable(&self) -> String {
+ self.executable
+ .as_ref()
+ .map(Clone::clone)
+ .unwrap_or_else(|| "latexmk".to_string())
+ }
+
+ pub fn args(&self) -> Vec<String> {
+ self.args.as_ref().map(Clone::clone).unwrap_or_else(|| {
+ vec![
+ "-pdf".to_string(),
+ "-interaction=nonstopmode".to_string(),
+ "-synctex=1".to_string(),
+ "%f".to_string(),
+ ]
+ })
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ChktexOptions {
+ #[serde(default)]
+ pub on_open_and_save: bool,
+
+ #[serde(default)]
+ pub on_edit: bool,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)]
+pub struct ForwardSearchOptions {
+ pub executable: Option<String>,
+ pub args: Option<Vec<String>>,
+}
diff --git a/support/texlab/src/outline.rs b/support/texlab/src/outline.rs
deleted file mode 100644
index 07ffaee09f..0000000000
--- a/support/texlab/src/outline.rs
+++ /dev/null
@@ -1,437 +0,0 @@
-use crate::{
- feature::DocumentView,
- protocol::{MarkupContent, MarkupKind, Options, Position, Range, RangeExt, Uri},
- syntax::{latex, SyntaxNode},
- workspace::{Document, DocumentContent},
-};
-use std::{borrow::Cow, collections::HashSet, path::Path};
-use titlecase::titlecase;
-use OutlineContextItem::*;
-
-#[derive(Debug, Clone, Copy)]
-pub struct OutlineSection<'a> {
- pub document: &'a Document,
- pub item: &'a latex::Section,
-}
-
-pub struct Outline<'a> {
- sections: Vec<OutlineSection<'a>>,
-}
-
-impl<'a> Outline<'a> {
- pub fn find(&'a self, uri: &Uri, pos: Position) -> Option<&'a latex::Section> {
- self.sections
- .iter()
- .filter(|section| section.document.uri == *uri)
- .rev()
- .find(|section| {
- let table = section.document.content.as_latex().unwrap();
- table[section.item.parent].end() <= pos
- })
- .map(|sec| sec.item)
- }
-
- pub fn analyze(view: &'a DocumentView, options: &Options, current_dir: &Path) -> Self {
- let mut finder = OutlineSectionFinder::default();
- let doc = view
- .snapshot
- .parent(&view.current.uri, options, current_dir)
- .map(|parent| {
- view.related
- .iter()
- .find(|doc| doc.uri == parent.uri)
- .unwrap()
- })
- .unwrap_or(&view.current);
-
- finder.analyze(view, doc);
- Self {
- sections: finder.sections,
- }
- }
-}
-
-#[derive(Debug, Clone, Copy)]
-enum OutlineItem<'a> {
- Section(&'a latex::Section),
- Include(&'a latex::Include),
-}
-
-#[derive(Debug, Default)]
-struct OutlineSectionFinder<'a> {
- visited: HashSet<&'a Uri>,
- sections: Vec<OutlineSection<'a>>,
-}
-
-impl<'a> OutlineSectionFinder<'a> {
- fn analyze(&mut self, view: &'a DocumentView, doc: &'a Document) {
- if !self.visited.insert(&doc.uri) {
- return;
- }
-
- if let DocumentContent::Latex(table) = &doc.content {
- let mut items = Vec::new();
- for section in &table.sections {
- items.push(OutlineItem::Section(section));
- }
- for include in &table.includes {
- items.push(OutlineItem::Include(include));
- }
- items.sort_by_key(|item| match item {
- OutlineItem::Include(include) => table[include.parent].start(),
- OutlineItem::Section(section) => table[section.parent].start(),
- });
-
- for item in items {
- match item {
- OutlineItem::Section(item) => {
- let section = OutlineSection {
- document: doc,
- item,
- };
- self.sections.push(section);
- }
- OutlineItem::Include(item) => {
- for doc in &view.related {
- for targets in &item.all_targets {
- if targets.contains(&doc.uri) {
- self.analyze(view, doc);
- break;
- }
- }
- }
- }
- }
- }
- }
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum OutlineCaptionKind {
- Figure,
- Table,
- Listing,
- Algorithm,
-}
-
-impl OutlineCaptionKind {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::Figure => "Figure",
- Self::Table => "Table",
- Self::Listing => "Listing",
- Self::Algorithm => "Algorithm",
- }
- }
-
- pub fn parse(env_name: &str) -> Option<Self> {
- match env_name {
- "figure" | "subfigure" => Some(Self::Figure),
- "table" | "subtable" => Some(Self::Table),
- "listing" | "lstlisting" => Some(Self::Listing),
- "algorithm" => Some(Self::Algorithm),
- _ => None,
- }
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub enum OutlineContextItem {
- Section {
- prefix: Cow<'static, str>,
- text: String,
- },
- Caption {
- kind: Option<OutlineCaptionKind>,
- text: String,
- },
- Theorem {
- kind: String,
- description: Option<String>,
- },
- Equation,
- Item,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub struct OutlineContext {
- pub range: Range,
- pub number: Option<String>,
- pub item: OutlineContextItem,
-}
-
-impl OutlineContext {
- pub fn reference(&self) -> String {
- match &self.number {
- Some(number) => match &self.item {
- Section { prefix, text } => format!("{} {} ({})", prefix, number, text),
- Caption { kind: None, text } => format!("{} {}", number, text),
- Caption {
- kind: Some(kind),
- text,
- } => format!("{} {}: {}", kind.as_str(), number, text),
- Theorem {
- kind,
- description: None,
- } => format!("{} {}", kind, number),
- Theorem {
- kind,
- description: Some(description),
- } => format!("{} {} ({})", kind, number, description),
- Equation => format!("Equation ({})", number),
- Item => format!("Item {}", number),
- },
- None => match &self.item {
- Section { prefix, text } => format!("{} ({})", prefix, text),
- Caption { kind: None, text } => text.clone(),
- Caption {
- kind: Some(kind),
- text,
- } => format!("{}: {}", kind.as_str(), text),
- Theorem {
- kind,
- description: None,
- } => kind.into(),
- Theorem {
- kind,
- description: Some(description),
- } => format!("{} ({})", kind, description),
- Equation => "Equation".into(),
- Item => "Item".into(),
- },
- }
- }
-
- pub fn detail(&self) -> Option<String> {
- match &self.item {
- Section { .. } | Theorem { .. } | Equation | Item => Some(self.reference()),
- Caption {
- kind: Some(kind), ..
- } => {
- let result = match &self.number {
- Some(number) => format!("{} {}", kind.as_str(), number),
- None => kind.as_str().to_owned(),
- };
- Some(result)
- }
- Caption { .. } => None,
- }
- }
-
- pub fn documentation(&self) -> MarkupContent {
- MarkupContent {
- kind: MarkupKind::PlainText,
- value: self.reference(),
- }
- }
-
- pub fn parse(view: &DocumentView, outline: &Outline, label: latex::Label) -> Option<Self> {
- if let DocumentContent::Latex(table) = &view.current.content {
- Self::find_caption(view, table, label)
- .or_else(|| Self::find_theorem(view, table, label))
- .or_else(|| Self::find_equation(view, table, label))
- .or_else(|| Self::find_item(view, table, label))
- .or_else(|| Self::find_section(view, outline, table, label))
- } else {
- None
- }
- }
-
- fn find_caption(
- view: &DocumentView,
- table: &latex::SymbolTable,
- label: latex::Label,
- ) -> Option<Self> {
- let label_range = table[label.parent].range();
- let caption_env = table
- .environments
- .iter()
- .filter(|env| !env.is_root(&table))
- .find(|env| env.range(&table).contains(label_range.start))?;
-
- let caption = table
- .captions
- .iter()
- .find(|cap| table.is_direct_child(*caption_env, table[cap.parent].start()))?;
-
- let caption_text = table.print_group_content(
- caption.parent,
- latex::GroupKind::Group,
- caption.arg_index,
- )?;
-
- let caption_kind = caption_env
- .left
- .name(&table)
- .map(latex::Token::text)
- .and_then(OutlineCaptionKind::parse);
-
- Some(Self {
- range: caption_env.range(&table),
- number: Self::find_number(view, table, label),
- item: Caption {
- kind: caption_kind,
- text: caption_text,
- },
- })
- }
-
- fn find_theorem(
- view: &DocumentView,
- main_table: &latex::SymbolTable,
- label: latex::Label,
- ) -> Option<Self> {
- let label_range = main_table[label.parent].range();
- let env = main_table
- .environments
- .iter()
- .find(|env| env.range(&main_table).contains(label_range.start))?;
-
- let env_name = env.left.name(&main_table).map(latex::Token::text)?;
-
- for doc in &view.related {
- if let DocumentContent::Latex(table) = &doc.content {
- for def in &table.theorem_definitions {
- if env_name == def.name(&table).text() {
- let kind = table
- .print_group_content(
- def.parent,
- latex::GroupKind::Group,
- def.arg_index + 1,
- )
- .unwrap_or_else(|| titlecase(&env_name));
-
- let description = main_table.print_group_content(
- env.left.parent,
- latex::GroupKind::Options,
- 0,
- );
-
- return Some(Self {
- range: env.range(&main_table),
- number: Self::find_number(view, main_table, label),
- item: Theorem { kind, description },
- });
- }
- }
- }
- }
- None
- }
-
- fn find_equation(
- view: &DocumentView,
- table: &latex::SymbolTable,
- label: latex::Label,
- ) -> Option<Self> {
- let label_range = table[label.parent].range();
- table
- .environments
- .iter()
- .filter(|env| env.left.is_math(&table))
- .map(|env| env.range(&table))
- .find(|range| range.contains(label_range.start))
- .map(|range| Self {
- range,
- number: Self::find_number(view, table, label),
- item: Equation,
- })
- }
-
- fn find_item(
- view: &DocumentView,
- table: &latex::SymbolTable,
- label: latex::Label,
- ) -> Option<Self> {
- let label_range = table[label.parent].range();
- struct LatexItemNode {
- item: latex::Item,
- range: Range,
- }
-
- let enumeration = table
- .environments
- .iter()
- .filter(|env| env.left.is_enum(&table))
- .find(|env| env.range(&table).contains(label_range.start))?;
-
- let mut item_nodes: Vec<_> = table
- .items
- .iter()
- .filter(|item| table.is_enum_item(*enumeration, **item))
- .map(|item| LatexItemNode {
- item: *item,
- range: Range::default(),
- })
- .collect();
-
- for i in 0..item_nodes.len() {
- let start = table[item_nodes[i].item.parent].start();
- let end = item_nodes
- .get(i + 1)
- .map(|node| table[node.item.parent].start())
- .unwrap_or_else(|| table[enumeration.right.parent].start());
- item_nodes[i].range = Range::new(start, end);
- }
-
- let node = item_nodes
- .iter()
- .find(|node| node.range.contains(label_range.start))?;
-
- let number = node
- .item
- .name(&table)
- .or_else(|| Self::find_number(view, table, label));
-
- Some(Self {
- range: enumeration.range(&table),
- number,
- item: Item,
- })
- }
-
- fn find_section(
- view: &DocumentView,
- outline: &Outline,
- table: &latex::SymbolTable,
- label: latex::Label,
- ) -> Option<Self> {
- let label_range = table[label.parent].range();
- let section = outline.find(&view.current.uri, label_range.start)?;
- Some(Self {
- range: table[section.parent].range(),
- number: Self::find_number(view, table, label),
- item: Section {
- prefix: section.prefix.clone(),
- text: table.print_group_content(
- section.parent,
- latex::GroupKind::Group,
- section.arg_index,
- )?,
- },
- })
- }
-
- pub fn find_number(
- view: &DocumentView,
- table: &latex::SymbolTable,
- label: latex::Label,
- ) -> Option<String> {
- let label_names = label.names(&table);
- if label_names.len() != 1 {
- return None;
- }
-
- for doc in &view.related {
- if let DocumentContent::Latex(table) = &doc.content {
- for numbering in &table.label_numberings {
- if numbering.name(&table).text() == label_names[0].text() {
- return Some(numbering.number.clone());
- }
- }
- }
- }
- None
- }
-}
diff --git a/support/texlab/src/protocol/client.rs b/support/texlab/src/protocol/client.rs
deleted file mode 100644
index 471f1fa41d..0000000000
--- a/support/texlab/src/protocol/client.rs
+++ /dev/null
@@ -1,27 +0,0 @@
-use jsonrpc::client::Result;
-use jsonrpc_derive::{jsonrpc_client, jsonrpc_method};
-use lsp_types::*;
-
-#[jsonrpc_client(LatexLspClient)]
-pub trait LspClient {
- #[jsonrpc_method("workspace/configuration", kind = "request")]
- async fn configuration(&self, params: ConfigurationParams) -> Result<serde_json::Value>;
-
- #[jsonrpc_method("window/showMessage", kind = "notification")]
- async fn show_message(&self, params: ShowMessageParams);
-
- #[jsonrpc_method("client/registerCapability", kind = "request")]
- async fn register_capability(&self, params: RegistrationParams) -> Result<()>;
-
- #[jsonrpc_method("textDocument/publishDiagnostics", kind = "notification")]
- async fn publish_diagnostics(&self, params: PublishDiagnosticsParams);
-
- #[jsonrpc_method("$/progress", kind = "notification")]
- async fn progress(&self, params: ProgressParams);
-
- #[jsonrpc_method("window/workDoneProgress/create", kind = "request")]
- async fn work_done_progress_create(&self, params: WorkDoneProgressCreateParams) -> Result<()>;
-
- #[jsonrpc_method("window/logMessage", kind = "notification")]
- async fn log_message(&self, params: LogMessageParams);
-}
diff --git a/support/texlab/src/protocol/codec.rs b/support/texlab/src/protocol/codec.rs
deleted file mode 100644
index 980ae1307e..0000000000
--- a/support/texlab/src/protocol/codec.rs
+++ /dev/null
@@ -1,144 +0,0 @@
-use bytes::{BufMut, BytesMut};
-use log::trace;
-use std::io::{Error, ErrorKind};
-use tokio_util::codec::{Decoder, Encoder};
-
-pub struct LspCodec;
-
-impl Decoder for LspCodec {
- type Item = String;
- type Error = Error;
-
- fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
- match parser::parse(src) {
- Ok((remaining, content)) => {
- trace!("Received message:\n{}\n", content);
-
- let offset = src.len() - remaining.len();
- let _ = src.split_to(offset);
- Ok(Some(content))
- }
- Err(error) => {
- if error.is_incomplete() {
- Ok(None)
- } else {
- Err(ErrorKind::InvalidData.into())
- }
- }
- }
- }
-}
-
-impl Encoder<String> for LspCodec {
- type Error = Error;
-
- fn encode(&mut self, item: String, dst: &mut BytesMut) -> Result<(), Self::Error> {
- let message = format!("Content-Length: {}\r\n\r\n{}", item.len(), item);
- trace!("Sent message:\n{}\n", message);
-
- dst.reserve(message.len());
- dst.put(message.as_bytes());
- Ok(())
- }
-}
-
-mod parser {
- use nom::{
- bytes::streaming::{tag, take, take_while},
- character::{is_digit, streaming::line_ending},
- combinator::{map_res, opt},
- IResult,
- };
- use std::str;
-
- pub fn parse(input: &[u8]) -> IResult<&[u8], String> {
- let (input, _) = opt(content_type)(input)?;
- let (input, length) = content_length(input)?;
- let (input, _) = opt(content_type)(input)?;
- let (input, _) = line_ending(input)?;
- let (input, content) = map_res(take(length), str::from_utf8)(input)?;
- Ok((input, content.to_owned()))
- }
-
- fn content_type(input: &[u8]) -> IResult<&[u8], &[u8]> {
- let (input, _) = tag("Content-Type: application/vscode-jsonrpc;charset=utf")(input)?;
- let (input, _) = opt(tag("-"))(input)?;
- let (input, _) = tag("8")(input)?;
- line_ending(input)
- }
-
- fn content_length(input: &[u8]) -> IResult<&[u8], usize> {
- let (input, _) = tag("Content-Length: ")(input)?;
- let (input, length) = map_res(take_while(is_digit), from_bytes)(input)?;
- let (input, _) = line_ending(input)?;
- Ok((input, length))
- }
-
- fn from_bytes(input: &[u8]) -> Result<usize, std::num::ParseIntError> {
- usize::from_str_radix(str::from_utf8(input).unwrap(), 10)
- }
-
- #[cfg(test)]
- mod tests {
- use super::*;
-
- #[test]
- fn parse_content_type() {
- let result =
- content_type(b"Content-Type: application/vscode-jsonrpc;charset=utf-8\r\n");
- assert!(result.is_ok());
- }
-
- #[test]
- fn parse_content_type_utf8() {
- let result = content_type(b"Content-Type: application/vscode-jsonrpc;charset=utf8\r\n");
- assert!(result.is_ok());
- }
-
- #[test]
- fn parse_content_length() {
- let result = content_length(b"Content-Length: 42\r\n");
- assert_eq!(result.unwrap().1, 42usize);
- }
-
- #[test]
- fn parse_message_full() {
- let result = parse(
- b"Content-Length: 2\r\nContent-Type: application/vscode-jsonrpc;charset=utf8\r\n\r\n{}",
- );
- assert_eq!(result.unwrap().1, "{}");
- }
-
- #[test]
- fn parse_message_type_first() {
- let result = parse(
- b"Content-Type: application/vscode-jsonrpc;charset=utf8\r\nContent-Length: 2\r\n\r\n{}",
- );
- assert_eq!(result.unwrap().1, "{}");
- }
-
- #[test]
- fn parse_message_without_type() {
- let result = parse(b"Content-Length: 2\r\n\r\n{}");
- assert_eq!(result.unwrap().1, "{}");
- }
-
- #[test]
- fn parse_message_incomplete() {
- let result = parse(b"Content-Length:");
- assert!(result.unwrap_err().is_incomplete());
- }
-
- #[test]
- fn parse_message_invalid() {
- let error = parse(b"foo").unwrap_err();
- assert!(!error.is_incomplete());
- }
-
- #[test]
- fn parse_message_overflow() {
- let result = parse(b"Content-Length: 4\r\n\r\n{}");
- assert!(result.unwrap_err().is_incomplete());
- }
- }
-}
diff --git a/support/texlab/src/protocol/edit.rs b/support/texlab/src/protocol/edit.rs
deleted file mode 100644
index 9091f2d393..0000000000
--- a/support/texlab/src/protocol/edit.rs
+++ /dev/null
@@ -1,12 +0,0 @@
-use lsp_types::{CompletionTextEdit, TextEdit};
-
-pub trait CompletionTextEditExt {
- fn text_edit(&self) -> Option<&TextEdit>;
-}
-
-impl CompletionTextEditExt for CompletionTextEdit {
- fn text_edit(&self) -> Option<&TextEdit> {
- let CompletionTextEdit::Edit(edit) = self;
- Some(edit)
- }
-}
diff --git a/support/texlab/src/protocol/mod.rs b/support/texlab/src/protocol/mod.rs
deleted file mode 100644
index c08a7d82f5..0000000000
--- a/support/texlab/src/protocol/mod.rs
+++ /dev/null
@@ -1,71 +0,0 @@
-cfg_if::cfg_if! {
- if #[cfg(feature = "server")] {
- mod client;
- mod codec;
-
- pub use self::{
- client::{LatexLspClient, LspClient},
- codec::LspCodec,
- };
- }
-}
-
-mod capabilities;
-mod edit;
-mod options;
-mod range;
-mod uri;
-
-pub use self::{
- capabilities::ClientCapabilitiesExt,
- edit::*,
- options::*,
- range::RangeExt,
- uri::{AsUri, Uri},
-};
-pub use lsp_types::*;
-
-use serde::{Deserialize, Serialize};
-use serde_repr::*;
-
-#[serde(untagged)]
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub enum DefinitionResponse {
- Locations(Vec<Location>),
- LocationLinks(Vec<LocationLink>),
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize_repr, Deserialize_repr)]
-#[repr(i32)]
-pub enum ForwardSearchStatus {
- Success = 0,
- Error = 1,
- Failure = 2,
- Unconfigured = 3,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct ForwardSearchResult {
- pub status: ForwardSearchStatus,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct BuildParams {
- pub text_document: TextDocumentIdentifier,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize_repr, Deserialize_repr)]
-#[repr(i32)]
-pub enum BuildStatus {
- Success = 0,
- Error = 1,
- Failure = 2,
- Cancelled = 3,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct BuildResult {
- pub status: BuildStatus,
-}
diff --git a/support/texlab/src/protocol/options.rs b/support/texlab/src/protocol/options.rs
deleted file mode 100644
index 86718d1e72..0000000000
--- a/support/texlab/src/protocol/options.rs
+++ /dev/null
@@ -1,104 +0,0 @@
-use serde::{Deserialize, Serialize};
-use std::path::PathBuf;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub enum BibtexFormatter {
- Texlab,
- Latexindent,
-}
-
-impl Default for BibtexFormatter {
- fn default() -> Self {
- Self::Texlab
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct BibtexFormattingOptions {
- pub line_length: Option<i32>,
- pub formatter: Option<BibtexFormatter>,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)]
-pub struct LatexForwardSearchOptions {
- pub executable: Option<String>,
- pub args: Option<Vec<String>>,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Default)]
-#[serde(rename_all = "camelCase")]
-pub struct LatexLintOptions {
- pub on_change: Option<bool>,
- pub on_save: Option<bool>,
-}
-
-impl LatexLintOptions {
- pub fn on_change(&self) -> bool {
- self.on_change.unwrap_or(false)
- }
-
- pub fn on_save(&self) -> bool {
- self.on_save.unwrap_or(false)
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct LatexBuildOptions {
- pub executable: Option<String>,
- pub args: Option<Vec<String>>,
- pub on_save: Option<bool>,
- pub output_directory: Option<PathBuf>,
- pub forward_search_after: Option<bool>,
-}
-
-impl LatexBuildOptions {
- pub fn executable(&self) -> String {
- self.executable
- .as_ref()
- .map(Clone::clone)
- .unwrap_or_else(|| "latexmk".to_owned())
- }
-
- pub fn args(&self) -> Vec<String> {
- self.args.as_ref().map(Clone::clone).unwrap_or_else(|| {
- vec![
- "-pdf".to_owned(),
- "-interaction=nonstopmode".to_owned(),
- "-synctex=1".to_owned(),
- ]
- })
- }
-
- pub fn on_save(&self) -> bool {
- self.on_save.unwrap_or(false)
- }
-
- pub fn forward_search_after(&self) -> bool {
- self.forward_search_after.unwrap_or(false)
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct LatexOptions {
- pub forward_search: Option<LatexForwardSearchOptions>,
- pub lint: Option<LatexLintOptions>,
- pub build: Option<LatexBuildOptions>,
- pub root_directory: Option<PathBuf>,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct BibtexOptions {
- pub formatting: Option<BibtexFormattingOptions>,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct Options {
- pub latex: Option<LatexOptions>,
- pub bibtex: Option<BibtexOptions>,
-}
diff --git a/support/texlab/src/protocol/range.rs b/support/texlab/src/protocol/range.rs
deleted file mode 100644
index 774df1e699..0000000000
--- a/support/texlab/src/protocol/range.rs
+++ /dev/null
@@ -1,97 +0,0 @@
-use lsp_types::{Position, Range};
-
-pub trait RangeExt {
- fn new_simple(start_line: u64, start_character: u64, end_line: u64, end_character: u64)
- -> Self;
-
- fn contains(&self, pos: Position) -> bool;
-
- fn contains_exclusive(&self, pos: Position) -> bool;
-}
-
-impl RangeExt for Range {
- fn new_simple(
- start_line: u64,
- start_character: u64,
- end_line: u64,
- end_character: u64,
- ) -> Self {
- Self {
- start: Position::new(start_line, start_character),
- end: Position::new(end_line, end_character),
- }
- }
-
- fn contains(&self, pos: Position) -> bool {
- pos >= self.start && pos <= self.end
- }
-
- fn contains_exclusive(&self, pos: Position) -> bool {
- pos > self.start && pos < self.end
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn contains_inside() {
- let range = Range::new_simple(1, 2, 3, 4);
- assert!(range.contains(Position::new(2, 5)));
- }
-
- #[test]
- fn contains_begin() {
- let range = Range::new_simple(1, 2, 3, 4);
- assert!(range.contains(range.start));
- }
-
- #[test]
- fn contains_end() {
- let range = Range::new_simple(1, 2, 3, 4);
- assert!(range.contains(range.end));
- }
-
- #[test]
- fn contains_outside_left() {
- let range = Range::new_simple(1, 2, 3, 4);
- assert!(!range.contains(Position::new(0, 5)));
- }
-
- #[test]
- fn contains_outside_right() {
- let range = Range::new_simple(1, 2, 3, 4);
- assert!(!range.contains(Position::new(5, 1)));
- }
-
- #[test]
- fn contains_exclusive_inside() {
- let range = Range::new_simple(1, 2, 3, 4);
- assert!(range.contains_exclusive(Position::new(2, 5)));
- }
-
- #[test]
- fn contains_exclusive_begin() {
- let range = Range::new_simple(1, 2, 3, 4);
- assert!(!range.contains_exclusive(range.start));
- }
-
- #[test]
- fn contains_exclusive_end() {
- let range = Range::new_simple(1, 2, 3, 4);
- assert!(!range.contains_exclusive(range.end));
- }
-
- #[test]
- fn contains_exclusive_outside_left() {
- let range = Range::new_simple(1, 2, 3, 4);
- assert!(!range.contains_exclusive(Position::new(0, 5)));
- }
-
- #[test]
- fn contains_exclusive_outside_right() {
- let range = Range::new_simple(1, 2, 3, 4);
- assert!(!range.contains_exclusive(Position::new(5, 1)));
- }
-}
diff --git a/support/texlab/src/range.rs b/support/texlab/src/range.rs
new file mode 100644
index 0000000000..c70a573f39
--- /dev/null
+++ b/support/texlab/src/range.rs
@@ -0,0 +1,20 @@
+use lsp_types::{Position, Range};
+
+pub trait RangeExt {
+ fn new_simple(start_line: u32, start_character: u32, end_line: u32, end_character: u32)
+ -> Self;
+}
+
+impl RangeExt for Range {
+ fn new_simple(
+ start_line: u32,
+ start_character: u32,
+ end_line: u32,
+ end_character: u32,
+ ) -> Self {
+ Self {
+ start: Position::new(start_line, start_character),
+ end: Position::new(end_line, end_character),
+ }
+ }
+}
diff --git a/support/texlab/src/reference/bibtex_entry.rs b/support/texlab/src/reference/bibtex_entry.rs
deleted file mode 100644
index 2acf7bdfef..0000000000
--- a/support/texlab/src/reference/bibtex_entry.rs
+++ /dev/null
@@ -1,218 +0,0 @@
-use crate::{
- feature::{FeatureProvider, FeatureRequest},
- protocol::{Location, RangeExt, ReferenceParams, Url},
- syntax::{bibtex, latex, SyntaxNode},
- workspace::DocumentContent,
-};
-use async_trait::async_trait;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct BibtexEntryReferenceProvider;
-
-#[async_trait]
-impl FeatureProvider for BibtexEntryReferenceProvider {
- type Params = ReferenceParams;
- type Output = Vec<Location>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let mut refs = Vec::new();
- if let Some(key) = Self::find_key(req) {
- for doc in req.related() {
- match &doc.content {
- DocumentContent::Latex(table) => table
- .citations
- .iter()
- .flat_map(|citation| citation.keys(&table))
- .filter(|citation| citation.text() == key)
- .map(|citation| Location::new(doc.uri.clone().into(), citation.range()))
- .for_each(|location| refs.push(location)),
- DocumentContent::Bibtex(tree) => {
- if req.params.context.include_declaration {
- let uri: Url = doc.uri.clone().into();
- tree.children(tree.root)
- .filter_map(|node| tree.as_entry(node))
- .filter_map(|entry| entry.key.as_ref())
- .filter(|key_tok| key_tok.text() == key)
- .map(|key_tok| Location::new(uri.clone(), key_tok.range()))
- .for_each(|location| refs.push(location));
- }
- }
- }
- }
- }
- refs
- }
-}
-
-impl BibtexEntryReferenceProvider {
- fn find_key(req: &FeatureRequest<ReferenceParams>) -> Option<&str> {
- let pos = req.params.text_document_position.position;
- match &req.current().content {
- DocumentContent::Latex(table) => table
- .citations
- .iter()
- .flat_map(|citation| citation.keys(&table))
- .find(|key| key.range().contains(pos))
- .map(latex::Token::text),
- DocumentContent::Bibtex(tree) => tree
- .children(tree.root)
- .filter_map(|node| tree.as_entry(node))
- .filter_map(|entry| entry.key.as_ref())
- .find(|key| key.range().contains(pos))
- .map(bibtex::Token::text),
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{feature::FeatureTester, protocol::Range};
- use indoc::indoc;
-
- #[tokio::test]
- async fn entry() {
- let actual_refs = FeatureTester::new()
- .file("foo.bib", r#"@article{foo, bar = {baz}}"#)
- .file(
- "bar.tex",
- indoc!(
- r#"
- \addbibresource{foo.bib}
- \cite{foo}
- "#
- ),
- )
- .file("baz.tex", r#"\cite{foo}"#)
- .main("foo.bib")
- .position(0, 9)
- .test_reference(BibtexEntryReferenceProvider)
- .await;
-
- let expected_refs = vec![Location::new(
- FeatureTester::uri("bar.tex").into(),
- Range::new_simple(1, 6, 1, 9),
- )];
-
- assert_eq!(actual_refs, expected_refs);
- }
-
- #[tokio::test]
- async fn entry_include_declaration() {
- let actual_refs = FeatureTester::new()
- .file("foo.bib", r#"@article{foo, bar = {baz}}"#)
- .file(
- "bar.tex",
- indoc!(
- r#"
- \addbibresource{foo.bib}
- \cite{foo}
- "#
- ),
- )
- .file("baz.tex", r#"\cite{foo}"#)
- .main("foo.bib")
- .position(0, 9)
- .include_declaration()
- .test_reference(BibtexEntryReferenceProvider)
- .await;
-
- let expected_refs = vec![
- Location::new(
- FeatureTester::uri("foo.bib").into(),
- Range::new_simple(0, 9, 0, 12),
- ),
- Location::new(
- FeatureTester::uri("bar.tex").into(),
- Range::new_simple(1, 6, 1, 9),
- ),
- ];
-
- assert_eq!(actual_refs, expected_refs);
- }
-
- #[tokio::test]
- async fn citation() {
- let actual_refs = FeatureTester::new()
- .file("foo.bib", r#"@article{foo, bar = {baz}}"#)
- .file(
- "bar.tex",
- indoc!(
- r#"
- \addbibresource{foo.bib}
- \cite{foo}
- "#
- ),
- )
- .file("baz.tex", r#"\cite{foo}"#)
- .main("bar.tex")
- .position(1, 8)
- .test_reference(BibtexEntryReferenceProvider)
- .await;
-
- let expected_refs = vec![Location::new(
- FeatureTester::uri("bar.tex").into(),
- Range::new_simple(1, 6, 1, 9),
- )];
-
- assert_eq!(actual_refs, expected_refs);
- }
-
- #[tokio::test]
- async fn citation_include_declaration() {
- let actual_refs = FeatureTester::new()
- .file("foo.bib", r#"@article{foo, bar = {baz}}"#)
- .file(
- "bar.tex",
- indoc!(
- r#"
- \addbibresource{foo.bib}
- \cite{foo}
- "#
- ),
- )
- .file("baz.tex", r#"\cite{foo}"#)
- .main("bar.tex")
- .position(1, 8)
- .include_declaration()
- .test_reference(BibtexEntryReferenceProvider)
- .await;
-
- let expected_refs = vec![
- Location::new(
- FeatureTester::uri("bar.tex").into(),
- Range::new_simple(1, 6, 1, 9),
- ),
- Location::new(
- FeatureTester::uri("foo.bib").into(),
- Range::new_simple(0, 9, 0, 12),
- ),
- ];
-
- assert_eq!(actual_refs, expected_refs);
- }
-
- #[tokio::test]
- async fn empty_latex_document() {
- let actual_refs = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_reference(BibtexEntryReferenceProvider)
- .await;
-
- assert!(actual_refs.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let actual_refs = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_reference(BibtexEntryReferenceProvider)
- .await;
-
- assert!(actual_refs.is_empty());
- }
-}
diff --git a/support/texlab/src/reference/bibtex_string.rs b/support/texlab/src/reference/bibtex_string.rs
deleted file mode 100644
index 5f9804fbd6..0000000000
--- a/support/texlab/src/reference/bibtex_string.rs
+++ /dev/null
@@ -1,253 +0,0 @@
-use crate::{
- feature::{FeatureProvider, FeatureRequest},
- protocol::{Location, Position, RangeExt, ReferenceParams, Url},
- syntax::{
- bibtex::{self, Visitor},
- SyntaxNode,
- },
- workspace::DocumentContent,
-};
-use async_trait::async_trait;
-use petgraph::graph::NodeIndex;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct BibtexStringReferenceProvider;
-
-#[async_trait]
-impl FeatureProvider for BibtexStringReferenceProvider {
- type Params = ReferenceParams;
- type Output = Vec<Location>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let mut refs = Vec::new();
- if let DocumentContent::Bibtex(tree) = &req.current().content {
- if let Some(name) = Self::find_name(tree, req.params.text_document_position.position) {
- let uri: Url = req.current().uri.clone().into();
- if req.params.context.include_declaration {
- tree.children(tree.root)
- .filter_map(|node| tree.as_string(node))
- .filter_map(|string| string.name.as_ref())
- .filter(|string| string.text() == name.text())
- .for_each(|string| refs.push(Location::new(uri.clone(), string.range())));
- }
-
- let mut visitor = BibtexStringReferenceVisitor::default();
- visitor.visit(tree, tree.root);
- visitor
- .refs
- .into_iter()
- .filter(|reference| reference.text() == name.text())
- .for_each(|reference| refs.push(Location::new(uri.clone(), reference.range())));
- }
- }
- refs
- }
-}
-
-impl BibtexStringReferenceProvider {
- fn find_name(tree: &bibtex::Tree, pos: Position) -> Option<&bibtex::Token> {
- let mut nodes = tree.find(pos);
- nodes.reverse();
- let node0 = &tree.graph[nodes[0]];
- let node1 = nodes.get(1).map(|node| &tree.graph[*node]);
- match (node0, node1) {
- (bibtex::Node::Word(word), Some(bibtex::Node::Field(_)))
- | (bibtex::Node::Word(word), Some(bibtex::Node::Concat(_))) => Some(&word.token),
- (bibtex::Node::String(string), _) => string
- .name
- .as_ref()
- .filter(|name| name.range().contains(pos)),
- _ => None,
- }
- }
-}
-
-#[derive(Debug, Default)]
-pub struct BibtexStringReferenceVisitor<'a> {
- refs: Vec<&'a bibtex::Token>,
-}
-
-impl<'a> bibtex::Visitor<'a> for BibtexStringReferenceVisitor<'a> {
- fn visit(&mut self, tree: &'a bibtex::Tree, node: NodeIndex) {
- match &tree.graph[node] {
- bibtex::Node::Root(_)
- | bibtex::Node::Comment(_)
- | bibtex::Node::Preamble(_)
- | bibtex::Node::String(_)
- | bibtex::Node::Entry(_)
- | bibtex::Node::Word(_)
- | bibtex::Node::Command(_)
- | bibtex::Node::QuotedContent(_)
- | bibtex::Node::BracedContent(_) => (),
- bibtex::Node::Field(_) => {
- if let Some(word) = tree
- .children(node)
- .next()
- .and_then(|content| tree.as_word(content))
- {
- self.refs.push(&word.token);
- }
- }
- bibtex::Node::Concat(_) => {
- let mut children = tree.children(node);
- if let Some(word) = children.next().and_then(|left| tree.as_word(left)) {
- self.refs.push(&word.token);
- }
-
- if let Some(word) = children.next().and_then(|right| tree.as_word(right)) {
- self.refs.push(&word.token);
- }
- }
- }
- tree.walk(self, node);
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{feature::FeatureTester, protocol::Range};
- use indoc::indoc;
-
- #[tokio::test]
- async fn definition() {
- let actual_refs = FeatureTester::new()
- .file(
- "main.bib",
- indoc!(
- r#"
- @string{foo = {Foo}}
- @string{bar = {Bar}}
- @article{baz, author = foo}
- "#
- ),
- )
- .main("main.bib")
- .position(2, 24)
- .test_reference(BibtexStringReferenceProvider)
- .await;
-
- let expected_refs = vec![Location::new(
- FeatureTester::uri("main.bib").into(),
- Range::new_simple(2, 23, 2, 26),
- )];
-
- assert_eq!(actual_refs, expected_refs);
- }
-
- #[tokio::test]
- async fn definition_include_declaration() {
- let actual_refs = FeatureTester::new()
- .file(
- "main.bib",
- indoc!(
- r#"
- @string{foo = {Foo}}
- @string{bar = {Bar}}
- @article{baz, author = foo}
- "#
- ),
- )
- .main("main.bib")
- .position(2, 24)
- .include_declaration()
- .test_reference(BibtexStringReferenceProvider)
- .await;
-
- let expected_refs = vec![
- Location::new(
- FeatureTester::uri("main.bib").into(),
- Range::new_simple(0, 8, 0, 11),
- ),
- Location::new(
- FeatureTester::uri("main.bib").into(),
- Range::new_simple(2, 23, 2, 26),
- ),
- ];
-
- assert_eq!(actual_refs, expected_refs);
- }
-
- #[tokio::test]
- async fn reference() {
- let actual_refs = FeatureTester::new()
- .file(
- "main.bib",
- indoc!(
- r#"
- @string{foo = {Foo}}
- @string{bar = {Bar}}
- @article{baz, author = foo}
- "#
- ),
- )
- .main("main.bib")
- .position(0, 10)
- .test_reference(BibtexStringReferenceProvider)
- .await;
-
- let expected_refs = vec![Location::new(
- FeatureTester::uri("main.bib").into(),
- Range::new_simple(2, 23, 2, 26),
- )];
-
- assert_eq!(actual_refs, expected_refs);
- }
-
- #[tokio::test]
- async fn reference_include_declaration() {
- let actual_refs = FeatureTester::new()
- .file(
- "main.bib",
- indoc!(
- r#"
- @string{foo = {Foo}}
- @string{bar = {Bar}}
- @article{baz, author = foo}
- "#
- ),
- )
- .main("main.bib")
- .position(0, 10)
- .include_declaration()
- .test_reference(BibtexStringReferenceProvider)
- .await;
-
- let expected_refs = vec![
- Location::new(
- FeatureTester::uri("main.bib").into(),
- Range::new_simple(0, 8, 0, 11),
- ),
- Location::new(
- FeatureTester::uri("main.bib").into(),
- Range::new_simple(2, 23, 2, 26),
- ),
- ];
-
- assert_eq!(actual_refs, expected_refs);
- }
-
- #[tokio::test]
- async fn empty_latex_document() {
- let actual_refs = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_reference(BibtexStringReferenceProvider)
- .await;
-
- assert!(actual_refs.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let actual_refs = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_reference(BibtexStringReferenceProvider)
- .await;
-
- assert!(actual_refs.is_empty());
- }
-}
diff --git a/support/texlab/src/reference/latex_label.rs b/support/texlab/src/reference/latex_label.rs
deleted file mode 100644
index 16686f6015..0000000000
--- a/support/texlab/src/reference/latex_label.rs
+++ /dev/null
@@ -1,211 +0,0 @@
-use crate::{
- feature::{FeatureProvider, FeatureRequest},
- protocol::{Location, RangeExt, ReferenceParams},
- syntax::{latex, LatexLabelKind, SyntaxNode},
- workspace::DocumentContent,
-};
-use async_trait::async_trait;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct LatexLabelReferenceProvider;
-
-#[async_trait]
-impl FeatureProvider for LatexLabelReferenceProvider {
- type Params = ReferenceParams;
- type Output = Vec<Location>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let mut refs = Vec::new();
- if let Some(def) = Self::find_name(req) {
- for doc in req.related() {
- if let DocumentContent::Latex(table) = &doc.content {
- table
- .labels
- .iter()
- .filter(|label| Self::is_included(req, label))
- .flat_map(|label| label.names(&table))
- .filter(|label| label.text() == def)
- .map(|label| Location::new(doc.uri.clone().into(), label.range()))
- .for_each(|location| refs.push(location));
- }
- }
- }
- refs
- }
-}
-
-impl LatexLabelReferenceProvider {
- fn find_name(req: &FeatureRequest<ReferenceParams>) -> Option<&str> {
- let pos = req.params.text_document_position.position;
- if let DocumentContent::Latex(table) = &req.current().content {
- table
- .labels
- .iter()
- .flat_map(|label| label.names(&table))
- .find(|label| label.range().contains(pos))
- .map(latex::Token::text)
- } else {
- None
- }
- }
-
- fn is_included(req: &FeatureRequest<ReferenceParams>, label: &latex::Label) -> bool {
- match label.kind {
- LatexLabelKind::Reference(_) => true,
- LatexLabelKind::Definition => req.params.context.include_declaration,
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{feature::FeatureTester, protocol::Range};
- use indoc::indoc;
-
- #[tokio::test]
- async fn definition() {
- let actual_refs = FeatureTester::new()
- .file("foo.tex", r#"\label{foo}"#)
- .file(
- "bar.tex",
- indoc!(
- r#"
- \input{foo.tex}
- \ref{foo}
- "#
- ),
- )
- .file("baz.tex", r#"\ref{foo}"#)
- .main("foo.tex")
- .position(0, 8)
- .test_reference(LatexLabelReferenceProvider)
- .await;
-
- let expected_refs = vec![Location::new(
- FeatureTester::uri("bar.tex").into(),
- Range::new_simple(1, 5, 1, 8),
- )];
-
- assert_eq!(actual_refs, expected_refs);
- }
-
- #[tokio::test]
- async fn definition_include_declaration() {
- let actual_refs = FeatureTester::new()
- .file("foo.tex", r#"\label{foo}"#)
- .file(
- "bar.tex",
- indoc!(
- r#"
- \input{foo.tex}
- \ref{foo}
- "#
- ),
- )
- .file("baz.tex", r#"\ref{foo}"#)
- .main("foo.tex")
- .position(0, 8)
- .include_declaration()
- .test_reference(LatexLabelReferenceProvider)
- .await;
-
- let expected_refs = vec![
- Location::new(
- FeatureTester::uri("foo.tex").into(),
- Range::new_simple(0, 7, 0, 10),
- ),
- Location::new(
- FeatureTester::uri("bar.tex").into(),
- Range::new_simple(1, 5, 1, 8),
- ),
- ];
-
- assert_eq!(actual_refs, expected_refs);
- }
-
- #[tokio::test]
- async fn reference() {
- let actual_refs = FeatureTester::new()
- .file("foo.tex", r#"\label{foo}"#)
- .file(
- "bar.tex",
- indoc!(
- r#"
- \input{foo.tex}
- \ref{foo}
- "#
- ),
- )
- .file("baz.tex", r#"\ref{foo}"#)
- .main("bar.tex")
- .position(1, 7)
- .test_reference(LatexLabelReferenceProvider)
- .await;
-
- let expected_refs = vec![Location::new(
- FeatureTester::uri("bar.tex").into(),
- Range::new_simple(1, 5, 1, 8),
- )];
-
- assert_eq!(actual_refs, expected_refs);
- }
-
- #[tokio::test]
- async fn reference_include_declaration() {
- let actual_refs = FeatureTester::new()
- .file("foo.tex", r#"\label{foo}"#)
- .file(
- "bar.tex",
- indoc!(
- r#"
- \input{foo.tex}
- \ref{foo}
- "#
- ),
- )
- .file("baz.tex", r#"\ref{foo}"#)
- .main("bar.tex")
- .position(1, 7)
- .include_declaration()
- .test_reference(LatexLabelReferenceProvider)
- .await;
-
- let expected_refs = vec![
- Location::new(
- FeatureTester::uri("bar.tex").into(),
- Range::new_simple(1, 5, 1, 8),
- ),
- Location::new(
- FeatureTester::uri("foo.tex").into(),
- Range::new_simple(0, 7, 0, 10),
- ),
- ];
-
- assert_eq!(actual_refs, expected_refs);
- }
-
- #[tokio::test]
- async fn empty_latex_document() {
- let actual_refs = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .test_reference(LatexLabelReferenceProvider)
- .await;
-
- assert!(actual_refs.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let actual_refs = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .test_reference(LatexLabelReferenceProvider)
- .await;
-
- assert!(actual_refs.is_empty());
- }
-}
diff --git a/support/texlab/src/reference/mod.rs b/support/texlab/src/reference/mod.rs
deleted file mode 100644
index fc4379d394..0000000000
--- a/support/texlab/src/reference/mod.rs
+++ /dev/null
@@ -1,45 +0,0 @@
-mod bibtex_entry;
-mod bibtex_string;
-mod latex_label;
-
-use self::{
- bibtex_entry::BibtexEntryReferenceProvider, bibtex_string::BibtexStringReferenceProvider,
- latex_label::LatexLabelReferenceProvider,
-};
-use crate::{
- feature::{ConcatProvider, FeatureProvider, FeatureRequest},
- protocol::{Location, ReferenceParams},
-};
-use async_trait::async_trait;
-
-pub struct ReferenceProvider {
- provider: ConcatProvider<ReferenceParams, Location>,
-}
-
-impl ReferenceProvider {
- pub fn new() -> Self {
- Self {
- provider: ConcatProvider::new(vec![
- Box::new(BibtexEntryReferenceProvider),
- Box::new(BibtexStringReferenceProvider),
- Box::new(LatexLabelReferenceProvider),
- ]),
- }
- }
-}
-
-impl Default for ReferenceProvider {
- fn default() -> Self {
- Self::new()
- }
-}
-
-#[async_trait]
-impl FeatureProvider for ReferenceProvider {
- type Params = ReferenceParams;
- type Output = Vec<Location>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- self.provider.execute(req).await
- }
-}
diff --git a/support/texlab/src/rename/bibtex_entry.rs b/support/texlab/src/rename/bibtex_entry.rs
deleted file mode 100644
index 97eaa8fd9d..0000000000
--- a/support/texlab/src/rename/bibtex_entry.rs
+++ /dev/null
@@ -1,192 +0,0 @@
-use crate::{
- feature::{FeatureProvider, FeatureRequest},
- protocol::{
- Position, Range, RangeExt, RenameParams, TextDocumentPositionParams, TextEdit,
- WorkspaceEdit,
- },
- syntax::{Span, SyntaxNode},
- workspace::DocumentContent,
-};
-use async_trait::async_trait;
-use std::collections::HashMap;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct BibtexEntryPrepareRenameProvider;
-
-#[async_trait]
-impl FeatureProvider for BibtexEntryPrepareRenameProvider {
- type Params = TextDocumentPositionParams;
- type Output = Option<Range>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- find_key(&req.current().content, req.params.position).map(Span::range)
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct BibtexEntryRenameProvider;
-
-#[async_trait]
-impl FeatureProvider for BibtexEntryRenameProvider {
- type Params = RenameParams;
- type Output = Option<WorkspaceEdit>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let key_name = find_key(
- &req.current().content,
- req.params.text_document_position.position,
- )?;
- let mut changes = HashMap::new();
- for doc in req.related() {
- let edits = match &doc.content {
- DocumentContent::Latex(table) => table
- .citations
- .iter()
- .flat_map(|citation| citation.keys(&table))
- .filter(|citation| citation.text() == key_name.text)
- .map(|citation| TextEdit::new(citation.range(), req.params.new_name.clone()))
- .collect(),
- DocumentContent::Bibtex(tree) => tree
- .children(tree.root)
- .filter_map(|node| tree.as_entry(node))
- .filter_map(|entry| entry.key.as_ref())
- .filter(|entry_key| entry_key.text() == key_name.text)
- .map(|entry_key| TextEdit::new(entry_key.range(), req.params.new_name.clone()))
- .collect(),
- };
- changes.insert(doc.uri.clone().into(), edits);
- }
- Some(WorkspaceEdit::new(changes))
- }
-}
-
-fn find_key(content: &DocumentContent, pos: Position) -> Option<&Span> {
- match content {
- DocumentContent::Latex(table) => table
- .citations
- .iter()
- .flat_map(|citation| citation.keys(&table))
- .find(|key| key.range().contains(pos))
- .map(|key| &key.span),
- DocumentContent::Bibtex(tree) => tree
- .children(tree.root)
- .filter_map(|node| tree.as_entry(node))
- .filter_map(|entry| entry.key.as_ref())
- .find(|key| key.range().contains(pos))
- .map(|key| &key.span),
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{feature::FeatureTester, protocol::RangeExt};
- use indoc::indoc;
-
- #[tokio::test]
- async fn entry() {
- let actual_edit = FeatureTester::new()
- .file("main.bib", r#"@article{foo, bar = baz}"#)
- .file(
- "main.tex",
- indoc!(
- r#"
- \addbibresource{main.bib}
- \cite{foo}
- "#
- ),
- )
- .main("main.bib")
- .position(0, 9)
- .new_name("qux")
- .test_rename(BibtexEntryRenameProvider)
- .await
- .unwrap();
-
- let mut expected_changes = HashMap::new();
- expected_changes.insert(
- FeatureTester::uri("main.bib").into(),
- vec![TextEdit::new(Range::new_simple(0, 9, 0, 12), "qux".into())],
- );
- expected_changes.insert(
- FeatureTester::uri("main.tex").into(),
- vec![TextEdit::new(Range::new_simple(1, 6, 1, 9), "qux".into())],
- );
- let expected_edit = WorkspaceEdit::new(expected_changes);
-
- assert_eq!(actual_edit, expected_edit);
- }
-
- #[tokio::test]
- async fn citation() {
- let actual_edit = FeatureTester::new()
- .file("main.bib", r#"@article{foo, bar = baz}"#)
- .file(
- "main.tex",
- indoc!(
- r#"
- \addbibresource{main.bib}
- \cite{foo}
- "#
- ),
- )
- .main("main.tex")
- .position(1, 6)
- .new_name("qux")
- .test_rename(BibtexEntryRenameProvider)
- .await
- .unwrap();
-
- let mut expected_changes = HashMap::new();
- expected_changes.insert(
- FeatureTester::uri("main.bib").into(),
- vec![TextEdit::new(Range::new_simple(0, 9, 0, 12), "qux".into())],
- );
- expected_changes.insert(
- FeatureTester::uri("main.tex").into(),
- vec![TextEdit::new(Range::new_simple(1, 6, 1, 9), "qux".into())],
- );
- let expected_edit = WorkspaceEdit::new(expected_changes);
-
- assert_eq!(actual_edit, expected_edit);
- }
-
- #[tokio::test]
- async fn field_name() {
- let actual_edit = FeatureTester::new()
- .file("main.bib", r#"@article{foo, bar = baz}"#)
- .main("main.bib")
- .position(0, 14)
- .new_name("qux")
- .test_rename(BibtexEntryRenameProvider)
- .await;
-
- assert_eq!(actual_edit, None);
- }
-
- #[tokio::test]
- async fn empty_latex_document() {
- let actual_edit = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .new_name("")
- .test_rename(BibtexEntryRenameProvider)
- .await;
-
- assert_eq!(actual_edit, None);
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let actual_edit = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .new_name("")
- .test_rename(BibtexEntryRenameProvider)
- .await;
-
- assert_eq!(actual_edit, None);
- }
-}
diff --git a/support/texlab/src/rename/latex_cmd.rs b/support/texlab/src/rename/latex_cmd.rs
deleted file mode 100644
index 4770c4c485..0000000000
--- a/support/texlab/src/rename/latex_cmd.rs
+++ /dev/null
@@ -1,128 +0,0 @@
-use crate::{
- feature::{FeatureProvider, FeatureRequest},
- protocol::{
- Position, Range, RenameParams, TextDocumentPositionParams, TextEdit, WorkspaceEdit,
- },
- syntax::{latex, SyntaxNode},
- workspace::DocumentContent,
-};
-use async_trait::async_trait;
-use std::collections::HashMap;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct LatexCommandPrepareRenameProvider;
-
-#[async_trait]
-impl FeatureProvider for LatexCommandPrepareRenameProvider {
- type Params = TextDocumentPositionParams;
- type Output = Option<Range>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let pos = req.params.position;
- find_command(&req.current().content, pos).map(SyntaxNode::range)
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct LatexCommandRenameProvider;
-
-#[async_trait]
-impl FeatureProvider for LatexCommandRenameProvider {
- type Params = RenameParams;
- type Output = Option<WorkspaceEdit>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let pos = req.params.text_document_position.position;
- let cmd_name = find_command(&req.current().content, pos)?.name.text();
- let mut changes = HashMap::new();
- for doc in req.related() {
- if let DocumentContent::Latex(table) = &doc.content {
- let edits = table
- .commands
- .iter()
- .filter_map(|node| table.as_command(*node))
- .filter(|cmd| cmd.name.text() == cmd_name)
- .map(|cmd| {
- TextEdit::new(cmd.name.range(), format!("\\{}", req.params.new_name))
- })
- .collect();
- changes.insert(doc.uri.clone().into(), edits);
- }
- }
- Some(WorkspaceEdit::new(changes))
- }
-}
-
-fn find_command(content: &DocumentContent, pos: Position) -> Option<&latex::Command> {
- if let DocumentContent::Latex(table) = &content {
- table.as_command(table.find_command_by_short_name_range(pos)?)
- } else {
- None
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{feature::FeatureTester, protocol::RangeExt};
- use indoc::indoc;
-
- #[tokio::test]
- async fn command() {
- let actual_edit = FeatureTester::new()
- .file(
- "foo.tex",
- indoc!(
- r#"
- \include{bar.tex}
- \baz
- "#
- ),
- )
- .file("bar.tex", r#"\baz"#)
- .main("foo.tex")
- .position(1, 2)
- .new_name("qux")
- .test_rename(LatexCommandRenameProvider)
- .await
- .unwrap();
-
- let mut expected_changes = HashMap::new();
- expected_changes.insert(
- FeatureTester::uri("foo.tex").into(),
- vec![TextEdit::new(Range::new_simple(1, 0, 1, 4), "\\qux".into())],
- );
- expected_changes.insert(
- FeatureTester::uri("bar.tex").into(),
- vec![TextEdit::new(Range::new_simple(0, 0, 0, 4), "\\qux".into())],
- );
-
- assert_eq!(actual_edit, WorkspaceEdit::new(expected_changes));
- }
-
- #[tokio::test]
- async fn empty_latex_document() {
- let actual_edit = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .new_name("")
- .test_rename(LatexCommandRenameProvider)
- .await;
-
- assert_eq!(actual_edit, None);
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let actual_edit = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .new_name("")
- .test_rename(LatexCommandRenameProvider)
- .await;
-
- assert_eq!(actual_edit, None);
- }
-}
diff --git a/support/texlab/src/rename/latex_env.rs b/support/texlab/src/rename/latex_env.rs
deleted file mode 100644
index 6e302ef8b6..0000000000
--- a/support/texlab/src/rename/latex_env.rs
+++ /dev/null
@@ -1,157 +0,0 @@
-use crate::{
- feature::{FeatureProvider, FeatureRequest},
- protocol::{
- Position, Range, RangeExt, RenameParams, TextDocumentPositionParams, TextEdit,
- WorkspaceEdit,
- },
- syntax::{latex, SyntaxNode},
- workspace::DocumentContent,
-};
-use async_trait::async_trait;
-use std::collections::HashMap;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct LatexEnvironmentPrepareRenameProvider;
-
-#[async_trait]
-impl FeatureProvider for LatexEnvironmentPrepareRenameProvider {
- type Params = TextDocumentPositionParams;
- type Output = Option<Range>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let pos = req.params.position;
- let (left_name, right_name) = find_environment(&req.current().content, pos)?;
- let range = if left_name.range().contains(pos) {
- left_name.range()
- } else {
- right_name.range()
- };
- Some(range)
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct LatexEnvironmentRenameProvider;
-
-#[async_trait]
-impl FeatureProvider for LatexEnvironmentRenameProvider {
- type Params = RenameParams;
- type Output = Option<WorkspaceEdit>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let (left_name, right_name) = find_environment(
- &req.current().content,
- req.params.text_document_position.position,
- )?;
- let edits = vec![
- TextEdit::new(left_name.range(), req.params.new_name.clone()),
- TextEdit::new(right_name.range(), req.params.new_name.clone()),
- ];
- let mut changes = HashMap::new();
- changes.insert(req.current().uri.clone().into(), edits);
- Some(WorkspaceEdit::new(changes))
- }
-}
-
-fn find_environment(
- content: &DocumentContent,
- pos: Position,
-) -> Option<(&latex::Token, &latex::Token)> {
- if let DocumentContent::Latex(table) = content {
- for env in &table.environments {
- if let Some(left_name) = env.left.name(&table) {
- if let Some(right_name) = env.right.name(&table) {
- if left_name.range().contains(pos) || right_name.range().contains(pos) {
- return Some((left_name, right_name));
- }
- }
- }
- }
- }
- None
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::feature::FeatureTester;
- use indoc::indoc;
-
- #[tokio::test]
- async fn environment() {
- let actual_edit = FeatureTester::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \begin{foo}
- \end{bar}
- "#
- ),
- )
- .main("main.tex")
- .position(0, 8)
- .new_name("baz")
- .test_rename(LatexEnvironmentRenameProvider)
- .await
- .unwrap();
-
- let mut expected_changes = HashMap::new();
- expected_changes.insert(
- FeatureTester::uri("main.tex").into(),
- vec![
- TextEdit::new(Range::new_simple(0, 7, 0, 10), "baz".into()),
- TextEdit::new(Range::new_simple(1, 5, 1, 8), "baz".into()),
- ],
- );
-
- assert_eq!(actual_edit, WorkspaceEdit::new(expected_changes));
- }
-
- #[tokio::test]
- async fn command() {
- let actual_edit = FeatureTester::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \begin{foo}
- \end{bar}
- "#
- ),
- )
- .main("main.tex")
- .position(0, 5)
- .new_name("baz")
- .test_rename(LatexEnvironmentRenameProvider)
- .await;
-
- assert_eq!(actual_edit, None);
- }
-
- #[tokio::test]
- async fn empty_latex_document() {
- let actual_edit = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .new_name("")
- .test_rename(LatexEnvironmentRenameProvider)
- .await;
-
- assert_eq!(actual_edit, None);
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let actual_edit = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .new_name("")
- .test_rename(LatexEnvironmentRenameProvider)
- .await;
-
- assert_eq!(actual_edit, None);
- }
-}
diff --git a/support/texlab/src/rename/latex_label.rs b/support/texlab/src/rename/latex_label.rs
deleted file mode 100644
index 27e0f9b224..0000000000
--- a/support/texlab/src/rename/latex_label.rs
+++ /dev/null
@@ -1,146 +0,0 @@
-use crate::{
- feature::{FeatureProvider, FeatureRequest},
- protocol::{
- Position, Range, RangeExt, RenameParams, TextDocumentPositionParams, TextEdit,
- WorkspaceEdit,
- },
- syntax::{Span, SyntaxNode},
- workspace::DocumentContent,
-};
-use async_trait::async_trait;
-use std::collections::HashMap;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct LatexLabelPrepareRenameProvider;
-
-#[async_trait]
-impl FeatureProvider for LatexLabelPrepareRenameProvider {
- type Params = TextDocumentPositionParams;
- type Output = Option<Range>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let pos = req.params.position;
- find_label(&req.current().content, pos).map(Span::range)
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct LatexLabelRenameProvider;
-
-#[async_trait]
-impl FeatureProvider for LatexLabelRenameProvider {
- type Params = RenameParams;
- type Output = Option<WorkspaceEdit>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let pos = req.params.text_document_position.position;
- let name = find_label(&req.current().content, pos)?;
- let mut changes = HashMap::new();
- for doc in req.related() {
- if let DocumentContent::Latex(table) = &doc.content {
- let edits = table
- .labels
- .iter()
- .flat_map(|label| label.names(&table))
- .filter(|label| label.text() == name.text)
- .map(|label| TextEdit::new(label.range(), req.params.new_name.clone()))
- .collect();
- changes.insert(doc.uri.clone().into(), edits);
- }
- }
- Some(WorkspaceEdit::new(changes))
- }
-}
-
-fn find_label(content: &DocumentContent, pos: Position) -> Option<&Span> {
- if let DocumentContent::Latex(table) = content {
- table
- .labels
- .iter()
- .flat_map(|label| label.names(&table))
- .find(|label| label.range().contains(pos))
- .map(|label| &label.span)
- } else {
- None
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::feature::FeatureTester;
- use indoc::indoc;
-
- #[tokio::test]
- async fn label() {
- let actual_edit = FeatureTester::new()
- .file(
- "foo.tex",
- indoc!(
- r#"
- \label{foo}
- \include{bar}
- "#
- ),
- )
- .file("bar.tex", r#"\ref{foo}"#)
- .file("baz.tex", r#"\ref{foo}"#)
- .main("foo.tex")
- .position(0, 7)
- .new_name("bar")
- .test_rename(LatexLabelRenameProvider)
- .await
- .unwrap();
-
- let mut expected_changes = HashMap::new();
- expected_changes.insert(
- FeatureTester::uri("foo.tex").into(),
- vec![TextEdit::new(Range::new_simple(0, 7, 0, 10), "bar".into())],
- );
- expected_changes.insert(
- FeatureTester::uri("bar.tex").into(),
- vec![TextEdit::new(Range::new_simple(0, 5, 0, 8), "bar".into())],
- );
-
- assert_eq!(actual_edit, WorkspaceEdit::new(expected_changes));
- }
-
- #[tokio::test]
- async fn command_args() {
- let actual_edit = FeatureTester::new()
- .file("main.tex", r#"\foo{bar}"#)
- .main("main.tex")
- .position(0, 5)
- .new_name("baz")
- .test_rename(LatexLabelRenameProvider)
- .await;
-
- assert_eq!(actual_edit, None);
- }
-
- #[tokio::test]
- async fn empty_latex_document() {
- let actual_edit = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .position(0, 0)
- .new_name("")
- .test_rename(LatexLabelRenameProvider)
- .await;
-
- assert_eq!(actual_edit, None);
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let actual_edit = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .position(0, 0)
- .new_name("")
- .test_rename(LatexLabelRenameProvider)
- .await;
-
- assert_eq!(actual_edit, None);
- }
-}
diff --git a/support/texlab/src/rename/mod.rs b/support/texlab/src/rename/mod.rs
deleted file mode 100644
index 40bf8ce1e1..0000000000
--- a/support/texlab/src/rename/mod.rs
+++ /dev/null
@@ -1,88 +0,0 @@
-mod bibtex_entry;
-mod latex_cmd;
-mod latex_env;
-mod latex_label;
-
-use self::{
- bibtex_entry::{BibtexEntryPrepareRenameProvider, BibtexEntryRenameProvider},
- latex_cmd::{LatexCommandPrepareRenameProvider, LatexCommandRenameProvider},
- latex_env::{LatexEnvironmentPrepareRenameProvider, LatexEnvironmentRenameProvider},
- latex_label::{LatexLabelPrepareRenameProvider, LatexLabelRenameProvider},
-};
-use crate::{
- feature::{ChoiceProvider, FeatureProvider, FeatureRequest},
- protocol::{Range, RenameParams, TextDocumentPositionParams, WorkspaceEdit},
-};
-use async_trait::async_trait;
-
-pub struct PrepareRenameProvider {
- provider: ChoiceProvider<TextDocumentPositionParams, Range>,
-}
-
-impl PrepareRenameProvider {
- pub fn new() -> Self {
- Self {
- provider: ChoiceProvider::new(vec![
- Box::new(BibtexEntryPrepareRenameProvider),
- Box::new(LatexCommandPrepareRenameProvider),
- Box::new(LatexEnvironmentPrepareRenameProvider),
- Box::new(LatexLabelPrepareRenameProvider),
- ]),
- }
- }
-}
-
-impl Default for PrepareRenameProvider {
- fn default() -> Self {
- Self::new()
- }
-}
-
-#[async_trait]
-impl FeatureProvider for PrepareRenameProvider {
- type Params = TextDocumentPositionParams;
- type Output = Option<Range>;
-
- async fn execute<'a>(
- &'a self,
- req: &'a FeatureRequest<TextDocumentPositionParams>,
- ) -> Option<Range> {
- self.provider.execute(req).await
- }
-}
-
-pub struct RenameProvider {
- provider: ChoiceProvider<RenameParams, WorkspaceEdit>,
-}
-
-impl RenameProvider {
- pub fn new() -> Self {
- Self {
- provider: ChoiceProvider::new(vec![
- Box::new(BibtexEntryRenameProvider),
- Box::new(LatexCommandRenameProvider),
- Box::new(LatexEnvironmentRenameProvider),
- Box::new(LatexLabelRenameProvider),
- ]),
- }
- }
-}
-
-impl Default for RenameProvider {
- fn default() -> Self {
- Self::new()
- }
-}
-
-#[async_trait]
-impl FeatureProvider for RenameProvider {
- type Params = RenameParams;
- type Output = Option<WorkspaceEdit>;
-
- async fn execute<'a>(
- &'a self,
- request: &'a FeatureRequest<RenameParams>,
- ) -> Option<WorkspaceEdit> {
- self.provider.execute(request).await
- }
-}
diff --git a/support/texlab/src/req_queue.rs b/support/texlab/src/req_queue.rs
new file mode 100644
index 0000000000..3fb875a8be
--- /dev/null
+++ b/support/texlab/src/req_queue.rs
@@ -0,0 +1,13 @@
+use cancellation::CancellationTokenSource;
+use crossbeam_channel::Sender;
+use lsp_server::ResponseError;
+
+pub struct IncomingData {
+ pub(crate) token_source: CancellationTokenSource,
+}
+
+pub struct OutgoingData {
+ pub(crate) sender: Sender<Result<serde_json::Value, ResponseError>>,
+}
+
+pub type ReqQueue = lsp_server::ReqQueue<IncomingData, OutgoingData>;
diff --git a/support/texlab/src/server.rs b/support/texlab/src/server.rs
index 9c5e1b857b..5a78d5e2e2 100644
--- a/support/texlab/src/server.rs
+++ b/support/texlab/src/server.rs
@@ -1,131 +1,113 @@
-#[cfg(feature = "citation")]
-use crate::citeproc::render_citation;
+use std::{
+ path::PathBuf,
+ sync::{Arc, Mutex},
+ thread,
+};
+
+use anyhow::Result;
+use cancellation::{CancellationToken, CancellationTokenSource};
+use crossbeam_channel::Sender;
+use log::{info, warn};
+use lsp_server::{Connection, ErrorCode, Message, RequestId};
+use lsp_types::{
+ notification::{
+ Cancel, DidChangeConfiguration, DidChangeTextDocument, DidOpenTextDocument,
+ DidSaveTextDocument, LogMessage, PublishDiagnostics,
+ },
+ request::{
+ DocumentLinkRequest, FoldingRangeRequest, Formatting, GotoDefinition, PrepareRenameRequest,
+ References, Rename, SemanticTokensRangeRequest,
+ },
+ *,
+};
+use notification::DidCloseTextDocument;
+use request::{
+ Completion, DocumentHighlightRequest, DocumentSymbolRequest, HoverRequest,
+ ResolveCompletionItem, WorkspaceSymbol,
+};
+use serde::Serialize;
+use threadpool::ThreadPool;
use crate::{
- build::BuildProvider,
- completion::{CompletionItemData, CompletionProvider, COMPLETION_LIMIT},
- components::COMPONENT_DATABASE,
- config::ConfigManager,
- definition::DefinitionProvider,
- diagnostics::DiagnosticsManager,
- feature::{DocumentView, FeatureProvider, FeatureRequest},
- folding::FoldingProvider,
- forward_search,
- highlight::HighlightProvider,
- hover::HoverProvider,
- link::LinkProvider,
- protocol::*,
- reference::ReferenceProvider,
- rename::{PrepareRenameProvider, RenameProvider},
- symbol::{document_symbols, workspace_symbols, SymbolProvider},
- syntax::{bibtex, latexindent, CharStream, SyntaxNode},
- tex::{Distribution, DistributionKind, KpsewhichError},
- workspace::{DocumentContent, Workspace},
+ client::send_notification,
+ component_db::COMPONENT_DATABASE,
+ config::{pull_config, push_config, register_config_capability},
+ create_workspace_full,
+ diagnostics::{DiagnosticsDebouncer, DiagnosticsManager, DiagnosticsMessage},
+ dispatch::{NotificationDispatcher, RequestDispatcher},
+ distro::Distribution,
+ features::{
+ build_document, find_all_references, find_document_highlights, find_document_links,
+ find_document_symbols, find_foldings, find_hover, find_workspace_symbols,
+ format_source_code, goto_definition, prepare_rename_all, rename_all, BuildParams,
+ BuildResult, FeatureRequest, ForwardSearchResult,
+ },
+ req_queue::{IncomingData, ReqQueue},
+ DocumentLanguage, ServerContext, Uri, Workspace, WorkspaceSource,
};
-use async_trait::async_trait;
-use chashmap::CHashMap;
-use futures::lock::Mutex;
-use jsonrpc::{server::Result, Middleware};
-use jsonrpc_derive::{jsonrpc_method, jsonrpc_server};
-use log::{debug, error, info, warn};
-use once_cell::sync::{Lazy, OnceCell};
-use std::{mem, path::PathBuf, sync::Arc};
-
-pub struct LatexLspServer<C> {
- distro: Arc<dyn Distribution>,
- client: Arc<C>,
- client_capabilities: OnceCell<Arc<ClientCapabilities>>,
- client_info: OnceCell<Option<ClientInfo>>,
- current_dir: Arc<PathBuf>,
- config_manager: OnceCell<ConfigManager<C>>,
- action_manager: ActionManager,
- workspace: Workspace,
- build_provider: BuildProvider<C>,
- completion_provider: CompletionProvider,
- definition_provider: DefinitionProvider,
- folding_provider: FoldingProvider,
- highlight_provider: HighlightProvider,
- link_provider: LinkProvider,
- reference_provider: ReferenceProvider,
- prepare_rename_provider: PrepareRenameProvider,
- rename_provider: RenameProvider,
- symbol_provider: SymbolProvider,
- hover_provider: HoverProvider,
- diagnostics_manager: DiagnosticsManager,
- last_position_by_uri: CHashMap<Uri, Position>,
+
+pub struct Server {
+ connection: Connection,
+ context: Arc<ServerContext>,
+ req_queue: Arc<Mutex<ReqQueue>>,
+ workspace: Arc<dyn Workspace>,
+ static_debouncer: DiagnosticsDebouncer,
+ chktex_debouncer: DiagnosticsDebouncer,
+ pool: ThreadPool,
+ load_resolver: bool,
}
-#[jsonrpc_server]
-impl<C: LspClient + Send + Sync + 'static> LatexLspServer<C> {
- pub fn new(distro: Arc<dyn Distribution>, client: Arc<C>, current_dir: Arc<PathBuf>) -> Self {
- let workspace = Workspace::new(distro.clone(), Arc::clone(&current_dir));
- Self {
- distro,
- client: Arc::clone(&client),
- client_capabilities: OnceCell::new(),
- client_info: OnceCell::new(),
- current_dir,
- config_manager: OnceCell::new(),
- action_manager: ActionManager::default(),
+impl Server {
+ pub fn with_connection(
+ connection: Connection,
+ current_dir: PathBuf,
+ load_resolver: bool,
+ ) -> Result<Self> {
+ let context = Arc::new(ServerContext::new(current_dir));
+ let req_queue = Arc::default();
+ let workspace = Arc::new(create_workspace_full(Arc::clone(&context))?);
+ let diag_manager = Arc::new(Mutex::new(DiagnosticsManager::default()));
+
+ let static_debouncer =
+ create_static_debouncer(Arc::clone(&diag_manager), &connection, Arc::clone(&context));
+
+ let chktex_debouncer =
+ create_chktex_debouncer(diag_manager, &connection, Arc::clone(&context));
+
+ Ok(Self {
+ connection,
+ context,
+ req_queue,
workspace,
- build_provider: BuildProvider::new(client),
- completion_provider: CompletionProvider::new(),
- definition_provider: DefinitionProvider::new(),
- folding_provider: FoldingProvider::new(),
- highlight_provider: HighlightProvider::new(),
- link_provider: LinkProvider::new(),
- reference_provider: ReferenceProvider::new(),
- prepare_rename_provider: PrepareRenameProvider::new(),
- rename_provider: RenameProvider::new(),
- symbol_provider: SymbolProvider::new(),
- hover_provider: HoverProvider::new(),
- diagnostics_manager: DiagnosticsManager::default(),
- last_position_by_uri: CHashMap::new(),
- }
- }
-
- fn client_capabilities(&self) -> Arc<ClientCapabilities> {
- Arc::clone(
- self.client_capabilities
- .get()
- .expect("initialize has not been called"),
- )
- }
-
- fn config_manager(&self) -> &ConfigManager<C> {
- self.config_manager
- .get()
- .expect("initialize has not been called")
+ static_debouncer,
+ chktex_debouncer,
+ pool: threadpool::Builder::new().build(),
+ load_resolver,
+ })
}
- #[jsonrpc_method("initialize", kind = "request")]
- pub async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
- self.client_capabilities
- .set(Arc::new(params.capabilities))
- .expect("initialize was called two times");
-
- self.client_info
- .set(params.client_info)
- .expect("initialize was called two times");
-
- let _ = self.config_manager.set(ConfigManager::new(
- Arc::clone(&self.client),
- self.client_capabilities(),
- ));
-
- let capabilities = ServerCapabilities {
+ fn capabilities(&self) -> ServerCapabilities {
+ ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Options(
TextDocumentSyncOptions {
open_close: Some(true),
change: Some(TextDocumentSyncKind::Full),
will_save: None,
will_save_wait_until: None,
- save: Some(SaveOptions {
+ save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
include_text: Some(false),
- }),
+ })),
},
)),
- hover_provider: Some(true),
+ document_link_provider: Some(DocumentLinkOptions {
+ resolve_provider: Some(false),
+ work_done_progress_options: WorkDoneProgressOptions::default(),
+ }),
+ folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),
+ definition_provider: Some(OneOf::Left(true)),
+ references_provider: Some(OneOf::Left(true)),
+ hover_provider: Some(HoverProviderCapability::Simple(true)),
+ #[cfg(feature = "completion")]
completion_provider: Some(CompletionOptions {
resolve_provider: Some(true),
trigger_characters: Some(vec![
@@ -138,625 +120,728 @@ impl<C: LspClient + Send + Sync + 'static> LatexLspServer<C> {
]),
..CompletionOptions::default()
}),
- definition_provider: Some(true),
- references_provider: Some(true),
- document_highlight_provider: Some(true),
- document_symbol_provider: Some(true),
- workspace_symbol_provider: Some(true),
- document_formatting_provider: Some(true),
- rename_provider: Some(RenameProviderCapability::Options(RenameOptions {
+ document_symbol_provider: Some(OneOf::Left(true)),
+ workspace_symbol_provider: Some(OneOf::Left(true)),
+ rename_provider: Some(OneOf::Right(RenameOptions {
prepare_provider: Some(true),
work_done_progress_options: WorkDoneProgressOptions::default(),
})),
- document_link_provider: Some(DocumentLinkOptions {
- resolve_provider: Some(false),
- work_done_progress_options: WorkDoneProgressOptions::default(),
- }),
- folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),
+ document_highlight_provider: Some(OneOf::Left(true)),
+ document_formatting_provider: Some(OneOf::Left(true)),
+ #[cfg(feature = "semantic")]
+ semantic_tokens_provider: Some(
+ SemanticTokensServerCapabilities::SemanticTokensOptions(SemanticTokensOptions {
+ full: None,
+ range: Some(true),
+ legend: SemanticTokensLegend {
+ token_types: crate::features::legend::SUPPORTED_TYPES.to_vec(),
+ token_modifiers: crate::features::legend::SUPPORTED_MODIFIERS.to_vec(),
+ },
+ work_done_progress_options: WorkDoneProgressOptions::default(),
+ }),
+ ),
..ServerCapabilities::default()
- };
+ }
+ }
+
+ fn initialize(&mut self) -> Result<()> {
+ let (id, params) = self.connection.initialize_start()?;
+ let params: InitializeParams = serde_json::from_value(params)?;
- Lazy::force(&COMPONENT_DATABASE);
- Ok(InitializeResult {
- capabilities,
+ *self.context.client_capabilities.lock().unwrap() = params.capabilities;
+ *self.context.client_info.lock().unwrap() = params.client_info;
+
+ let result = InitializeResult {
+ capabilities: self.capabilities(),
server_info: Some(ServerInfo {
name: "TexLab".to_owned(),
version: Some(env!("CARGO_PKG_VERSION").to_owned()),
}),
- })
+ };
+ self.connection
+ .initialize_finish(id, serde_json::to_value(result)?)?;
+
+ let cx = Arc::clone(&self.context);
+ if self.load_resolver {
+ self.pool.execute(move || {
+ let distro = Distribution::detect();
+ info!("Detected distribution: {}", distro.kind);
+ *cx.resolver.lock().unwrap() = distro.resolver;
+ });
+ }
+
+ self.register_diagnostics_handler();
+
+ let req_queue = Arc::clone(&self.req_queue);
+ let sender = self.connection.sender.clone();
+ let context = Arc::clone(&self.context);
+ self.pool.execute(move || {
+ register_config_capability(&req_queue, &sender, &context.client_capabilities);
+ pull_config(
+ &req_queue,
+ &sender,
+ &context.options,
+ &context.client_capabilities.lock().unwrap(),
+ );
+ });
+ Ok(())
}
- #[jsonrpc_method("initialized", kind = "notification")]
- pub async fn initialized(&self, _params: InitializedParams) {
- self.action_manager.push(Action::PullConfiguration).await;
- self.action_manager.push(Action::RegisterCapabilities).await;
- self.action_manager.push(Action::LoadDistribution).await;
- self.action_manager.push(Action::PublishDiagnostics).await;
+ fn register_diagnostics_handler(&mut self) {
+ let sender = self.static_debouncer.sender.clone();
+ self.workspace
+ .register_open_handler(Arc::new(move |workspace, document| {
+ let message = DiagnosticsMessage::Analyze {
+ workspace,
+ document,
+ };
+ sender.send(message).unwrap();
+ }));
+ }
+
+ fn register_incoming_request(&self, id: RequestId) -> Arc<CancellationToken> {
+ let token_source = CancellationTokenSource::new();
+ let token = Arc::clone(token_source.token());
+ let mut req_queue = self.req_queue.lock().unwrap();
+ req_queue
+ .incoming
+ .register(id.clone(), IncomingData { token_source });
+ token
+ }
+
+ fn cancel(&self, params: CancelParams) -> Result<()> {
+ let id = match params.id {
+ NumberOrString::Number(id) => RequestId::from(id),
+ NumberOrString::String(id) => RequestId::from(id),
+ };
+
+ let mut req_queue = self.req_queue.lock().unwrap();
+ if let Some(data) = req_queue.incoming.complete(id.clone()) {
+ data.token_source.cancel();
+ }
+
+ Ok(())
}
- #[jsonrpc_method("shutdown", kind = "request")]
- pub async fn shutdown(&self, _params: ()) -> Result<()> {
+ fn did_change_configuration(&self, params: DidChangeConfigurationParams) -> Result<()> {
+ push_config(&self.context.options, params.settings);
Ok(())
}
- #[jsonrpc_method("exit", kind = "notification")]
- pub async fn exit(&self, _params: ()) {}
-
- #[jsonrpc_method("$/cancelRequest", kind = "notification")]
- pub async fn cancel_request(&self, _params: CancelParams) {}
-
- #[jsonrpc_method("textDocument/didOpen", kind = "notification")]
- pub async fn did_open(&self, params: DidOpenTextDocumentParams) {
- let uri = params.text_document.uri.clone();
- let options = self.config_manager().get().await;
- self.workspace.add(params.text_document, &options).await;
- self.action_manager
- .push(Action::DetectRoot(uri.clone().into()))
- .await;
- self.action_manager
- .push(Action::RunLinter(uri.into(), LintReason::Save))
- .await;
- self.action_manager.push(Action::PublishDiagnostics).await;
- }
-
- #[jsonrpc_method("textDocument/didChange", kind = "notification")]
- pub async fn did_change(&self, params: DidChangeTextDocumentParams) {
- let options = self.config_manager().get().await;
- for change in params.content_changes {
- let uri = params.text_document.uri.clone();
- self.workspace
- .update(uri.into(), change.text, &options)
- .await;
- }
- self.action_manager
- .push(Action::RunLinter(
- params.text_document.uri.clone().into(),
- LintReason::Change,
- ))
- .await;
- self.action_manager.push(Action::PublishDiagnostics).await;
- }
-
- #[jsonrpc_method("textDocument/didSave", kind = "notification")]
- pub async fn did_save(&self, params: DidSaveTextDocumentParams) {
- self.action_manager
- .push(Action::Build(params.text_document.uri.clone().into()))
- .await;
-
- self.action_manager
- .push(Action::RunLinter(
- params.text_document.uri.into(),
- LintReason::Save,
- ))
- .await;
- self.action_manager.push(Action::PublishDiagnostics).await;
- }
-
- #[jsonrpc_method("textDocument/didClose", kind = "notification")]
- pub async fn did_close(&self, _params: DidCloseTextDocumentParams) {}
-
- #[jsonrpc_method("workspace/didChangeConfiguration", kind = "notification")]
- pub async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
- let config_manager = self.config_manager();
- config_manager.push(params.settings).await;
- let options = config_manager.get().await;
- self.workspace.reparse(&options).await;
- }
-
- #[jsonrpc_method("window/workDoneProgress/cancel", kind = "notification")]
- pub async fn work_done_progress_cancel(&self, params: WorkDoneProgressCancelParams) {
- self.build_provider.cancel(params.token).await;
- }
-
- #[jsonrpc_method("textDocument/completion", kind = "request")]
- pub async fn completion(&self, params: CompletionParams) -> Result<CompletionList> {
- let req = self
- .make_feature_request(params.text_document_position.text_document.as_uri(), params)
- .await?;
-
- self.last_position_by_uri.insert(
- req.current().uri.clone(),
- req.params.text_document_position.position,
+ fn did_open(&self, params: DidOpenTextDocumentParams) -> Result<()> {
+ let language_id = &params.text_document.language_id;
+ let language = DocumentLanguage::by_language_id(language_id);
+ let document = self.workspace.open(
+ Arc::new(params.text_document.uri.into()),
+ params.text_document.text,
+ language.unwrap_or(DocumentLanguage::Latex),
+ WorkspaceSource::Client,
);
- let items = self.completion_provider.execute(&req).await;
- let is_incomplete = if self
- .client_info
- .get()
- .and_then(|info| info.as_ref())
- .map(|info| info.name.as_str())
- .unwrap_or_default()
- == "vscode"
+ let should_lint = { self.context.options.read().unwrap().chktex.on_open_and_save };
+ if let Some(document) = self
+ .workspace
+ .get(document.uri.as_ref())
+ .filter(|_| should_lint)
{
- true
- } else {
- items.len() >= COMPLETION_LIMIT
+ self.chktex_debouncer
+ .sender
+ .send(DiagnosticsMessage::Analyze {
+ workspace: Arc::clone(&self.workspace),
+ document,
+ })?;
};
+ Ok(())
+ }
- Ok(CompletionList {
- is_incomplete,
- items,
- })
+ fn did_change(&self, mut params: DidChangeTextDocumentParams) -> Result<()> {
+ let uri = params.text_document.uri.into();
+ assert_eq!(params.content_changes.len(), 1);
+ let text = params.content_changes.pop().unwrap().text;
+ let language = self
+ .workspace
+ .get(&uri)
+ .map(|document| document.data.language())
+ .unwrap_or(DocumentLanguage::Latex);
+
+ let document = self
+ .workspace
+ .open(Arc::new(uri), text, language, WorkspaceSource::Client);
+
+ let should_lint = { self.context.options.read().unwrap().chktex.on_edit };
+ if let Some(document) = self
+ .workspace
+ .get(document.uri.as_ref())
+ .filter(|_| should_lint)
+ {
+ self.chktex_debouncer
+ .sender
+ .send(DiagnosticsMessage::Analyze {
+ workspace: Arc::clone(&self.workspace),
+ document,
+ })?;
+ };
+
+ Ok(())
}
- #[jsonrpc_method("completionItem/resolve", kind = "request")]
- pub async fn completion_resolve(&self, mut item: CompletionItem) -> Result<CompletionItem> {
- let data: CompletionItemData = serde_json::from_value(item.data.clone().unwrap()).unwrap();
- match data {
- CompletionItemData::Package | CompletionItemData::Class => {
- item.documentation = COMPONENT_DATABASE
- .documentation(&item.label)
- .map(Documentation::MarkupContent);
- }
- #[cfg(feature = "citation")]
- CompletionItemData::Citation { uri, key } => {
- let snapshot = self.workspace.get().await;
- if let Some(doc) = snapshot.find(&uri) {
- if let DocumentContent::Bibtex(tree) = &doc.content {
- let markup = render_citation(&tree, &key);
- item.documentation = markup.map(Documentation::MarkupContent);
- }
- }
- }
- _ => {}
+ fn did_save(&self, params: DidSaveTextDocumentParams) -> Result<()> {
+ let uri = params.text_document.uri.into();
+ let should_lint = { self.context.options.read().unwrap().chktex.on_open_and_save };
+ if let Some(document) = self.workspace.get(&uri).filter(|_| should_lint) {
+ self.chktex_debouncer
+ .sender
+ .send(DiagnosticsMessage::Analyze {
+ workspace: Arc::clone(&self.workspace),
+ document,
+ })?;
};
- Ok(item)
+ Ok(())
}
- #[jsonrpc_method("textDocument/hover", kind = "request")]
- pub async fn hover(&self, params: TextDocumentPositionParams) -> Result<Option<Hover>> {
- let req = self
- .make_feature_request(params.text_document.as_uri(), params)
- .await?;
+ fn did_close(&self, params: DidCloseTextDocumentParams) -> Result<()> {
+ let uri = params.text_document.uri.into();
+ self.workspace.close(&uri);
+ Ok(())
+ }
- self.last_position_by_uri
- .insert(req.current().uri.clone(), req.params.position);
+ fn feature_request<P>(&self, uri: Arc<Uri>, params: P) -> Option<FeatureRequest<P>> {
+ let req_queue = Arc::clone(&self.req_queue);
+ let sender = self.connection.sender.clone();
+ let cx = Arc::clone(&self.context);
+ self.pool.execute(move || {
+ pull_config(
+ &req_queue,
+ &sender,
+ &cx.options,
+ &cx.client_capabilities.lock().unwrap(),
+ );
+ });
+
+ Some(FeatureRequest {
+ context: Arc::clone(&self.context),
+ params,
+ workspace: Arc::clone(&self.workspace),
+ subset: self.workspace.subset(uri)?,
+ })
+ }
- Ok(self.hover_provider.execute(&req).await)
+ fn send_feature_error(&self, id: RequestId) -> Result<()> {
+ let resp = lsp_server::Response::new_err(
+ id,
+ ErrorCode::InternalError as i32,
+ "unknown document URI".to_string(),
+ );
+ self.connection.sender.send(resp.into())?;
+ Ok(())
}
- #[jsonrpc_method("textDocument/definition", kind = "request")]
- pub async fn definition(
+ fn handle_feature_request<P, R, H>(
&self,
- params: TextDocumentPositionParams,
- ) -> Result<DefinitionResponse> {
- let req = self
- .make_feature_request(params.text_document.as_uri(), params)
- .await?;
- let results = self.definition_provider.execute(&req).await;
- let response = if req.client_capabilities.has_definition_link_support() {
- DefinitionResponse::LocationLinks(results)
- } else {
- DefinitionResponse::Locations(
- results
- .into_iter()
- .map(|link| Location::new(link.target_uri, link.target_selection_range))
- .collect(),
- )
+ id: RequestId,
+ params: P,
+ uri: Arc<Uri>,
+ token: &Arc<CancellationToken>,
+ handler: H,
+ ) -> Result<()>
+ where
+ P: Send + 'static,
+ R: Serialize,
+ H: FnOnce(FeatureRequest<P>, &CancellationToken) -> R + Send + 'static,
+ {
+ match self.feature_request(uri, params) {
+ Some(req) => {
+ let sender = self.connection.sender.clone();
+ let token = Arc::clone(token);
+ self.pool.execute(move || {
+ let result = handler(req, &token);
+ if token.is_canceled() {
+ sender.send(cancel_response(id).into()).unwrap();
+ } else {
+ sender
+ .send(lsp_server::Response::new_ok(id, result).into())
+ .unwrap();
+ }
+ });
+ }
+ None => {
+ self.send_feature_error(id)?;
+ }
};
-
- Ok(response)
+ Ok(())
}
- #[jsonrpc_method("textDocument/references", kind = "request")]
- pub async fn references(&self, params: ReferenceParams) -> Result<Vec<Location>> {
- let req = self
- .make_feature_request(params.text_document_position.as_uri(), params)
- .await?;
- Ok(self.reference_provider.execute(&req).await)
+ fn document_link(
+ &self,
+ id: RequestId,
+ params: DocumentLinkParams,
+ token: &Arc<CancellationToken>,
+ ) -> Result<()> {
+ let uri = Arc::new(params.text_document.uri.clone().into());
+ self.handle_feature_request(id, params, uri, token, find_document_links)?;
+ Ok(())
}
- #[jsonrpc_method("textDocument/documentHighlight", kind = "request")]
- pub async fn document_highlight(
+ fn document_symbols(
&self,
- params: TextDocumentPositionParams,
- ) -> Result<Vec<DocumentHighlight>> {
- let req = self
- .make_feature_request(params.text_document.as_uri(), params)
- .await?;
- Ok(self.highlight_provider.execute(&req).await)
+ id: RequestId,
+ params: DocumentSymbolParams,
+ token: &Arc<CancellationToken>,
+ ) -> Result<()> {
+ let uri = Arc::new(params.text_document.uri.clone().into());
+ self.handle_feature_request(id, params, uri, token, find_document_symbols)?;
+ Ok(())
}
- #[jsonrpc_method("workspace/symbol", kind = "request")]
- pub async fn workspace_symbol(
+ fn workspace_symbols(
&self,
+ id: RequestId,
params: WorkspaceSymbolParams,
- ) -> Result<Vec<SymbolInformation>> {
- let distro = self.distro.clone();
- let client_capabilities = self.client_capabilities();
- let snapshot = self.workspace.get().await;
- let options = self.config_manager().get().await;
- let symbols = workspace_symbols(
- distro,
- client_capabilities,
- snapshot,
- &options,
- Arc::clone(&self.current_dir),
- &params,
- )
- .await;
- Ok(symbols)
- }
-
- #[jsonrpc_method("textDocument/documentSymbol", kind = "request")]
- pub async fn document_symbol(
+ token: &Arc<CancellationToken>,
+ ) -> Result<()> {
+ let sender = self.connection.sender.clone();
+ let workspace = Arc::clone(&self.workspace);
+ let token = Arc::clone(token);
+ self.pool.execute(move || {
+ let result = find_workspace_symbols(workspace.as_ref(), &params, &token);
+ if token.is_canceled() {
+ sender.send(cancel_response(id).into()).unwrap();
+ } else {
+ sender
+ .send(lsp_server::Response::new_ok(id, result).into())
+ .unwrap();
+ }
+ });
+ Ok(())
+ }
+
+ #[cfg(feature = "completion")]
+ fn completion(
&self,
- params: DocumentSymbolParams,
- ) -> Result<DocumentSymbolResponse> {
- let req = self
- .make_feature_request(params.text_document.as_uri(), params)
- .await?;
-
- let symbols = self.symbol_provider.execute(&req).await;
- let response = document_symbols(
- &req.client_capabilities,
- req.snapshot(),
- &req.current().uri,
- &req.options,
- &req.current_dir,
- symbols.into_iter().map(Into::into).collect(),
+ id: RequestId,
+ params: CompletionParams,
+ token: &Arc<CancellationToken>,
+ ) -> Result<()> {
+ let uri = Arc::new(
+ params
+ .text_document_position
+ .text_document
+ .uri
+ .clone()
+ .into(),
);
- Ok(response)
- }
-
- #[jsonrpc_method("textDocument/documentLink", kind = "request")]
- pub async fn document_link(&self, params: DocumentLinkParams) -> Result<Vec<DocumentLink>> {
- let req = self
- .make_feature_request(params.text_document.as_uri(), params)
- .await?;
- Ok(self.link_provider.execute(&req).await)
- }
-
- #[jsonrpc_method("textDocument/formatting", kind = "request")]
- pub async fn formatting(&self, params: DocumentFormattingParams) -> Result<Vec<TextEdit>> {
- let req = self
- .make_feature_request(params.text_document.as_uri(), params)
- .await?;
- let mut edits = Vec::new();
- match &req.current().content {
- DocumentContent::Latex(_) => {
- Self::run_latexindent(&req.current().text, "tex", &mut edits).await;
- }
- DocumentContent::Bibtex(tree) => {
- let options = req
- .options
- .bibtex
- .clone()
- .and_then(|opts| opts.formatting)
- .unwrap_or_default();
-
- match options.formatter.unwrap_or_default() {
- BibtexFormatter::Texlab => {
- let params = bibtex::FormattingParams {
- tab_size: req.params.options.tab_size as usize,
- insert_spaces: req.params.options.insert_spaces,
- options: &options,
- };
-
- for node in tree.children(tree.root) {
- let should_format = match &tree.graph[node] {
- bibtex::Node::Preamble(_) | bibtex::Node::String(_) => true,
- bibtex::Node::Entry(entry) => !entry.is_comment(),
- _ => false,
- };
- if should_format {
- let text = bibtex::format(&tree, node, params);
- edits.push(TextEdit::new(tree.graph[node].range(), text));
- }
+ self.handle_feature_request(id, params, uri, token, crate::features::complete)?;
+ Ok(())
+ }
+
+ #[cfg(feature = "completion")]
+ fn completion_resolve(
+ &self,
+ id: RequestId,
+ mut item: CompletionItem,
+ token: &Arc<CancellationToken>,
+ ) -> Result<()> {
+ let sender = self.connection.sender.clone();
+ let token = Arc::clone(token);
+ let workspace = Arc::clone(&self.workspace);
+ self.pool.execute(move || {
+ match serde_json::from_value(item.data.clone().unwrap()).unwrap() {
+ crate::features::CompletionItemData::Package
+ | crate::features::CompletionItemData::Class => {
+ item.documentation = COMPONENT_DATABASE
+ .documentation(&item.label)
+ .map(Documentation::MarkupContent);
+ }
+ #[cfg(feature = "citeproc")]
+ crate::features::CompletionItemData::Citation { uri, key } => {
+ if let Some(document) = workspace.get(&uri) {
+ if let Some(data) = document.data.as_bibtex() {
+ let markup = crate::citation::render_citation(&data.root, &key);
+ item.documentation = markup.map(Documentation::MarkupContent);
}
}
- BibtexFormatter::Latexindent => {
- Self::run_latexindent(&req.current().text, "bib", &mut edits).await;
- }
}
+ _ => {}
+ };
+
+ drop(workspace);
+ if token.is_canceled() {
+ sender.send(cancel_response(id).into()).unwrap();
+ } else {
+ sender
+ .send(lsp_server::Response::new_ok(id, item).into())
+ .unwrap();
}
- }
- Ok(edits)
+ });
+ Ok(())
}
- async fn run_latexindent(old_text: &str, extension: &str, edits: &mut Vec<TextEdit>) {
- match latexindent::format(old_text, extension).await {
- Ok(new_text) => {
- let mut stream = CharStream::new(&old_text);
- while stream.next().is_some() {}
- let range = Range::new(Position::new(0, 0), stream.current_position);
- edits.push(TextEdit::new(range, new_text));
- }
- Err(why) => {
- debug!("Failed to run latexindent.pl: {}", why);
- }
- }
+ fn folding_range(
+ &self,
+ id: RequestId,
+ params: FoldingRangeParams,
+ token: &Arc<CancellationToken>,
+ ) -> Result<()> {
+ let uri = Arc::new(params.text_document.uri.clone().into());
+ self.handle_feature_request(id, params, uri, token, find_foldings)?;
+ Ok(())
}
- #[jsonrpc_method("textDocument/prepareRename", kind = "request")]
- pub async fn prepare_rename(
+ fn references(
&self,
- params: TextDocumentPositionParams,
- ) -> Result<Option<Range>> {
- let req = self
- .make_feature_request(params.text_document.as_uri(), params)
- .await?;
- Ok(self.prepare_rename_provider.execute(&req).await)
- }
-
- #[jsonrpc_method("textDocument/rename", kind = "request")]
- pub async fn rename(&self, params: RenameParams) -> Result<Option<WorkspaceEdit>> {
- let req = self
- .make_feature_request(params.text_document_position.text_document.as_uri(), params)
- .await?;
- Ok(self.rename_provider.execute(&req).await)
- }
-
- #[jsonrpc_method("textDocument/foldingRange", kind = "request")]
- pub async fn folding_range(&self, params: FoldingRangeParams) -> Result<Vec<FoldingRange>> {
- let req = self
- .make_feature_request(params.text_document.as_uri(), params)
- .await?;
- Ok(self.folding_provider.execute(&req).await)
- }
-
- #[jsonrpc_method("textDocument/build", kind = "request")]
- pub async fn build(&self, params: BuildParams) -> Result<BuildResult> {
- let req = self
- .make_feature_request(params.text_document.as_uri(), params)
- .await?;
-
- let pos = self
- .last_position_by_uri
- .get(&req.current().uri)
- .map(|pos| *pos)
- .unwrap_or_default();
-
- let res = self.build_provider.execute(&req).await;
-
- if req
- .options
- .latex
- .and_then(|opts| opts.build)
- .unwrap_or_default()
- .forward_search_after()
- && !self.build_provider.is_building()
- {
- let params = TextDocumentPositionParams::new(req.params.text_document, pos);
- self.forward_search(params).await?;
- }
+ id: RequestId,
+ params: ReferenceParams,
+ token: &Arc<CancellationToken>,
+ ) -> Result<()> {
+ let uri = Arc::new(
+ params
+ .text_document_position
+ .text_document
+ .uri
+ .clone()
+ .into(),
+ );
+ self.handle_feature_request(id, params, uri, token, find_all_references)?;
+ Ok(())
+ }
+
+ fn hover(
+ &self,
+ id: RequestId,
+ params: HoverParams,
+ token: &Arc<CancellationToken>,
+ ) -> Result<()> {
+ let uri = Arc::new(
+ params
+ .text_document_position_params
+ .text_document
+ .uri
+ .clone()
+ .into(),
+ );
+ self.handle_feature_request(id, params, uri, token, find_hover)?;
+ Ok(())
+ }
- Ok(res)
+ fn goto_definition(
+ &self,
+ id: RequestId,
+ params: GotoDefinitionParams,
+ token: &Arc<CancellationToken>,
+ ) -> Result<()> {
+ let uri = Arc::new(
+ params
+ .text_document_position_params
+ .text_document
+ .uri
+ .clone()
+ .into(),
+ );
+ self.handle_feature_request(id, params, uri, token, goto_definition)?;
+ Ok(())
}
- #[jsonrpc_method("textDocument/forwardSearch", kind = "request")]
- pub async fn forward_search(
+ fn prepare_rename(
&self,
+ id: RequestId,
params: TextDocumentPositionParams,
- ) -> Result<ForwardSearchResult> {
- let req = self
- .make_feature_request(params.text_document.as_uri(), params)
- .await?;
-
- forward_search::search(
- &req.view.snapshot,
- &req.current().uri,
- req.params.position.line,
- &req.options,
- &self.current_dir,
- )
- .await
- .ok_or_else(|| "Unable to execute forward search".into())
- }
-
- #[jsonrpc_method("$/detectRoot", kind = "request")]
- pub async fn detect_root(&self, params: TextDocumentIdentifier) -> Result<()> {
- let options = self.config_manager().get().await;
- let _ = self.workspace.detect_root(&params.as_uri(), &options).await;
+ token: &Arc<CancellationToken>,
+ ) -> Result<()> {
+ let uri = Arc::new(params.text_document.uri.clone().into());
+ self.handle_feature_request(id, params, uri, token, prepare_rename_all)?;
Ok(())
}
- async fn make_feature_request<P>(&self, uri: Uri, params: P) -> Result<FeatureRequest<P>> {
- let options = self.pull_configuration().await;
- let snapshot = self.workspace.get().await;
- let client_capabilities = self.client_capabilities();
- match snapshot.find(&uri) {
- Some(current) => Ok(FeatureRequest {
- params,
- view: DocumentView::analyze(snapshot, current, &options, &self.current_dir),
- distro: self.distro.clone(),
- client_capabilities,
- options,
- current_dir: Arc::clone(&self.current_dir),
- }),
- None => {
- let msg = format!("Unknown document: {}", uri);
- Err(msg)
- }
- }
+ fn rename(
+ &self,
+ id: RequestId,
+ params: RenameParams,
+ token: &Arc<CancellationToken>,
+ ) -> Result<()> {
+ let uri = Arc::new(
+ params
+ .text_document_position
+ .text_document
+ .uri
+ .clone()
+ .into(),
+ );
+ self.handle_feature_request(id, params, uri, token, rename_all)?;
+ Ok(())
}
- async fn pull_configuration(&self) -> Options {
- let config_manager = self.config_manager();
- let has_changed = config_manager.pull().await;
- let options = config_manager.get().await;
- if has_changed {
- self.workspace.reparse(&options).await;
- }
- options
+ fn document_highlight(
+ &self,
+ id: RequestId,
+ params: DocumentHighlightParams,
+ token: &Arc<CancellationToken>,
+ ) -> Result<()> {
+ let uri = Arc::new(
+ params
+ .text_document_position_params
+ .text_document
+ .uri
+ .clone()
+ .into(),
+ );
+ self.handle_feature_request(id, params, uri, token, find_document_highlights)?;
+ Ok(())
}
- async fn update_build_diagnostics(&self) {
- let snapshot = self.workspace.get().await;
- let options = self.config_manager().get().await;
+ fn formatting(
+ &self,
+ id: RequestId,
+ params: DocumentFormattingParams,
+ token: &Arc<CancellationToken>,
+ ) -> Result<()> {
+ let uri = Arc::new(params.text_document.uri.clone().into());
+ self.handle_feature_request(id, params, uri, token, format_source_code)?;
+ Ok(())
+ }
- for doc in snapshot.0.iter().filter(|doc| doc.uri.scheme() == "file") {
- if let DocumentContent::Latex(table) = &doc.content {
- if table.is_standalone {
- match self
- .diagnostics_manager
- .build
- .update(&snapshot, &doc.uri, &options, &self.current_dir)
- .await
- {
- Ok(true) => self.action_manager.push(Action::PublishDiagnostics).await,
- Ok(false) => (),
- Err(why) => {
- warn!("Unable to read log file ({}): {}", why, doc.uri.as_str())
- }
- }
- }
- }
- }
+ #[cfg(feature = "semantic")]
+ fn semantic_tokens_range(
+ &self,
+ id: RequestId,
+ params: SemanticTokensRangeParams,
+ token: &Arc<CancellationToken>,
+ ) -> Result<()> {
+ let uri = Arc::new(params.text_document.uri.clone().into());
+ self.handle_feature_request(
+ id,
+ params,
+ uri,
+ token,
+ crate::features::find_semantic_tokens_range,
+ )?;
+ Ok(())
}
- async fn load_distribution(&self) {
- info!("Detected TeX distribution: {}", self.distro.kind());
- if self.distro.kind() == DistributionKind::Unknown {
- let params = ShowMessageParams {
- message: "Your TeX distribution could not be detected. \
- Please make sure that your distribution is in your PATH."
- .into(),
- typ: MessageType::Error,
- };
- self.client.show_message(params).await;
- }
+ #[cfg(not(feature = "semantic"))]
+ fn semantic_tokens_range(
+ &self,
+ _id: RequestId,
+ _params: SemanticTokensRangeParams,
+ _token: &Arc<CancellationToken>,
+ ) -> Result<()> {
+ Ok(())
+ }
- if let Err(why) = self.distro.load().await {
- let message = match why {
- KpsewhichError::NotInstalled | KpsewhichError::InvalidOutput => {
- "An error occurred while executing `kpsewhich`.\
- Please make sure that your distribution is in your PATH \
- environment variable and provides the `kpsewhich` tool."
- }
- KpsewhichError::CorruptDatabase | KpsewhichError::NoDatabase => {
- "The file database of your TeX distribution seems \
- to be corrupt. Please rebuild it and try again."
- }
- KpsewhichError::Decode(_) => {
- "An error occurred while decoding the output of `kpsewhich`."
- }
- KpsewhichError::IO(why) => {
- error!("An I/O error occurred while executing 'kpsewhich': {}", why);
- "An I/O error occurred while executing 'kpsewhich'"
+ fn build(
+ &self,
+ id: RequestId,
+ params: BuildParams,
+ token: &Arc<CancellationToken>,
+ ) -> Result<()> {
+ let uri = Arc::new(params.text_document.uri.clone().into());
+ let lsp_sender = self.connection.sender.clone();
+ self.handle_feature_request(id, params, uri, token, |request, token| {
+ let (log_sender, log_receiver) = crossbeam_channel::unbounded();
+
+ thread::spawn(move || {
+ for message in &log_receiver {
+ send_notification::<LogMessage>(
+ &lsp_sender,
+ LogMessageParams {
+ message,
+ typ: MessageType::Log,
+ },
+ )
+ .unwrap();
}
- };
- let params = ShowMessageParams {
- message: message.into(),
- typ: MessageType::Error,
- };
- self.client.show_message(params).await;
- };
+ });
+
+ build_document(request, token, log_sender)
+ })?;
+ Ok(())
}
-}
-#[async_trait]
-impl<C: LspClient + Send + Sync + 'static> Middleware for LatexLspServer<C> {
- async fn before_message(&self) {
- if let Some(config_manager) = self.config_manager.get() {
- let options = config_manager.get().await;
- self.workspace.detect_children(&options).await;
- self.workspace.reparse_all_if_newer(&options).await;
- }
+ fn forward_search(
+ &self,
+ id: RequestId,
+ params: TextDocumentPositionParams,
+ token: &Arc<CancellationToken>,
+ ) -> Result<()> {
+ let uri = Arc::new(params.text_document.uri.clone().into());
+ self.handle_feature_request(
+ id,
+ params,
+ uri,
+ token,
+ crate::features::execute_forward_search,
+ )?;
+ Ok(())
}
- async fn after_message(&self) {
- self.update_build_diagnostics().await;
- for action in self.action_manager.take().await {
- match action {
- Action::LoadDistribution => {
- self.load_distribution().await;
- }
- Action::RegisterCapabilities => {
- let config_manager = self.config_manager();
- config_manager.register().await;
- }
- Action::PullConfiguration => {
- self.pull_configuration().await;
- }
- Action::DetectRoot(uri) => {
- let options = self.config_manager().get().await;
- let _ = self.workspace.detect_root(&uri, &options).await;
- }
- Action::PublishDiagnostics => {
- let snapshot = self.workspace.get().await;
- for doc in &snapshot.0 {
- let diagnostics = self.diagnostics_manager.get(doc).await;
- let params = PublishDiagnosticsParams {
- uri: doc.uri.clone().into(),
- diagnostics,
- version: None,
- };
- self.client.publish_diagnostics(params).await;
+ fn process_messages(&self) -> Result<()> {
+ for msg in &self.connection.receiver {
+ match msg {
+ Message::Request(request) => {
+ if self.connection.handle_shutdown(&request)? {
+ return Ok(());
}
- }
- Action::Build(uri) => {
- let options = self
- .config_manager()
- .get()
- .await
- .latex
- .and_then(|opts| opts.build)
- .unwrap_or_default();
-
- if options.on_save() {
- let text_document = TextDocumentIdentifier::new(uri.into());
- self.build(BuildParams { text_document }).await.unwrap();
+
+ let token = self.register_incoming_request(request.id.clone());
+ if let Some(response) = RequestDispatcher::new(request)
+ .on::<DocumentLinkRequest, _>(|id, params| {
+ self.document_link(id, params, &token)
+ })?
+ .on::<FoldingRangeRequest, _>(|id, params| {
+ self.folding_range(id, params, &token)
+ })?
+ .on::<References, _>(|id, params| self.references(id, params, &token))?
+ .on::<HoverRequest, _>(|id, params| self.hover(id, params, &token))?
+ .on::<DocumentSymbolRequest, _>(|id, params| {
+ self.document_symbols(id, params, &token)
+ })?
+ .on::<WorkspaceSymbol, _>(|id, params| {
+ self.workspace_symbols(id, params, &token)
+ })?
+ .on::<Completion, _>(|id, params| {
+ #[cfg(feature = "completion")]
+ self.completion(id, params, &token)?;
+ Ok(())
+ })?
+ .on::<ResolveCompletionItem, _>(|id, params| {
+ #[cfg(feature = "completion")]
+ self.completion_resolve(id, params, &token)?;
+ Ok(())
+ })?
+ .on::<GotoDefinition, _>(|id, params| {
+ self.goto_definition(id, params, &token)
+ })?
+ .on::<PrepareRenameRequest, _>(|id, params| {
+ self.prepare_rename(id, params, &token)
+ })?
+ .on::<Rename, _>(|id, params| self.rename(id, params, &token))?
+ .on::<DocumentHighlightRequest, _>(|id, params| {
+ self.document_highlight(id, params, &token)
+ })?
+ .on::<Formatting, _>(|id, params| self.formatting(id, params, &token))?
+ .on::<BuildRequest, _>(|id, params| self.build(id, params, &token))?
+ .on::<ForwardSearchRequest, _>(|id, params| {
+ self.forward_search(id, params, &token)
+ })?
+ .on::<SemanticTokensRangeRequest, _>(|id, params| {
+ self.semantic_tokens_range(id, params, &token)
+ })?
+ .default()
+ {
+ self.connection.sender.send(response.into())?;
}
}
- Action::RunLinter(uri, reason) => {
- let options = self
- .config_manager()
- .get()
- .await
- .latex
- .and_then(|opts| opts.lint)
- .unwrap_or_default();
-
- let should_lint = match reason {
- LintReason::Change => options.on_change(),
- LintReason::Save => options.on_save() || options.on_change(),
+ Message::Notification(notification) => {
+ NotificationDispatcher::new(notification)
+ .on::<Cancel, _>(|params| self.cancel(params))?
+ .on::<DidChangeConfiguration, _>(|params| {
+ self.did_change_configuration(params)
+ })?
+ .on::<DidOpenTextDocument, _>(|params| self.did_open(params))?
+ .on::<DidChangeTextDocument, _>(|params| self.did_change(params))?
+ .on::<DidSaveTextDocument, _>(|params| self.did_save(params))?
+ .on::<DidCloseTextDocument, _>(|params| self.did_close(params))?
+ .default();
+ }
+ Message::Response(response) => {
+ let mut req_queue = self.req_queue.lock().unwrap();
+ let data = req_queue.outgoing.complete(response.id);
+ let result = match response.result {
+ Some(result) => Ok(result),
+ None => Err(response
+ .error
+ .expect("response without result or error received")),
};
-
- if should_lint {
- let snapshot = self.workspace.get().await;
- if let Some(doc) = snapshot.find(&uri) {
- if let DocumentContent::Latex(_) = &doc.content {
- self.diagnostics_manager.latex.update(&uri, &doc.text).await;
- }
- }
- }
+ data.sender.send(result)?;
}
}
}
+ Ok(())
+ }
+
+ pub fn run(mut self) -> Result<()> {
+ self.initialize()?;
+ self.process_messages()?;
+ drop(self.static_debouncer);
+ drop(self.chktex_debouncer);
+ self.pool.join();
+ Ok(())
}
}
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-enum LintReason {
- Change,
- Save,
+fn create_static_debouncer(
+ manager: Arc<Mutex<DiagnosticsManager>>,
+ conn: &Connection,
+ context: Arc<ServerContext>,
+) -> DiagnosticsDebouncer {
+ let sender = conn.sender.clone();
+ DiagnosticsDebouncer::launch(context, move |workspace, document| {
+ let mut manager = manager.lock().unwrap();
+ manager.update_static(workspace.as_ref(), Arc::clone(&document.uri));
+ if let Err(why) = publish_diagnostics(&sender, workspace.as_ref(), &manager) {
+ warn!("Failed to publish diagnostics: {}", why);
+ }
+ })
}
-#[derive(Debug, PartialEq, Clone)]
-enum Action {
- LoadDistribution,
- RegisterCapabilities,
- PullConfiguration,
- DetectRoot(Uri),
- PublishDiagnostics,
- Build(Uri),
- RunLinter(Uri, LintReason),
+fn create_chktex_debouncer(
+ manager: Arc<Mutex<DiagnosticsManager>>,
+ conn: &Connection,
+ context: Arc<ServerContext>,
+) -> DiagnosticsDebouncer {
+ let sender = conn.sender.clone();
+ DiagnosticsDebouncer::launch(Arc::clone(&context), move |workspace, document| {
+ let options = { context.options.read().unwrap().clone() };
+ let mut manager = manager.lock().unwrap();
+ manager.update_chktex(workspace.as_ref(), Arc::clone(&document.uri), &options);
+ if let Err(why) = publish_diagnostics(&sender, workspace.as_ref(), &manager) {
+ warn!("Failed to publish diagnostics: {}", why);
+ }
+ })
}
-#[derive(Debug, Default)]
-struct ActionManager {
- actions: Mutex<Vec<Action>>,
+fn publish_diagnostics(
+ sender: &Sender<lsp_server::Message>,
+ workspace: &dyn Workspace,
+ diag_manager: &DiagnosticsManager,
+) -> Result<()> {
+ for document in workspace.documents() {
+ let diagnostics = diag_manager.publish(Arc::clone(&document.uri));
+ send_notification::<PublishDiagnostics>(
+ sender,
+ PublishDiagnosticsParams {
+ uri: document.uri.as_ref().clone().into(),
+ version: None,
+ diagnostics,
+ },
+ )?;
+ }
+ Ok(())
}
-impl ActionManager {
- pub async fn push(&self, action: Action) {
- let mut actions = self.actions.lock().await;
- actions.push(action);
- }
+fn cancel_response(id: RequestId) -> lsp_server::Response {
+ lsp_server::Response::new_err(
+ id,
+ ErrorCode::RequestCanceled as i32,
+ "canceled by client".to_string(),
+ )
+}
- pub async fn take(&self) -> Vec<Action> {
- let mut actions = self.actions.lock().await;
- mem::replace(&mut *actions, Vec::new())
- }
+struct BuildRequest;
+
+impl lsp_types::request::Request for BuildRequest {
+ type Params = BuildParams;
+
+ type Result = BuildResult;
+
+ const METHOD: &'static str = "textDocument/build";
+}
+
+struct ForwardSearchRequest;
+
+impl lsp_types::request::Request for ForwardSearchRequest {
+ type Params = TextDocumentPositionParams;
+
+ type Result = ForwardSearchResult;
+
+ const METHOD: &'static str = "textDocument/forwardSearch";
}
diff --git a/support/texlab/src/symbol/bibtex_entry.rs b/support/texlab/src/symbol/bibtex_entry.rs
deleted file mode 100644
index f719937e80..0000000000
--- a/support/texlab/src/symbol/bibtex_entry.rs
+++ /dev/null
@@ -1,153 +0,0 @@
-use super::types::{LatexSymbol, LatexSymbolKind};
-use crate::{
- feature::{FeatureProvider, FeatureRequest},
- protocol::DocumentSymbolParams,
- syntax::{bibtex, BibtexEntryTypeCategory, SyntaxNode, LANGUAGE_DATA},
- workspace::DocumentContent,
-};
-use async_trait::async_trait;
-use petgraph::graph::NodeIndex;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct BibtexEntrySymbolProvider;
-
-#[async_trait]
-impl FeatureProvider for BibtexEntrySymbolProvider {
- type Params = DocumentSymbolParams;
- type Output = Vec<LatexSymbol>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let mut symbols = Vec::new();
- if let DocumentContent::Bibtex(tree) = &req.current().content {
- for entry_node in tree.children(tree.root) {
- if let Some(entry) = tree
- .as_entry(entry_node)
- .filter(|entry| !entry.is_comment())
- .filter(|entry| entry.key.is_some())
- {
- let category = LANGUAGE_DATA
- .find_entry_type(&entry.ty.text()[1..])
- .map(|ty| ty.category)
- .unwrap_or(BibtexEntryTypeCategory::Misc);
-
- let key = entry.key.as_ref().unwrap();
- let symbol = LatexSymbol {
- name: key.text().to_owned(),
- label: None,
- kind: LatexSymbolKind::Entry(category),
- deprecated: false,
- full_range: entry.range(),
- selection_range: key.range(),
- children: Self::field_symbols(tree, entry_node),
- };
- symbols.push(symbol);
- }
- }
- }
- symbols
- }
-}
-
-impl BibtexEntrySymbolProvider {
- fn field_symbols(tree: &bibtex::Tree, entry_node: NodeIndex) -> Vec<LatexSymbol> {
- let mut symbols = Vec::new();
- for field in tree
- .children(entry_node)
- .filter_map(|node| tree.as_field(node))
- {
- let symbol = LatexSymbol {
- name: field.name.text().to_owned(),
- label: None,
- kind: LatexSymbolKind::Field,
- deprecated: false,
- full_range: field.range(),
- selection_range: field.name.range(),
- children: Vec::new(),
- };
- symbols.push(symbol);
- }
- symbols
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- feature::FeatureTester,
- protocol::{Range, RangeExt},
- };
-
- #[tokio::test]
- async fn empty_latex_document() {
- let actual_symbols = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .test_symbol(BibtexEntrySymbolProvider)
- .await;
-
- assert!(actual_symbols.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let actual_symbols = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .test_symbol(BibtexEntrySymbolProvider)
- .await;
-
- assert!(actual_symbols.is_empty());
- }
-
- #[tokio::test]
- async fn entry() {
- let actual_symbols = FeatureTester::new()
- .file("main.bib", r#"@article{key, foo = bar, baz = qux}"#)
- .main("main.bib")
- .test_symbol(BibtexEntrySymbolProvider)
- .await;
-
- let expected_symbols = vec![LatexSymbol {
- name: "key".into(),
- label: None,
- kind: LatexSymbolKind::Entry(BibtexEntryTypeCategory::Article),
- deprecated: false,
- full_range: Range::new_simple(0, 0, 0, 35),
- selection_range: Range::new_simple(0, 9, 0, 12),
- children: vec![
- LatexSymbol {
- name: "foo".into(),
- label: None,
- kind: LatexSymbolKind::Field,
- deprecated: false,
- full_range: Range::new_simple(0, 14, 0, 24),
- selection_range: Range::new_simple(0, 14, 0, 17),
- children: Vec::new(),
- },
- LatexSymbol {
- name: "baz".into(),
- label: None,
- kind: LatexSymbolKind::Field,
- deprecated: false,
- full_range: Range::new_simple(0, 25, 0, 34),
- selection_range: Range::new_simple(0, 25, 0, 28),
- children: Vec::new(),
- },
- ],
- }];
-
- assert_eq!(actual_symbols, expected_symbols);
- }
-
- #[tokio::test]
- async fn comment() {
- let actual_symbols = FeatureTester::new()
- .file("main.bib", r#"@comment{key, foo = bar, baz = qux}"#)
- .main("main.bib")
- .test_symbol(BibtexEntrySymbolProvider)
- .await;
-
- assert!(actual_symbols.is_empty());
- }
-}
diff --git a/support/texlab/src/symbol/bibtex_string.rs b/support/texlab/src/symbol/bibtex_string.rs
deleted file mode 100644
index d2283f640b..0000000000
--- a/support/texlab/src/symbol/bibtex_string.rs
+++ /dev/null
@@ -1,102 +0,0 @@
-use super::types::{LatexSymbol, LatexSymbolKind};
-use crate::{
- feature::{FeatureProvider, FeatureRequest},
- protocol::DocumentSymbolParams,
- syntax::SyntaxNode,
- workspace::DocumentContent,
-};
-use async_trait::async_trait;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct BibtexStringSymbolProvider;
-
-#[async_trait]
-impl FeatureProvider for BibtexStringSymbolProvider {
- type Params = DocumentSymbolParams;
- type Output = Vec<LatexSymbol>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let mut symbols = Vec::new();
- if let DocumentContent::Bibtex(tree) = &req.current().content {
- for string_node in tree.children(tree.root) {
- if let Some(string) = &tree.as_string(string_node) {
- if let Some(name) = &string.name {
- symbols.push(LatexSymbol {
- name: name.text().into(),
- label: None,
- kind: LatexSymbolKind::String,
- deprecated: false,
- full_range: string.range(),
- selection_range: name.range(),
- children: Vec::new(),
- });
- }
- }
- }
- }
- symbols
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- feature::FeatureTester,
- protocol::{Range, RangeExt},
- };
-
- #[tokio::test]
- async fn empty_latex_document() {
- let actual_symbols = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .test_symbol(BibtexStringSymbolProvider)
- .await;
-
- assert!(actual_symbols.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let actual_symbols = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .test_symbol(BibtexStringSymbolProvider)
- .await;
-
- assert!(actual_symbols.is_empty());
- }
-
- #[tokio::test]
- async fn valid() {
- let actual_symbols = FeatureTester::new()
- .file("main.bib", r#"@string{key = "value"}"#)
- .main("main.bib")
- .test_symbol(BibtexStringSymbolProvider)
- .await;
-
- let expected_symbols = vec![LatexSymbol {
- name: "key".into(),
- label: None,
- kind: LatexSymbolKind::String,
- deprecated: false,
- full_range: Range::new_simple(0, 0, 0, 22),
- selection_range: Range::new_simple(0, 8, 0, 11),
- children: Vec::new(),
- }];
-
- assert_eq!(actual_symbols, expected_symbols);
- }
-
- #[tokio::test]
- async fn invalid() {
- let actual_symbols = FeatureTester::new()
- .file("main.bib", r#"@string{}"#)
- .main("main.bib")
- .test_symbol(BibtexStringSymbolProvider)
- .await;
-
- assert!(actual_symbols.is_empty());
- }
-}
diff --git a/support/texlab/src/symbol/latex_section/enumeration.rs b/support/texlab/src/symbol/latex_section/enumeration.rs
deleted file mode 100644
index e96a856eac..0000000000
--- a/support/texlab/src/symbol/latex_section/enumeration.rs
+++ /dev/null
@@ -1,79 +0,0 @@
-use super::{label_name, selection_range};
-use crate::{
- feature::DocumentView,
- outline::OutlineContext,
- protocol::{Range, RangeExt},
- symbol::types::{LatexSymbol, LatexSymbolKind},
- syntax::{latex, SyntaxNode},
-};
-use titlecase::titlecase;
-
-pub fn symbols(view: &DocumentView, table: &latex::SymbolTable) -> Vec<LatexSymbol> {
- table
- .environments
- .iter()
- .filter(|env| env.left.is_enum(&table))
- .map(|enumeration| make_symbol(view, table, *enumeration))
- .collect()
-}
-
-fn make_symbol(
- view: &DocumentView,
- table: &latex::SymbolTable,
- enumeration: latex::Environment,
-) -> LatexSymbol {
- let name = titlecase(enumeration.left.name(&table).unwrap().text());
-
- let items: Vec<_> = table
- .items
- .iter()
- .filter(|item| table.is_enum_item(enumeration, **item))
- .collect();
-
- let mut children = Vec::new();
- for i in 0..items.len() {
- let start = table[items[i].parent].start();
- let end = items
- .get(i + 1)
- .map(|item| table[item.parent].start())
- .unwrap_or_else(|| table[enumeration.right.parent].start());
- let range = Range::new(start, end);
-
- let label = find_item_label(table, range);
-
- let number = items[i]
- .name(&table)
- .or_else(|| label.and_then(|label| OutlineContext::find_number(view, table, *label)));
-
- let name = number.unwrap_or_else(|| "Item".into());
- children.push(LatexSymbol {
- name,
- label: label_name(table, label),
- kind: LatexSymbolKind::EnumerationItem,
- deprecated: false,
- full_range: range,
- selection_range: selection_range(table, table[items[i].parent].range(), label),
- children: Vec::new(),
- });
- }
-
- LatexSymbol {
- name,
- label: None,
- kind: LatexSymbolKind::Enumeration,
- deprecated: false,
- full_range: enumeration.range(&table),
- selection_range: enumeration.range(&table),
- children,
- }
-}
-
-fn find_item_label(table: &latex::SymbolTable, item_range: Range) -> Option<&latex::Label> {
- table.find_label_by_range(item_range).filter(|label| {
- table
- .environments
- .iter()
- .filter(|env| item_range.contains(env.range(&table).start))
- .all(|env| !env.range(&table).contains(table[label.parent].start()))
- })
-}
diff --git a/support/texlab/src/symbol/latex_section/equation.rs b/support/texlab/src/symbol/latex_section/equation.rs
deleted file mode 100644
index 2889aefd4d..0000000000
--- a/support/texlab/src/symbol/latex_section/equation.rs
+++ /dev/null
@@ -1,41 +0,0 @@
-use super::{label_name, selection_range};
-use crate::{
- feature::DocumentView,
- outline::OutlineContext,
- protocol::Range,
- symbol::types::{LatexSymbol, LatexSymbolKind},
- syntax::latex,
-};
-
-pub fn symbols(view: &DocumentView, table: &latex::SymbolTable) -> Vec<LatexSymbol> {
- let mut symbols = Vec::new();
- for equation in &table.equations {
- symbols.push(make_symbol(view, table, equation.range(&table)));
- }
-
- for equation in &table.environments {
- if equation.left.is_math(&table) {
- symbols.push(make_symbol(view, table, equation.range(&table)));
- }
- }
- symbols
-}
-
-fn make_symbol(view: &DocumentView, table: &latex::SymbolTable, full_range: Range) -> LatexSymbol {
- let label = table.find_label_by_range(full_range);
-
- let name = match label.and_then(|label| OutlineContext::find_number(view, table, *label)) {
- Some(num) => format!("Equation ({})", num),
- None => "Equation".to_owned(),
- };
-
- LatexSymbol {
- name,
- label: label_name(table, label),
- kind: LatexSymbolKind::Equation,
- deprecated: false,
- full_range,
- selection_range: selection_range(table, full_range, label),
- children: Vec::new(),
- }
-}
diff --git a/support/texlab/src/symbol/latex_section/float.rs b/support/texlab/src/symbol/latex_section/float.rs
deleted file mode 100644
index 7ade3602cf..0000000000
--- a/support/texlab/src/symbol/latex_section/float.rs
+++ /dev/null
@@ -1,59 +0,0 @@
-use super::{label_name, selection_range};
-use crate::{
- feature::DocumentView,
- outline::{OutlineCaptionKind, OutlineContext},
- symbol::types::{LatexSymbol, LatexSymbolKind},
- syntax::{latex, SyntaxNode},
-};
-
-pub fn symbols(view: &DocumentView, table: &latex::SymbolTable) -> Vec<LatexSymbol> {
- table
- .captions
- .iter()
- .filter_map(|caption| make_symbol(view, table, *caption))
- .collect()
-}
-
-fn make_symbol(
- view: &DocumentView,
- table: &latex::SymbolTable,
- caption: latex::Caption,
-) -> Option<LatexSymbol> {
- let env = table
- .environments
- .iter()
- .find(|env| table.is_direct_child(**env, table[caption.parent].start()))?;
-
- let text =
- table.print_group_content(caption.parent, latex::GroupKind::Group, caption.arg_index)?;
-
- let kind = env
- .left
- .name(&table)
- .map(latex::Token::text)
- .and_then(OutlineCaptionKind::parse)?;
-
- let label = table.find_label_by_environment(*env);
- let number = label.and_then(|label| OutlineContext::find_number(view, table, *label));
-
- let name = match &number {
- Some(number) => format!("{} {}: {}", kind.as_str(), number, text),
- None => format!("{}: {}", kind.as_str(), text),
- };
-
- let symbol = LatexSymbol {
- name,
- label: label_name(table, label),
- kind: match kind {
- OutlineCaptionKind::Figure => LatexSymbolKind::Figure,
- OutlineCaptionKind::Table => LatexSymbolKind::Table,
- OutlineCaptionKind::Listing => LatexSymbolKind::Listing,
- OutlineCaptionKind::Algorithm => LatexSymbolKind::Algorithm,
- },
- deprecated: false,
- full_range: env.range(&table),
- selection_range: selection_range(table, env.range(&table), label),
- children: Vec::new(),
- };
- Some(symbol)
-}
diff --git a/support/texlab/src/symbol/latex_section/mod.rs b/support/texlab/src/symbol/latex_section/mod.rs
deleted file mode 100644
index fc81695b50..0000000000
--- a/support/texlab/src/symbol/latex_section/mod.rs
+++ /dev/null
@@ -1,671 +0,0 @@
-mod enumeration;
-mod equation;
-mod float;
-mod theorem;
-
-use super::types::{LatexSymbol, LatexSymbolKind};
-use crate::{
- feature::{DocumentView, FeatureProvider, FeatureRequest},
- outline::{Outline, OutlineContext, OutlineContextItem},
- protocol::{DocumentSymbolParams, Options, Position, Range, RangeExt},
- syntax::{latex, CharStream, LatexLabelKind, SyntaxNode},
- workspace::DocumentContent,
-};
-use async_trait::async_trait;
-use std::path::Path;
-
-fn label_name(table: &latex::SymbolTable, label: Option<&latex::Label>) -> Option<String> {
- label.map(|label| label.names(&table)[0].text().to_owned())
-}
-
-fn selection_range(
- table: &latex::SymbolTable,
- full_range: Range,
- label: Option<&latex::Label>,
-) -> Range {
- label
- .map(|label| table[label.parent].range())
- .unwrap_or(full_range)
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
-pub struct LatexSectionSymbolProvider;
-
-#[async_trait]
-impl FeatureProvider for LatexSectionSymbolProvider {
- type Params = DocumentSymbolParams;
- type Output = Vec<LatexSymbol>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- let mut symbols = Vec::new();
- if let DocumentContent::Latex(table) = &req.current().content {
- let mut section_tree =
- build_section_tree(&req.view, table, &req.options, &req.current_dir);
- for symbol in enumeration::symbols(&req.view, table) {
- section_tree.insert_symbol(&symbol);
- }
-
- for symbol in equation::symbols(&req.view, table) {
- section_tree.insert_symbol(&symbol);
- }
-
- for symbol in float::symbols(&req.view, table) {
- section_tree.insert_symbol(&symbol);
- }
-
- for symbol in theorem::symbols(&req.view, table) {
- section_tree.insert_symbol(&symbol);
- }
-
- for symbol in section_tree.symbols {
- symbols.push(symbol);
- }
-
- for child in section_tree.children {
- symbols.push(child.into());
- }
- }
- symbols
- }
-}
-
-pub fn build_section_tree<'a>(
- view: &'a DocumentView,
- table: &'a latex::SymbolTable,
- options: &'a Options,
- current_dir: &'a Path,
-) -> LatexSectionTree<'a> {
- let mut section_tree = LatexSectionTree::from(table);
- section_tree.set_full_text(&view.current.text);
- let end_position = compute_end_position(table, &view.current.text);
- LatexSectionNode::set_full_range(&mut section_tree.children, table, end_position);
- let outline = Outline::analyze(view, options, current_dir);
- for child in &mut section_tree.children {
- child.set_label(view, &outline);
- }
- section_tree
-}
-
-fn compute_end_position(table: &latex::SymbolTable, text: &str) -> Position {
- let mut stream = CharStream::new(text);
- while stream.next().is_some() {}
- table
- .environments
- .iter()
- .find(|env| env.left.name(&table).map(latex::Token::text) == Some("document"))
- .map(|env| table[env.right.parent].start())
- .unwrap_or(stream.current_position)
-}
-
-#[derive(Debug, Clone)]
-pub struct LatexSectionNode<'a> {
- pub table: &'a latex::SymbolTable,
- pub section: &'a latex::Section,
- pub full_range: Range,
- full_text: &'a str,
- label: Option<String>,
- number: Option<String>,
- symbols: Vec<LatexSymbol>,
- children: Vec<Self>,
-}
-
-impl<'a> LatexSectionNode<'a> {
- fn new(table: &'a latex::SymbolTable, section: &'a latex::Section) -> Self {
- Self {
- table,
- section,
- full_range: Range::default(),
- full_text: "",
- label: None,
- number: None,
- symbols: Vec::new(),
- children: Vec::new(),
- }
- }
-
- fn set_full_text(&mut self, full_text: &'a str) {
- self.full_text = full_text;
- for child in &mut self.children {
- child.set_full_text(full_text);
- }
- }
-
- fn name(&self) -> String {
- self.table
- .print_group_content(
- self.section.parent,
- latex::GroupKind::Group,
- self.section.arg_index,
- )
- .unwrap_or_else(|| "Unknown".into())
- }
-
- fn set_full_range(
- children: &mut Vec<Self>,
- table: &latex::SymbolTable,
- end_position: Position,
- ) {
- for i in 0..children.len() {
- let current_end = children
- .get(i + 1)
- .map(|next| table[next.section.parent].start())
- .unwrap_or(end_position);
-
- let mut current = &mut children[i];
- current.full_range = Range::new(table[current.section.parent].start(), current_end);
- Self::set_full_range(&mut current.children, table, current_end);
- }
- }
-
- fn set_label(&mut self, view: &DocumentView, outline: &Outline) {
- if let Some(label) = self
- .table
- .labels
- .iter()
- .filter(|label| label.kind == LatexLabelKind::Definition)
- .find(|label| self.full_range.contains(self.table[label.parent].start()))
- {
- if let Some(ctx) = OutlineContext::parse(view, outline, *label) {
- let mut is_section = false;
- if let OutlineContextItem::Section { text, .. } = &ctx.item {
- if self.name() == *text {
- for name in label.names(&self.table) {
- self.label = Some(name.text().to_owned());
- }
-
- is_section = true;
- }
- }
-
- if is_section {
- self.number = ctx.number;
- }
- }
- }
-
- for child in &mut self.children {
- child.set_label(view, outline);
- }
- }
-
- fn insert_section(
- nodes: &mut Vec<Self>,
- table: &'a latex::SymbolTable,
- section: &'a latex::Section,
- ) {
- match nodes.last_mut() {
- Some(parent) => {
- if parent.section.level < section.level {
- Self::insert_section(&mut parent.children, table, section);
- } else {
- nodes.push(LatexSectionNode::new(table, section));
- }
- }
- None => {
- nodes.push(LatexSectionNode::new(table, section));
- }
- }
- }
-
- fn insert_symbol(&mut self, symbol: &LatexSymbol) -> bool {
- if !self.full_range.contains(symbol.selection_range.start) {
- return false;
- }
-
- for child in &mut self.children {
- if child.insert_symbol(symbol) {
- return true;
- }
- }
-
- self.symbols.push(symbol.clone());
- true
- }
-
- fn find(&self, label: &str) -> Option<&Self> {
- if self.label.as_ref().map(AsRef::as_ref) == Some(label) {
- Some(self)
- } else {
- for child in &self.children {
- let result = child.find(label);
- if result.is_some() {
- return result;
- }
- }
- None
- }
- }
-}
-
-impl<'a> Into<LatexSymbol> for LatexSectionNode<'a> {
- fn into(self) -> LatexSymbol {
- let name = self.name();
-
- let mut children: Vec<LatexSymbol> = self.children.into_iter().map(Into::into).collect();
-
- for symbol in self.symbols {
- children.push(symbol);
- }
-
- let full_name = match &self.number {
- Some(number) => format!("{} {}", number, name),
- None => name,
- };
-
- LatexSymbol {
- name: full_name,
- label: self.label,
- kind: LatexSymbolKind::Section,
- deprecated: false,
- full_range: self.full_range,
- selection_range: self.table[self.section.parent].range(),
- children,
- }
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct LatexSectionTree<'a> {
- symbols: Vec<LatexSymbol>,
- children: Vec<LatexSectionNode<'a>>,
-}
-
-impl<'a> LatexSectionTree<'a> {
- fn new() -> Self {
- Self {
- symbols: Vec::new(),
- children: Vec::new(),
- }
- }
-
- fn set_full_text(&mut self, full_text: &'a str) {
- for child in &mut self.children {
- child.set_full_text(full_text);
- }
- }
-
- fn insert_symbol(&mut self, symbol: &LatexSymbol) {
- for child in &mut self.children {
- if child.insert_symbol(symbol) {
- return;
- }
- }
- self.symbols.push(symbol.clone());
- }
-
- pub fn find(&self, label: &str) -> Option<&LatexSectionNode<'a>> {
- for child in &self.children {
- let result = child.find(label);
- if result.is_some() {
- return result;
- }
- }
- None
- }
-}
-
-impl<'a> From<&'a latex::SymbolTable> for LatexSectionTree<'a> {
- fn from(table: &'a latex::SymbolTable) -> Self {
- let mut root = Self::new();
- for section in &table.sections {
- LatexSectionNode::insert_section(&mut root.children, table, section);
- }
- root
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::feature::FeatureTester;
- use indoc::indoc;
-
- #[tokio::test]
- async fn empty_latex_document() {
- let actual_symbols = FeatureTester::new()
- .file("main.tex", "")
- .main("main.tex")
- .test_symbol(LatexSectionSymbolProvider)
- .await;
-
- assert!(actual_symbols.is_empty());
- }
-
- #[tokio::test]
- async fn empty_bibtex_document() {
- let actual_symbols = FeatureTester::new()
- .file("main.bib", "")
- .main("main.bib")
- .test_symbol(LatexSectionSymbolProvider)
- .await;
-
- assert!(actual_symbols.is_empty());
- }
-
- #[tokio::test]
- async fn subsection() {
- let actual_symbols = FeatureTester::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \section{Foo}
- \subsection{Bar}\label{sec:bar}
- \subsection{Baz}
- \section{Qux}
- "#
- ),
- )
- .file(
- "main.aux",
- r#"\newlabel{sec:bar}{{\relax 2.1}{4}{Bar\relax }{figure.caption.4}{}}"#,
- )
- .main("main.tex")
- .test_symbol(LatexSectionSymbolProvider)
- .await;
-
- let expected_symbols = vec![
- LatexSymbol {
- name: "Foo".into(),
- label: None,
- kind: LatexSymbolKind::Section,
- deprecated: false,
- full_range: Range::new_simple(0, 0, 3, 0),
- selection_range: Range::new_simple(0, 0, 0, 13),
- children: vec![
- LatexSymbol {
- name: "2.1 Bar".into(),
- label: Some("sec:bar".into()),
- kind: LatexSymbolKind::Section,
- deprecated: false,
- full_range: Range::new_simple(1, 0, 2, 0),
- selection_range: Range::new_simple(1, 0, 1, 16),
- children: Vec::new(),
- },
- LatexSymbol {
- name: "Baz".into(),
- label: None,
- kind: LatexSymbolKind::Section,
- deprecated: false,
- full_range: Range::new_simple(2, 0, 3, 0),
- selection_range: Range::new_simple(2, 0, 2, 16),
- children: Vec::new(),
- },
- ],
- },
- LatexSymbol {
- name: "Qux".into(),
- label: None,
- kind: LatexSymbolKind::Section,
- deprecated: false,
- full_range: Range::new_simple(3, 0, 3, 13),
- selection_range: Range::new_simple(3, 0, 3, 13),
- children: Vec::new(),
- },
- ];
-
- assert_eq!(actual_symbols, expected_symbols);
- }
-
- #[tokio::test]
- async fn section_inside_document_environment() {
- let actual_symbols = FeatureTester::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \begin{document}\section{Foo}\relax
- \end{document}
- "#
- ),
- )
- .main("main.tex")
- .test_symbol(LatexSectionSymbolProvider)
- .await;
-
- let expected_symbols = vec![LatexSymbol {
- name: "Foo".into(),
- label: None,
- kind: LatexSymbolKind::Section,
- deprecated: false,
- full_range: Range::new_simple(0, 16, 1, 0),
- selection_range: Range::new_simple(0, 16, 0, 29),
- children: Vec::new(),
- }];
-
- assert_eq!(actual_symbols, expected_symbols);
- }
-
- #[tokio::test]
- async fn enumeration() {
- let actual_symbols = FeatureTester::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \section{Foo}
- \begin{enumerate}
- \end{enumerate}
- "#
- ),
- )
- .main("main.tex")
- .test_symbol(LatexSectionSymbolProvider)
- .await;
-
- let expected_symbols = vec![LatexSymbol {
- name: "Foo".into(),
- label: None,
- kind: LatexSymbolKind::Section,
- deprecated: false,
- full_range: Range::new_simple(0, 0, 2, 15),
- selection_range: Range::new_simple(0, 0, 0, 13),
- children: vec![LatexSymbol {
- name: "Enumerate".into(),
- label: None,
- kind: LatexSymbolKind::Enumeration,
- deprecated: false,
- full_range: Range::new_simple(1, 0, 2, 15),
- selection_range: Range::new_simple(1, 0, 2, 15),
- children: Vec::new(),
- }],
- }];
-
- assert_eq!(actual_symbols, expected_symbols);
- }
-
- #[tokio::test]
- async fn equation() {
- let actual_symbols = FeatureTester::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \[Foo\]
- \begin{equation}\label{eq:foo}\end{equation}
- "#
- ),
- )
- .main("main.tex")
- .test_symbol(LatexSectionSymbolProvider)
- .await;
-
- let expected_symbols = vec![
- LatexSymbol {
- name: "Equation".into(),
- label: None,
- kind: LatexSymbolKind::Equation,
- deprecated: false,
- full_range: Range::new_simple(0, 0, 0, 7),
- selection_range: Range::new_simple(0, 0, 0, 7),
- children: Vec::new(),
- },
- LatexSymbol {
- name: "Equation".into(),
- label: Some("eq:foo".into()),
- kind: LatexSymbolKind::Equation,
- deprecated: false,
- full_range: Range::new_simple(1, 0, 1, 44),
- selection_range: Range::new_simple(1, 16, 1, 30),
- children: Vec::new(),
- },
- ];
-
- assert_eq!(actual_symbols, expected_symbols);
- }
-
- #[tokio::test]
- async fn equation_number() {
- let actual_symbols = FeatureTester::new()
- .file("main.tex", r#"\[\label{eq:foo}\]"#)
- .file(
- "main.aux",
- r#"\newlabel{eq:foo}{{\relax 2.1}{4}{Bar\relax }{figure.caption.4}{}}"#,
- )
- .main("main.tex")
- .test_symbol(LatexSectionSymbolProvider)
- .await;
-
- let expected_symbols = vec![LatexSymbol {
- name: "Equation (2.1)".into(),
- label: Some("eq:foo".into()),
- kind: LatexSymbolKind::Equation,
- deprecated: false,
- full_range: Range::new_simple(0, 0, 0, 18),
- selection_range: Range::new_simple(0, 2, 0, 16),
- children: Vec::new(),
- }];
-
- assert_eq!(actual_symbols, expected_symbols);
- }
-
- #[tokio::test]
- async fn table() {
- let actual_symbols = FeatureTester::new()
- .file("main.tex", r#"\begin{table}\caption{Foo}\end{table}"#)
- .main("main.tex")
- .test_symbol(LatexSectionSymbolProvider)
- .await;
-
- let expected_symbols = vec![LatexSymbol {
- name: "Table: Foo".into(),
- label: None,
- kind: LatexSymbolKind::Table,
- deprecated: false,
- full_range: Range::new_simple(0, 0, 0, 37),
- selection_range: Range::new_simple(0, 0, 0, 37),
- children: Vec::new(),
- }];
-
- assert_eq!(actual_symbols, expected_symbols);
- }
-
- #[tokio::test]
- async fn figure_number() {
- let actual_symbols = FeatureTester::new()
- .file(
- "main.tex",
- r#"\begin{figure}\caption{Foo}\label{fig:foo}\end{figure}"#,
- )
- .file(
- "main.aux",
- r#"\newlabel{fig:foo}{{\relax 2.1}{4}{Bar\relax }{figure.caption.4}{}}"#,
- )
- .main("main.tex")
- .test_symbol(LatexSectionSymbolProvider)
- .await;
-
- let expected_symbols = vec![LatexSymbol {
- name: "Figure 2.1: Foo".into(),
- label: Some("fig:foo".into()),
- kind: LatexSymbolKind::Figure,
- deprecated: false,
- full_range: Range::new_simple(0, 0, 0, 54),
- selection_range: Range::new_simple(0, 27, 0, 42),
- children: Vec::new(),
- }];
-
- assert_eq!(actual_symbols, expected_symbols);
- }
-
- #[tokio::test]
- async fn lemma() {
- let actual_symbols = FeatureTester::new()
- .file(
- "main.tex",
- r#"\newtheorem{lemma}{Lemma}\begin{lemma}\end{lemma}"#,
- )
- .main("main.tex")
- .test_symbol(LatexSectionSymbolProvider)
- .await;
-
- let expected_symbols = vec![LatexSymbol {
- name: "Lemma".into(),
- label: None,
- kind: LatexSymbolKind::Theorem,
- deprecated: false,
- full_range: Range::new_simple(0, 25, 0, 49),
- selection_range: Range::new_simple(0, 25, 0, 49),
- children: Vec::new(),
- }];
-
- assert_eq!(actual_symbols, expected_symbols);
- }
-
- #[tokio::test]
- async fn lemma_number() {
- let actual_symbols = FeatureTester::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \newtheorem{lemma}{Lemma}
- \begin{lemma}\label{thm:foo}\end{lemma}
- "#
- ),
- )
- .file(
- "main.aux",
- r#"\newlabel{thm:foo}{{\relax 2.1}{4}{Bar\relax }{figure.caption.4}{}}"#,
- )
- .main("main.tex")
- .test_symbol(LatexSectionSymbolProvider)
- .await;
-
- let expected_symbols = vec![LatexSymbol {
- name: "Lemma 2.1".into(),
- label: Some("thm:foo".into()),
- kind: LatexSymbolKind::Theorem,
- deprecated: false,
- full_range: Range::new_simple(1, 0, 1, 39),
- selection_range: Range::new_simple(1, 13, 1, 28),
- children: Vec::new(),
- }];
-
- assert_eq!(actual_symbols, expected_symbols);
- }
-
- #[tokio::test]
- async fn lemma_description() {
- let actual_symbols = FeatureTester::new()
- .file(
- "main.tex",
- r#"\newtheorem{lemma}{Lemma}\begin{lemma}[Foo]\end{lemma}"#,
- )
- .main("main.tex")
- .test_symbol(LatexSectionSymbolProvider)
- .await;
-
- let expected_symbols = vec![LatexSymbol {
- name: "Lemma (Foo)".into(),
- label: None,
- kind: LatexSymbolKind::Theorem,
- deprecated: false,
- full_range: Range::new_simple(0, 25, 0, 54),
- selection_range: Range::new_simple(0, 25, 0, 54),
- children: Vec::new(),
- }];
-
- assert_eq!(actual_symbols, expected_symbols);
- }
-}
diff --git a/support/texlab/src/symbol/latex_section/theorem.rs b/support/texlab/src/symbol/latex_section/theorem.rs
deleted file mode 100644
index dac231fbfa..0000000000
--- a/support/texlab/src/symbol/latex_section/theorem.rs
+++ /dev/null
@@ -1,70 +0,0 @@
-use super::{label_name, selection_range};
-use crate::{
- feature::DocumentView,
- outline::OutlineContext,
- symbol::types::{LatexSymbol, LatexSymbolKind},
- syntax::latex,
- workspace::DocumentContent,
-};
-use titlecase::titlecase;
-
-pub fn symbols(view: &DocumentView, table: &latex::SymbolTable) -> Vec<LatexSymbol> {
- table
- .environments
- .iter()
- .filter_map(|env| make_symbol(view, table, *env))
- .collect()
-}
-
-fn make_symbol(
- view: &DocumentView,
- main_table: &latex::SymbolTable,
- env: latex::Environment,
-) -> Option<LatexSymbol> {
- let env_name = env.left.name(&main_table).map(latex::Token::text)?;
-
- for document in &view.related {
- if let DocumentContent::Latex(table) = &document.content {
- for definition in &table.theorem_definitions {
- if definition.name(&table).text() == env_name {
- let kind = table
- .print_group_content(
- definition.parent,
- latex::GroupKind::Group,
- definition.arg_index + 1,
- )
- .unwrap_or_else(|| titlecase(env_name));
-
- let desc = main_table.print_group_content(
- env.left.parent,
- latex::GroupKind::Options,
- 0,
- );
-
- let label = main_table.find_label_by_environment(env);
- let number = label
- .and_then(|label| OutlineContext::find_number(view, &main_table, *label));
-
- let name = match (desc, number) {
- (Some(desc), Some(num)) => format!("{} {} ({})", kind, num, desc),
- (Some(desc), None) => format!("{} ({})", kind, desc),
- (None, Some(num)) => format!("{} {}", kind, num),
- (None, None) => kind,
- };
-
- let symbol = LatexSymbol {
- name,
- label: label_name(main_table, label),
- kind: LatexSymbolKind::Theorem,
- deprecated: false,
- full_range: env.range(&main_table),
- selection_range: selection_range(main_table, env.range(&main_table), label),
- children: Vec::new(),
- };
- return Some(symbol);
- }
- }
- }
- }
- None
-}
diff --git a/support/texlab/src/symbol/mod.rs b/support/texlab/src/symbol/mod.rs
deleted file mode 100644
index b292f15bb2..0000000000
--- a/support/texlab/src/symbol/mod.rs
+++ /dev/null
@@ -1,178 +0,0 @@
-mod bibtex_entry;
-mod bibtex_string;
-mod latex_section;
-mod project_order;
-mod types;
-
-pub use self::latex_section::{build_section_tree, LatexSectionNode, LatexSectionTree};
-
-use self::{
- bibtex_entry::BibtexEntrySymbolProvider, bibtex_string::BibtexStringSymbolProvider,
- latex_section::LatexSectionSymbolProvider, project_order::ProjectOrdering, types::LatexSymbol,
-};
-use crate::{
- feature::{ConcatProvider, DocumentView, FeatureProvider, FeatureRequest},
- protocol::{
- ClientCapabilities, ClientCapabilitiesExt, DocumentSymbolParams, DocumentSymbolResponse,
- Options, PartialResultParams, SymbolInformation, TextDocumentIdentifier, Uri,
- WorkDoneProgressParams, WorkspaceSymbolParams,
- },
- tex::Distribution,
- workspace::Snapshot,
-};
-use async_trait::async_trait;
-use std::{
- cmp::Reverse,
- path::{Path, PathBuf},
- sync::Arc,
-};
-
-pub struct SymbolProvider {
- provider: ConcatProvider<DocumentSymbolParams, LatexSymbol>,
-}
-
-impl SymbolProvider {
- pub fn new() -> Self {
- Self {
- provider: ConcatProvider::new(vec![
- Box::new(BibtexEntrySymbolProvider),
- Box::new(BibtexStringSymbolProvider),
- Box::new(LatexSectionSymbolProvider),
- ]),
- }
- }
-}
-
-impl Default for SymbolProvider {
- fn default() -> Self {
- Self::new()
- }
-}
-
-#[async_trait]
-impl FeatureProvider for SymbolProvider {
- type Params = DocumentSymbolParams;
- type Output = Vec<LatexSymbol>;
-
- async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output {
- self.provider.execute(req).await
- }
-}
-
-pub fn document_symbols(
- client_capabilities: &ClientCapabilities,
- snapshot: &Snapshot,
- uri: &Uri,
- options: &Options,
- current_dir: &Path,
- symbols: Vec<LatexSymbol>,
-) -> DocumentSymbolResponse {
- if client_capabilities.has_hierarchical_document_symbol_support() {
- DocumentSymbolResponse::Nested(symbols.into_iter().map(Into::into).collect())
- } else {
- let mut buffer = Vec::new();
- for symbol in symbols {
- symbol.flatten(&mut buffer);
- }
- let mut buffer = buffer
- .into_iter()
- .map(|symbol| symbol.into_symbol_info(uri.clone()))
- .collect();
- sort_symbols(snapshot, options, &current_dir, &mut buffer);
- DocumentSymbolResponse::Flat(buffer)
- }
-}
-
-struct WorkspaceSymbol {
- info: SymbolInformation,
- search_text: String,
-}
-
-pub async fn workspace_symbols<'a>(
- distro: Arc<dyn Distribution>,
- client_capabilities: Arc<ClientCapabilities>,
- snapshot: Arc<Snapshot>,
- options: &'a Options,
- current_dir: Arc<PathBuf>,
- params: &'a WorkspaceSymbolParams,
-) -> Vec<SymbolInformation> {
- let provider = SymbolProvider::new();
- let mut symbols = Vec::new();
-
- for doc in &snapshot.0 {
- let uri: Uri = doc.uri.clone();
- let req = FeatureRequest {
- params: DocumentSymbolParams {
- text_document: TextDocumentIdentifier::new(uri.clone().into()),
- work_done_progress_params: WorkDoneProgressParams::default(),
- partial_result_params: PartialResultParams::default(),
- },
- view: DocumentView::analyze(
- Arc::clone(&snapshot),
- Arc::clone(&doc),
- &options,
- &current_dir,
- ),
- distro: distro.clone(),
- client_capabilities: Arc::clone(&client_capabilities),
- options: options.clone(),
- current_dir: Arc::clone(&current_dir),
- };
-
- let mut buffer = Vec::new();
- for symbol in provider.execute(&req).await {
- symbol.flatten(&mut buffer);
- }
-
- for symbol in buffer {
- symbols.push(WorkspaceSymbol {
- search_text: symbol.search_text(),
- info: symbol.into_symbol_info(uri.clone()),
- });
- }
- }
-
- let query_words: Vec<_> = params
- .query
- .split_whitespace()
- .map(str::to_lowercase)
- .collect();
- let mut filtered = Vec::new();
- for symbol in symbols {
- let mut included = true;
- for word in &query_words {
- if !symbol.search_text.contains(word) {
- included = false;
- break;
- }
- }
-
- if included {
- filtered.push(symbol.info);
- }
- }
- sort_symbols(&snapshot, options, &current_dir, &mut filtered);
- filtered
-}
-
-fn sort_symbols(
- snapshot: &Snapshot,
- options: &Options,
- current_dir: &Path,
- symbols: &mut Vec<SymbolInformation>,
-) {
- let ordering = ProjectOrdering::analyze(snapshot, options, current_dir);
- symbols.sort_by(|left, right| {
- let left_key = (
- ordering.get(&Uri::from(left.location.uri.clone())),
- left.location.range.start,
- Reverse(left.location.range.end),
- );
- let right_key = (
- ordering.get(&Uri::from(right.location.uri.clone())),
- right.location.range.start,
- Reverse(right.location.range.end),
- );
- left_key.cmp(&right_key)
- });
-}
diff --git a/support/texlab/src/symbol/project_order.rs b/support/texlab/src/symbol/project_order.rs
deleted file mode 100644
index 8a2ec5f6d0..0000000000
--- a/support/texlab/src/symbol/project_order.rs
+++ /dev/null
@@ -1,181 +0,0 @@
-use crate::{
- protocol::{Options, Uri},
- workspace::{Document, DocumentContent, Snapshot},
-};
-use petgraph::{algo::tarjan_scc, Directed, Graph};
-use std::{collections::HashSet, path::Path, sync::Arc, usize};
-
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub struct ProjectOrdering {
- ordering: Vec<Arc<Document>>,
-}
-
-impl ProjectOrdering {
- pub fn get(&self, uri: &Uri) -> usize {
- self.ordering
- .iter()
- .position(|doc| doc.uri == *uri)
- .unwrap_or(usize::MAX)
- }
-
- pub fn analyze(snapshot: &Snapshot, options: &Options, current_dir: &Path) -> Self {
- let mut ordering = Vec::new();
- let comps = Self::connected_components(snapshot, options, current_dir);
- for comp in comps {
- let graph = Self::build_dependency_graph(&comp);
-
- let mut visited = HashSet::new();
- let root_index = *graph.node_weight(tarjan_scc(&graph)[0][0]).unwrap();
- let mut stack = vec![Arc::clone(&comp[root_index])];
-
- while let Some(doc) = stack.pop() {
- if !visited.insert(doc.uri.as_str().to_owned()) {
- continue;
- }
-
- ordering.push(Arc::clone(&doc));
- if let DocumentContent::Latex(tree) = &doc.content {
- for include in tree.includes.iter().rev() {
- for targets in &include.all_targets {
- for target in targets {
- if let Some(child) = snapshot.find(target) {
- stack.push(child);
- }
- }
- }
- }
- }
- }
- }
-
- Self { ordering }
- }
-
- fn connected_components(
- snapshot: &Snapshot,
- options: &Options,
- current_dir: &Path,
- ) -> Vec<Vec<Arc<Document>>> {
- let mut comps = Vec::new();
- let mut visited = HashSet::new();
- for root in &snapshot.0 {
- if !visited.insert(root.uri.clone()) {
- continue;
- }
-
- let comp = snapshot.relations(&root.uri, options, current_dir);
- for document in &comp {
- visited.insert(document.uri.clone());
- }
- comps.push(comp);
- }
- comps
- }
-
- fn build_dependency_graph(docs: &[Arc<Document>]) -> Graph<usize, (), Directed> {
- let mut graph = Graph::new();
- let nodes: Vec<_> = (0..docs.len()).map(|i| graph.add_node(i)).collect();
-
- for (i, doc) in docs.iter().enumerate() {
- if let DocumentContent::Latex(tree) = &doc.content {
- for targets in tree
- .includes
- .iter()
- .flat_map(|include| &include.all_targets)
- {
- for target in targets {
- if let Some(j) = docs.iter().position(|doc| doc.uri == *target) {
- graph.add_edge(nodes[j], nodes[i], ());
- break;
- }
- }
- }
- }
- }
- graph
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- tex::{Language, Resolver},
- workspace::DocumentParams,
- };
- use std::env;
-
- fn create_simple_document(uri: &Uri, language: Language, text: &str) -> Arc<Document> {
- Arc::new(Document::open(DocumentParams {
- uri: uri.clone(),
- text: text.into(),
- language,
- resolver: &Resolver::default(),
- options: &Options::default(),
- current_dir: &env::current_dir().unwrap(),
- }))
- }
-
- #[test]
- fn no_cycles() {
- let a = Uri::parse("http://example.com/a.tex").unwrap();
- let b = Uri::parse("http://example.com/b.tex").unwrap();
- let c = Uri::parse("http://example.com/c.tex").unwrap();
- let mut snapshot = Snapshot::new();
- snapshot.0 = vec![
- create_simple_document(&a, Language::Latex, ""),
- create_simple_document(&b, Language::Latex, ""),
- create_simple_document(&c, Language::Latex, r#"\include{b}\include{a}"#),
- ];
-
- let current_dir = env::current_dir().unwrap();
- let ordering = ProjectOrdering::analyze(&snapshot, &Options::default(), &current_dir);
-
- assert_eq!(ordering.get(&a), 2);
- assert_eq!(ordering.get(&b), 1);
- assert_eq!(ordering.get(&c), 0);
- }
-
- #[test]
- fn cycles() {
- let a = Uri::parse("http://example.com/a.tex").unwrap();
- let b = Uri::parse("http://example.com/b.tex").unwrap();
- let c = Uri::parse("http://example.com/c.tex").unwrap();
- let mut snapshot = Snapshot::new();
- snapshot.0 = vec![
- create_simple_document(&a, Language::Latex, r#"\include{b}"#),
- create_simple_document(&b, Language::Latex, r#"\include{a}"#),
- create_simple_document(&c, Language::Latex, r#"\include{a}"#),
- ];
-
- let current_dir = env::current_dir().unwrap();
- let ordering = ProjectOrdering::analyze(&snapshot, &Options::default(), &current_dir);
-
- assert_eq!(ordering.get(&a), 1);
- assert_eq!(ordering.get(&b), 2);
- assert_eq!(ordering.get(&c), 0);
- }
-
- #[test]
- fn multiple_roots() {
- let a = Uri::parse("http://example.com/a.tex").unwrap();
- let b = Uri::parse("http://example.com/b.tex").unwrap();
- let c = Uri::parse("http://example.com/c.tex").unwrap();
- let d = Uri::parse("http://example.com/d.tex").unwrap();
- let mut snapshot = Snapshot::new();
- snapshot.0 = vec![
- create_simple_document(&a, Language::Latex, r#"\include{b}"#),
- create_simple_document(&b, Language::Latex, ""),
- create_simple_document(&c, Language::Latex, ""),
- create_simple_document(&d, Language::Latex, r#"\include{c}"#),
- ];
-
- let current_dir = env::current_dir().unwrap();
- let ordering = ProjectOrdering::analyze(&snapshot, &Options::default(), &current_dir);
-
- assert_eq!(ordering.get(&a), 0);
- assert_eq!(ordering.get(&b), 1);
- assert_eq!(ordering.get(&d), 2);
- assert_eq!(ordering.get(&c), 3);
- }
-}
diff --git a/support/texlab/src/syntax.rs b/support/texlab/src/syntax.rs
new file mode 100644
index 0000000000..5a906c0ea7
--- /dev/null
+++ b/support/texlab/src/syntax.rs
@@ -0,0 +1,17 @@
+use cstree::TextRange;
+
+pub mod bibtex;
+pub mod build_log;
+pub mod latex;
+
+pub trait CstNode<'a> {
+ type Lang: cstree::Language;
+
+ fn cast(node: &'a cstree::ResolvedNode<Self::Lang>) -> Option<Self>
+ where
+ Self: Sized;
+
+ fn syntax(&self) -> &'a cstree::ResolvedNode<Self::Lang>;
+
+ fn small_range(&self) -> TextRange;
+}
diff --git a/support/texlab/src/syntax/bibtex.rs b/support/texlab/src/syntax/bibtex.rs
new file mode 100644
index 0000000000..c6e8bb3760
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex.rs
@@ -0,0 +1,34 @@
+mod cst;
+mod kind;
+mod lexer;
+mod parser;
+
+pub use self::{
+ cst::*,
+ kind::SyntaxKind::{self, *},
+ parser::{parse, Parse},
+};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum Language {}
+
+impl cstree::Language for Language {
+ type Kind = SyntaxKind;
+
+ fn kind_from_raw(raw: cstree::SyntaxKind) -> Self::Kind {
+ assert!(raw.0 <= ROOT as u16);
+ unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
+ }
+
+ fn kind_to_raw(kind: Self::Kind) -> cstree::SyntaxKind {
+ kind.into()
+ }
+}
+
+pub type SyntaxNode = cstree::ResolvedNode<Language>;
+
+pub type SyntaxToken = cstree::ResolvedToken<Language>;
+
+pub type SyntaxElement = cstree::ResolvedElement<Language>;
+
+pub type SyntaxElementRef<'a> = cstree::ResolvedElementRef<'a, Language>;
diff --git a/support/texlab/src/syntax/bibtex/ast.rs b/support/texlab/src/syntax/bibtex/ast.rs
deleted file mode 100644
index 77ee98e26d..0000000000
--- a/support/texlab/src/syntax/bibtex/ast.rs
+++ /dev/null
@@ -1,431 +0,0 @@
-use crate::{
- protocol::{Position, Range, RangeExt},
- syntax::{Span, SyntaxNode},
-};
-use itertools::Itertools;
-use petgraph::graph::{Graph, NodeIndex};
-use serde::{Deserialize, Serialize};
-use std::fmt;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
-pub enum TokenKind {
- PreambleKind,
- StringKind,
- EntryKind,
- Word,
- Command,
- Assign,
- Comma,
- Concat,
- Quote,
- BeginBrace,
- EndBrace,
- BeginParen,
- EndParen,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct Token {
- pub span: Span,
- pub kind: TokenKind,
-}
-
-impl SyntaxNode for Token {
- fn range(&self) -> Range {
- self.span.range()
- }
-}
-
-impl Token {
- pub fn new(span: Span, kind: TokenKind) -> Self {
- Self { span, kind }
- }
-
- pub fn text(&self) -> &str {
- &self.span.text
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
-pub struct Root {
- pub range: Range,
-}
-
-impl SyntaxNode for Root {
- fn range(&self) -> Range {
- self.range
- }
-}
-
-impl fmt::Debug for Root {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "Root")
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct Comment {
- pub token: Token,
-}
-
-impl SyntaxNode for Comment {
- fn range(&self) -> Range {
- self.token.range()
- }
-}
-
-impl fmt::Debug for Comment {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "Comment({})", self.token.text())
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct Preamble {
- pub range: Range,
- pub ty: Token,
- pub left: Option<Token>,
- pub right: Option<Token>,
-}
-
-impl SyntaxNode for Preamble {
- fn range(&self) -> Range {
- self.range
- }
-}
-
-impl fmt::Debug for Preamble {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "Preamble")
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct String {
- pub range: Range,
- pub ty: Token,
- pub left: Option<Token>,
- pub name: Option<Token>,
- pub assign: Option<Token>,
- pub right: Option<Token>,
-}
-
-impl SyntaxNode for String {
- fn range(&self) -> Range {
- self.range
- }
-}
-
-impl fmt::Debug for String {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "String({:?})", self.name.as_ref().map(Token::text))
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct Entry {
- pub range: Range,
- pub ty: Token,
- pub left: Option<Token>,
- pub key: Option<Token>,
- pub comma: Option<Token>,
- pub right: Option<Token>,
-}
-
-impl SyntaxNode for Entry {
- fn range(&self) -> Range {
- self.range
- }
-}
-
-impl fmt::Debug for Entry {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "Entry({:?})", self.key.as_ref().map(Token::text))
- }
-}
-
-impl Entry {
- pub fn is_comment(&self) -> bool {
- self.ty.text().to_lowercase() == "@comment"
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct Field {
- pub range: Range,
- pub name: Token,
- pub assign: Option<Token>,
- pub comma: Option<Token>,
-}
-
-impl SyntaxNode for Field {
- fn range(&self) -> Range {
- self.range
- }
-}
-
-impl fmt::Debug for Field {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "Field({})", self.name.text())
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct Word {
- pub token: Token,
-}
-
-impl SyntaxNode for Word {
- fn range(&self) -> Range {
- self.token.range()
- }
-}
-
-impl fmt::Debug for Word {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "Word({})", self.token.text())
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct Command {
- pub token: Token,
-}
-
-impl SyntaxNode for Command {
- fn range(&self) -> Range {
- self.token.range()
- }
-}
-
-impl fmt::Debug for Command {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "Command({})", self.token.text())
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct QuotedContent {
- pub range: Range,
- pub left: Token,
- pub right: Option<Token>,
-}
-
-impl SyntaxNode for QuotedContent {
- fn range(&self) -> Range {
- self.range
- }
-}
-
-impl fmt::Debug for QuotedContent {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "QuotedContent")
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct BracedContent {
- pub range: Range,
- pub left: Token,
- pub right: Option<Token>,
-}
-
-impl SyntaxNode for BracedContent {
- fn range(&self) -> Range {
- self.range
- }
-}
-
-impl fmt::Debug for BracedContent {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "BracedContent")
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct Concat {
- pub range: Range,
- pub operator: Token,
-}
-
-impl SyntaxNode for Concat {
- fn range(&self) -> Range {
- self.range
- }
-}
-
-impl fmt::Debug for Concat {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "Concat")
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub enum Node {
- Root(Root),
- Comment(Comment),
- Preamble(Box<Preamble>),
- String(Box<String>),
- Entry(Box<Entry>),
- Field(Box<Field>),
- Word(Word),
- Command(Command),
- QuotedContent(QuotedContent),
- BracedContent(BracedContent),
- Concat(Concat),
-}
-
-impl SyntaxNode for Node {
- fn range(&self) -> Range {
- match self {
- Self::Root(root) => root.range(),
- Self::Comment(comment) => comment.range(),
- Self::Preamble(preamble) => preamble.range(),
- Self::String(string) => string.range(),
- Self::Entry(entry) => entry.range(),
- Self::Field(field) => field.range(),
- Self::Word(word) => word.range(),
- Self::Command(cmd) => cmd.range(),
- Self::QuotedContent(content) => content.range(),
- Self::BracedContent(content) => content.range(),
- Self::Concat(concat) => concat.range(),
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct Tree {
- pub graph: Graph<Node, ()>,
- pub root: NodeIndex,
-}
-
-impl Tree {
- pub fn children(&self, parent: NodeIndex) -> impl Iterator<Item = NodeIndex> {
- self.graph
- .neighbors(parent)
- .sorted_by_key(|child| self.graph[*child].start())
- }
-
- pub fn has_children(&self, parent: NodeIndex) -> bool {
- self.children(parent).next().is_some()
- }
-
- pub fn walk<'a, V: Visitor<'a>>(&'a self, visitor: &mut V, parent: NodeIndex) {
- for child in self.children(parent) {
- visitor.visit(self, child);
- }
- }
-
- pub fn find(&self, pos: Position) -> Vec<NodeIndex> {
- let mut finder = Finder::new(pos);
- finder.visit(self, self.root);
- finder.results
- }
-
- pub fn as_preamble(&self, node: NodeIndex) -> Option<&Preamble> {
- if let Node::Preamble(preamble) = &self.graph[node] {
- Some(preamble)
- } else {
- None
- }
- }
-
- pub fn as_string(&self, node: NodeIndex) -> Option<&String> {
- if let Node::String(string) = &self.graph[node] {
- Some(string)
- } else {
- None
- }
- }
-
- pub fn as_entry(&self, node: NodeIndex) -> Option<&Entry> {
- if let Node::Entry(entry) = &self.graph[node] {
- Some(entry)
- } else {
- None
- }
- }
-
- pub fn as_field(&self, node: NodeIndex) -> Option<&Field> {
- if let Node::Field(field) = &self.graph[node] {
- Some(field)
- } else {
- None
- }
- }
-
- pub fn as_command(&self, node: NodeIndex) -> Option<&Command> {
- if let Node::Command(cmd) = &self.graph[node] {
- Some(cmd)
- } else {
- None
- }
- }
-
- pub fn as_word(&self, node: NodeIndex) -> Option<&Word> {
- if let Node::Word(word) = &self.graph[node] {
- Some(word)
- } else {
- None
- }
- }
-
- pub fn entry_by_key(&self, key: &str) -> Option<NodeIndex> {
- for node in self.children(self.root) {
- if let Some(entry) = self.as_entry(node) {
- if entry.key.as_ref().map(Token::text) == Some(key) {
- return Some(node);
- }
- }
- }
- None
- }
-
- pub fn field_by_name(&self, parent: NodeIndex, name: &str) -> Option<NodeIndex> {
- let name = name.to_lowercase();
- self.as_entry(parent)?;
- for node in self.children(parent) {
- if let Some(field) = self.as_field(node) {
- if field.name.text() == name {
- return Some(node);
- }
- }
- }
- None
- }
-
- pub fn crossref(&self, entry: NodeIndex) -> Option<NodeIndex> {
- let field = self.field_by_name(entry, "crossref")?;
- let content = self.children(field).next()?;
- let key = self.as_word(self.children(content).next()?)?;
- self.entry_by_key(key.token.text())
- }
-}
-
-pub trait Visitor<'a> {
- fn visit(&mut self, tree: &'a Tree, node: NodeIndex);
-}
-
-#[derive(Debug)]
-struct Finder {
- position: Position,
- results: Vec<NodeIndex>,
-}
-
-impl Finder {
- fn new(position: Position) -> Self {
- Self {
- position,
- results: Vec::new(),
- }
- }
-}
-
-impl<'a> Visitor<'a> for Finder {
- fn visit(&mut self, tree: &'a Tree, node: NodeIndex) {
- if tree.graph[node].range().contains(self.position) {
- self.results.push(node);
- tree.walk(self, node);
- }
- }
-}
diff --git a/support/texlab/src/syntax/bibtex/cst.rs b/support/texlab/src/syntax/bibtex/cst.rs
new file mode 100644
index 0000000000..7f824681dc
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/cst.rs
@@ -0,0 +1,212 @@
+use cstree::TextRange;
+
+use crate::syntax::CstNode;
+
+use super::{Language, SyntaxKind::*, SyntaxNode, SyntaxToken};
+
+macro_rules! cst_node {
+ ($name:ident, $($kind:pat),+) => {
+ #[derive(Clone)]
+ #[repr(transparent)]
+ pub struct $name<'a>(&'a SyntaxNode);
+
+ impl<'a> CstNode<'a> for $name<'a> {
+ type Lang = Language;
+
+ fn cast(node: &'a cstree::ResolvedNode<Self::Lang>) -> Option<Self>
+ where
+ Self: Sized,
+ {
+ match node.kind() {
+ $($kind => Some(Self(node)),)+
+ _ => None,
+ }
+ }
+
+ fn syntax(&self) -> &'a cstree::ResolvedNode<Self::Lang> {
+ &self.0
+ }
+
+ fn small_range(&self) -> TextRange {
+ let full_range = self.syntax().text_range();
+ let start = full_range.start();
+ let mut token = self.syntax().last_token();
+ while let Some(current) = token {
+ if !matches!(current.kind(), WHITESPACE | JUNK) {
+ return TextRange::new(start, current.text_range().end());
+ }
+ token = current.prev_token();
+ }
+ TextRange::new(start, start)
+ }
+ }
+ };
+}
+
+pub trait HasCurly<'a>: CstNode<'a, Lang = Language> {
+ fn left_curly(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == L_CURLY.into())
+ }
+
+ fn right_curly(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == R_CURLY.into())
+ }
+}
+
+pub trait HasQuotes<'a>: CstNode<'a, Lang = Language> {
+ fn left_quote(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == QUOTE.into())
+ }
+
+ fn right_quote(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .filter(|node| node.kind() == QUOTE.into())
+ .nth(1)
+ }
+}
+
+pub trait HasDelimiters<'a>: CstNode<'a, Lang = Language> {
+ fn left_delimiter(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| matches!(node.kind(), L_CURLY | L_PAREN))
+ }
+
+ fn right_delimiter(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| matches!(node.kind(), R_CURLY | R_PAREN))
+ }
+}
+
+pub trait HasType<'a>: CstNode<'a, Lang = Language> {
+ fn ty(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| {
+ matches!(
+ node.kind(),
+ PREAMBLE_TYPE | STRING_TYPE | COMMENT_TYPE | ENTRY_TYPE
+ )
+ })
+ }
+}
+
+cst_node!(Root, ROOT);
+
+cst_node!(Junk, JUNK);
+
+cst_node!(Comment, COMMENT);
+
+impl<'a> HasType<'a> for Comment<'a> {}
+
+cst_node!(Preamble, PREAMBLE);
+
+impl<'a> HasType<'a> for Preamble<'a> {}
+
+impl<'a> HasDelimiters<'a> for Preamble<'a> {}
+
+impl<'a> Preamble<'a> {
+ pub fn value(&self) -> Option<Value<'a>> {
+ self.syntax().children().find_map(Value::cast)
+ }
+}
+
+cst_node!(String, STRING);
+
+impl<'a> HasType<'a> for String<'a> {}
+
+impl<'a> HasDelimiters<'a> for String<'a> {}
+
+impl<'a> String<'a> {
+ pub fn name(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == WORD)
+ }
+
+ pub fn equality_sign(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == EQUALITY_SIGN)
+ }
+
+ pub fn value(&self) -> Option<Value<'a>> {
+ self.syntax().children().find_map(Value::cast)
+ }
+}
+
+cst_node!(Entry, ENTRY);
+
+impl<'a> HasType<'a> for Entry<'a> {}
+
+impl<'a> HasDelimiters<'a> for Entry<'a> {}
+
+impl<'a> Entry<'a> {
+ pub fn key(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == WORD)
+ }
+
+ pub fn fields(&self) -> impl Iterator<Item = Field<'a>> {
+ self.syntax().children().filter_map(Field::cast)
+ }
+}
+
+cst_node!(Field, FIELD);
+
+impl<'a> Field<'a> {
+ pub fn name(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == WORD)
+ }
+
+ pub fn equality_sign(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == EQUALITY_SIGN)
+ }
+
+ pub fn value(&self) -> Option<Value<'a>> {
+ self.syntax().children().find_map(Value::cast)
+ }
+}
+
+cst_node!(Value, VALUE);
+
+impl<'a> Value<'a> {
+ pub fn tokens(&self) -> impl Iterator<Item = Token<'a>> {
+ self.syntax().children().filter_map(Token::cast)
+ }
+}
+
+cst_node!(Token, TOKEN);
+
+cst_node!(BraceGroup, BRACE_GROUP);
+
+impl<'a> HasCurly<'a> for BraceGroup<'a> {}
+
+cst_node!(QuoteGroup, QUOTE_GROUP);
+
+impl<'a> HasQuotes<'a> for QuoteGroup<'a> {}
diff --git a/support/texlab/src/syntax/bibtex/formatter.rs b/support/texlab/src/syntax/bibtex/formatter.rs
deleted file mode 100644
index d4965e7cd4..0000000000
--- a/support/texlab/src/syntax/bibtex/formatter.rs
+++ /dev/null
@@ -1,329 +0,0 @@
-use super::ast::*;
-use crate::{protocol::BibtexFormattingOptions, syntax::text::SyntaxNode};
-use petgraph::graph::NodeIndex;
-use std::{i32, string::String as StdString};
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub struct FormattingParams<'a> {
- pub tab_size: usize,
- pub insert_spaces: bool,
- pub options: &'a BibtexFormattingOptions,
-}
-
-impl<'a> FormattingParams<'a> {
- fn line_length(self) -> i32 {
- let line_length = self.options.line_length.unwrap_or(120);
- if line_length <= 0 {
- i32::MAX
- } else {
- line_length
- }
- }
-
- fn indent(self) -> StdString {
- if self.insert_spaces {
- let mut buffer = StdString::new();
- for _ in 0..self.tab_size {
- buffer.push(' ');
- }
- buffer
- } else {
- "\t".into()
- }
- }
-}
-
-#[derive(Debug, Clone)]
-struct Formatter<'a> {
- params: FormattingParams<'a>,
- indent: StdString,
- output: StdString,
- align: Vec<usize>,
-}
-
-impl<'a> Formatter<'a> {
- fn new(params: FormattingParams<'a>) -> Self {
- Self {
- params,
- indent: params.indent(),
- output: StdString::new(),
- align: Vec::new(),
- }
- }
-
- fn visit_token_lowercase(&mut self, token: &Token) {
- self.output.push_str(token.text().to_lowercase().as_ref());
- }
-
- fn should_insert_space(previous: &Token, current: &Token) -> bool {
- previous.start().line != current.start().line
- || previous.end().character < current.start().character
- }
-}
-
-impl<'a, 'b> Visitor<'b> for Formatter<'a> {
- fn visit(&mut self, tree: &'b Tree, node: NodeIndex) {
- match &tree.graph[node] {
- Node::Root(_) => tree.walk(self, node),
- Node::Comment(comment) => self.output.push_str(comment.token.text()),
- Node::Preamble(preamble) => {
- self.visit_token_lowercase(&preamble.ty);
- self.output.push('{');
- if tree.has_children(node) {
- self.align.push(self.output.chars().count());
- tree.walk(self, node);
- self.output.push('}');
- }
- }
- Node::String(string) => {
- self.visit_token_lowercase(&string.ty);
- self.output.push('{');
- if let Some(name) = &string.name {
- self.output.push_str(name.text());
- self.output.push_str(" = ");
- if tree.has_children(node) {
- self.align.push(self.output.chars().count());
- tree.walk(self, node);
- self.output.push('}');
- }
- }
- }
- Node::Entry(entry) => {
- self.visit_token_lowercase(&entry.ty);
- self.output.push('{');
- if let Some(key) = &entry.key {
- self.output.push_str(key.text());
- self.output.push(',');
- self.output.push('\n');
- tree.walk(self, node);
- self.output.push('}');
- }
- }
- Node::Field(field) => {
- self.output.push_str(&self.indent);
- self.visit_token_lowercase(&field.name);
- self.output.push_str(" = ");
- if tree.has_children(node) {
- let count = field.name.text().chars().count();
- self.align.push(self.params.tab_size as usize + count + 3);
- tree.walk(self, node);
- self.output.push(',');
- self.output.push('\n');
- }
- }
- Node::Word(_)
- | Node::Command(_)
- | Node::BracedContent(_)
- | Node::QuotedContent(_)
- | Node::Concat(_) => {
- let mut analyzer = ContentAnalyzer::default();
- analyzer.visit(tree, node);
- let tokens = analyzer.tokens;
- self.output.push_str(tokens[0].text());
-
- let align = self.align.pop().unwrap_or_default();
- let mut length = align + tokens[0].text().chars().count();
- for i in 1..tokens.len() {
- let previous = tokens[i - 1];
- let current = tokens[i];
- let current_length = current.text().chars().count();
-
- let insert_space = Self::should_insert_space(previous, current);
- let space_length = if insert_space { 1 } else { 0 };
-
- if length + current_length + space_length > self.params.line_length() as usize {
- self.output.push('\n');
- self.output.push_str(self.indent.as_ref());
- for _ in 0..=align - self.params.tab_size {
- self.output.push(' ');
- }
- length = align;
- } else if insert_space {
- self.output.push(' ');
- length += 1;
- }
- self.output.push_str(current.text());
- length += current_length;
- }
- }
- }
- }
-}
-
-#[derive(Debug, Default)]
-struct ContentAnalyzer<'a> {
- tokens: Vec<&'a Token>,
-}
-
-impl<'a> Visitor<'a> for ContentAnalyzer<'a> {
- fn visit(&mut self, tree: &'a Tree, node: NodeIndex) {
- match &tree.graph[node] {
- Node::Root(_)
- | Node::Comment(_)
- | Node::Preamble(_)
- | Node::String(_)
- | Node::Entry(_)
- | Node::Field(_) => tree.walk(self, node),
- Node::Word(word) => self.tokens.push(&word.token),
- Node::Command(cmd) => self.tokens.push(&cmd.token),
- Node::QuotedContent(content) => {
- self.tokens.push(&content.left);
- tree.walk(self, node);
- if let Some(right) = &content.right {
- self.tokens.push(right);
- }
- }
- Node::BracedContent(content) => {
- self.tokens.push(&content.left);
- tree.walk(self, node);
- if let Some(right) = &content.right {
- self.tokens.push(right);
- }
- }
- Node::Concat(concat) => {
- let mut children = tree.children(node);
- let left = children.next().unwrap();
- self.visit(tree, left);
- self.tokens.push(&concat.operator);
- children.for_each(|right| self.visit(tree, right));
- }
- }
- }
-}
-
-pub fn format(tree: &Tree, node: NodeIndex, params: FormattingParams) -> StdString {
- let mut formatter = Formatter::new(params);
- formatter.visit(tree, node);
- formatter.output
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::syntax::bibtex;
- use indoc::indoc;
-
- fn verify(source: &str, expected: &str, line_length: i32) {
- let tree = bibtex::open(source);
- let options = BibtexFormattingOptions {
- line_length: Some(line_length),
- formatter: None,
- };
-
- let mut children = tree.children(tree.root);
- let declaration = children.next().unwrap();
- assert_eq!(children.next(), None);
-
- let actual = format(
- &tree,
- declaration,
- FormattingParams {
- tab_size: 4,
- insert_spaces: true,
- options: &options,
- },
- );
- assert_eq!(actual, expected);
- }
-
- #[test]
- fn wrap_long_lines() {
- let source =
- "@article{foo, bar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit.},}";
- let expected = indoc!(
- "
- @article{foo,
- bar = {Lorem ipsum dolor
- sit amet,
- consectetur
- adipiscing elit.},
- }"
- );
- verify(source, expected, 30);
- }
-
- #[test]
- fn line_length_zero() {
- let source =
- "@article{foo, bar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit.},}";
- let expected = indoc!(
- "
- @article{foo,
- bar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit.},
- }"
- );
- verify(source, expected, 0);
- }
-
- #[test]
- fn trailing_commas() {
- let source = "@article{foo, bar = baz}";
- let expected = indoc!(
- "
- @article{foo,
- bar = baz,
- }"
- );
- verify(source, expected, 30);
- }
-
- #[test]
- fn insert_braces() {
- let source = "@article{foo, bar = baz,";
- let expected = indoc!(
- "
- @article{foo,
- bar = baz,
- }"
- );
- verify(source, expected, 30);
- }
-
- #[test]
- fn commands() {
- let source = "@article{foo, bar = \"\\baz\",}";
- let expected = indoc!(
- "@article{foo,
- bar = \"\\baz\",
- }"
- );
- verify(source, expected, 30);
- }
-
- #[test]
- fn concatenation() {
- let source = "@article{foo, bar = \"baz\" # \"qux\"}";
- let expected = indoc!(
- "
- @article{foo,
- bar = \"baz\" # \"qux\",
- }"
- );
- verify(source, expected, 30);
- }
-
- #[test]
- fn parentheses() {
- let source = "@article(foo,)";
- let expected = indoc!(
- "
- @article{foo,
- }"
- );
- verify(source, expected, 30);
- }
-
- #[test]
- fn string() {
- let source = "@string{foo=\"bar\"}";
- let expected = "@string{foo = \"bar\"}";
- verify(source, expected, 30);
- }
-
- #[test]
- fn preamble() {
- let source = "@preamble{\n\"foo bar baz\"}";
- let expected = "@preamble{\"foo bar baz\"}";
- verify(source, expected, 30);
- }
-}
diff --git a/support/texlab/src/syntax/bibtex/kind.rs b/support/texlab/src/syntax/bibtex/kind.rs
new file mode 100644
index 0000000000..fcb26b69bd
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/kind.rs
@@ -0,0 +1,51 @@
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
+#[allow(non_camel_case_types)]
+#[repr(u16)]
+pub enum SyntaxKind {
+ ERROR = 0,
+ MISSING,
+
+ WHITESPACE,
+ PREAMBLE_TYPE,
+ STRING_TYPE,
+ COMMENT_TYPE,
+ ENTRY_TYPE,
+ WORD,
+ L_CURLY,
+ R_CURLY,
+ L_PAREN,
+ R_PAREN,
+ COMMA,
+ HASH,
+ QUOTE,
+ EQUALITY_SIGN,
+ COMMAND_NAME,
+
+ JUNK,
+ PREAMBLE,
+ STRING,
+ COMMENT,
+ ENTRY,
+ FIELD,
+ VALUE,
+ TOKEN,
+ BRACE_GROUP,
+ QUOTE_GROUP,
+ ROOT,
+}
+
+impl SyntaxKind {
+ pub fn is_type(&self) -> bool {
+ use SyntaxKind::*;
+ matches!(
+ self,
+ PREAMBLE_TYPE | STRING_TYPE | COMMENT_TYPE | ENTRY_TYPE
+ )
+ }
+}
+
+impl From<SyntaxKind> for cstree::SyntaxKind {
+ fn from(kind: SyntaxKind) -> Self {
+ Self(kind as u16)
+ }
+}
diff --git a/support/texlab/src/syntax/bibtex/lexer.rs b/support/texlab/src/syntax/bibtex/lexer.rs
index e089b15864..dff111d645 100644
--- a/support/texlab/src/syntax/bibtex/lexer.rs
+++ b/support/texlab/src/syntax/bibtex/lexer.rs
@@ -1,173 +1,82 @@
-use super::ast::{Token, TokenKind};
-use crate::syntax::text::CharStream;
+use logos::Logos;
-#[derive(Debug)]
-pub struct Lexer<'a> {
- stream: CharStream<'a>,
-}
+use super::kind::SyntaxKind;
-impl<'a> Lexer<'a> {
- pub fn new(text: &'a str) -> Self {
- Self {
- stream: CharStream::new(text),
- }
- }
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord, Logos)]
+#[allow(non_camel_case_types)]
+#[repr(u16)]
+enum LogosToken {
+ #[regex(r"\s+")]
+ WHITESPACE = 2,
- fn kind(&mut self) -> Token {
- fn is_type_char(c: char) -> bool {
- c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'
- }
+ #[regex(r"@[Pp][Rr][Ee][Aa][Mm][Bb][Ll][Ee]")]
+ PREAMBLE_TYPE,
- self.stream.start_span();
- self.stream.next().unwrap();
- while self.stream.satifies(|c| is_type_char(*c)) {
- self.stream.next();
- }
- let span = self.stream.end_span();
- let kind = match span.text.to_lowercase().as_ref() {
- "@preamble" => TokenKind::PreambleKind,
- "@string" => TokenKind::StringKind,
- _ => TokenKind::EntryKind,
- };
- Token::new(span, kind)
- }
+ #[regex(r"@[Ss][Tt][Rr][Ii][Nn][Gg]")]
+ STRING_TYPE,
- fn single_character(&mut self, kind: TokenKind) -> Token {
- self.stream.start_span();
- self.stream.next();
- let span = self.stream.end_span();
- Token::new(span, kind)
- }
+ #[regex(r"@[Cc][Oo][Mm][Mm][Ee][Nn][Tt]")]
+ COMMENT_TYPE,
- fn command(&mut self) -> Token {
- let span = self.stream.command();
- Token::new(span, TokenKind::Command)
- }
+ #[regex(r"@[!\$\&\*\+\-\./:;<>\?@\[\]\\\^_`\|\~a-zA-Z][!\$\&\*\+\-\./:;<>\?@\[\]\\\^_`\|\~a-zA-Z0-9]*|@")]
+ ENTRY_TYPE,
- fn word(&mut self) -> Token {
- fn is_word_char(c: char) -> bool {
- !c.is_whitespace()
- && c != '@'
- && c != '='
- && c != ','
- && c != '#'
- && c != '"'
- && c != '{'
- && c != '}'
- && c != '('
- && c != ')'
- }
+ #[regex(r#"[^\s\{\}\(\),#"=\\]+"#)]
+ #[error]
+ WORD,
- self.stream.start_span();
- while self.stream.satifies(|c| is_word_char(*c)) {
- self.stream.next();
- }
- let span = self.stream.end_span();
- Token::new(span, TokenKind::Word)
- }
-}
+ #[token("{")]
+ L_CURLY,
-impl<'a> Iterator for Lexer<'a> {
- type Item = Token;
-
- fn next(&mut self) -> Option<Token> {
- loop {
- match self.stream.peek() {
- Some('@') => return Some(self.kind()),
- Some('=') => return Some(self.single_character(TokenKind::Assign)),
- Some(',') => return Some(self.single_character(TokenKind::Comma)),
- Some('#') => return Some(self.single_character(TokenKind::Concat)),
- Some('"') => return Some(self.single_character(TokenKind::Quote)),
- Some('{') => return Some(self.single_character(TokenKind::BeginBrace)),
- Some('}') => return Some(self.single_character(TokenKind::EndBrace)),
- Some('(') => return Some(self.single_character(TokenKind::BeginParen)),
- Some(')') => return Some(self.single_character(TokenKind::EndParen)),
- Some('\\') => return Some(self.command()),
- Some(c) => {
- if c.is_whitespace() {
- self.stream.next();
- } else {
- return Some(self.word());
- }
- }
- None => {
- return None;
- }
- }
- }
- }
-}
+ #[token("}")]
+ R_CURLY,
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- protocol::{Position, Range},
- syntax::text::Span,
- };
-
- fn verify<'a>(lexer: &mut Lexer<'a>, line: u64, character: u64, text: &str, kind: TokenKind) {
- let start = Position::new(line, character);
- let end = Position::new(line, character + text.chars().count() as u64);
- let range = Range::new(start, end);
- let span = Span::new(range, text.to_owned());
- let token = Token::new(span, kind);
- assert_eq!(Some(token), lexer.next());
- }
+ #[token("(")]
+ L_PAREN,
- #[test]
- fn word() {
- let mut lexer = Lexer::new("foo bar baz");
- verify(&mut lexer, 0, 0, "foo", TokenKind::Word);
- verify(&mut lexer, 0, 4, "bar", TokenKind::Word);
- verify(&mut lexer, 0, 8, "baz", TokenKind::Word);
- assert_eq!(None, lexer.next());
- }
+ #[token(")")]
+ R_PAREN,
- #[test]
- fn command() {
- let mut lexer = Lexer::new("\\foo\\bar@baz");
- verify(&mut lexer, 0, 0, "\\foo", TokenKind::Command);
- verify(&mut lexer, 0, 4, "\\bar@baz", TokenKind::Command);
- assert_eq!(None, lexer.next());
- }
+ #[token(",")]
+ COMMA,
- #[test]
- fn escape_sequence() {
- let mut lexer = Lexer::new("\\foo*\n\\%\\**");
- verify(&mut lexer, 0, 0, "\\foo*", TokenKind::Command);
- verify(&mut lexer, 1, 0, "\\%", TokenKind::Command);
- verify(&mut lexer, 1, 2, "\\*", TokenKind::Command);
- verify(&mut lexer, 1, 4, "*", TokenKind::Word);
- assert_eq!(None, lexer.next());
- }
+ #[token("#")]
+ HASH,
+
+ #[token("\"")]
+ QUOTE,
+
+ #[token("=")]
+ EQUALITY_SIGN,
- #[test]
- fn delimiter() {
- let mut lexer = Lexer::new("{}()\"");
- verify(&mut lexer, 0, 0, "{", TokenKind::BeginBrace);
- verify(&mut lexer, 0, 1, "}", TokenKind::EndBrace);
- verify(&mut lexer, 0, 2, "(", TokenKind::BeginParen);
- verify(&mut lexer, 0, 3, ")", TokenKind::EndParen);
- verify(&mut lexer, 0, 4, "\"", TokenKind::Quote);
- assert_eq!(None, lexer.next());
+ #[regex(r"\\([^\r\n]|[@a-zA-Z:_]+\*?)?")]
+ COMMAND_NAME,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct Lexer<'a> {
+ tokens: Vec<(SyntaxKind, &'a str)>,
+}
+
+impl<'a> Lexer<'a> {
+ pub fn new(text: &'a str) -> Self {
+ let mut tokens = Vec::new();
+ let mut lexer = LogosToken::lexer(text);
+ while let Some(kind) = lexer.next() {
+ tokens.push((
+ unsafe { std::mem::transmute::<LogosToken, SyntaxKind>(kind) },
+ lexer.slice(),
+ ));
+ }
+ tokens.reverse();
+ Self { tokens }
}
- #[test]
- fn kind() {
- let mut lexer = Lexer::new("@pReAmBlE\n@article\n@string");
- verify(&mut lexer, 0, 0, "@pReAmBlE", TokenKind::PreambleKind);
- verify(&mut lexer, 1, 0, "@article", TokenKind::EntryKind);
- verify(&mut lexer, 2, 0, "@string", TokenKind::StringKind);
- assert_eq!(None, lexer.next());
+ pub fn peek(&self) -> Option<SyntaxKind> {
+ self.tokens.last().map(|(kind, _)| *kind)
}
- #[test]
- fn operator() {
- let mut lexer = Lexer::new("=,#");
- verify(&mut lexer, 0, 0, "=", TokenKind::Assign);
- verify(&mut lexer, 0, 1, ",", TokenKind::Comma);
- verify(&mut lexer, 0, 2, "#", TokenKind::Concat);
- assert_eq!(None, lexer.next());
+ pub fn consume(&mut self) -> Option<(SyntaxKind, &'a str)> {
+ self.tokens.pop()
}
}
diff --git a/support/texlab/src/syntax/bibtex/mod.rs b/support/texlab/src/syntax/bibtex/mod.rs
deleted file mode 100644
index aa7242c1f7..0000000000
--- a/support/texlab/src/syntax/bibtex/mod.rs
+++ /dev/null
@@ -1,396 +0,0 @@
-mod ast;
-mod formatter;
-mod lexer;
-mod parser;
-
-pub use self::{ast::*, formatter::*};
-
-use self::{lexer::Lexer, parser::Parser};
-
-pub fn open(text: &str) -> Tree {
- let lexer = Lexer::new(text);
- let parser = Parser::new(lexer);
- parser.parse()
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- protocol::{Range, RangeExt},
- syntax::text::SyntaxNode,
- };
- use petgraph::graph::NodeIndex;
-
- #[derive(Debug, Default)]
- struct TreeTraversal {
- nodes: Vec<NodeIndex>,
- }
-
- impl<'a> Visitor<'a> for TreeTraversal {
- fn visit(&mut self, tree: &Tree, node: NodeIndex) {
- self.nodes.push(node);
- tree.walk(self, node);
- }
- }
-
- mod range {
- use super::*;
- use indoc::indoc;
-
- fn verify(expected_ranges: Vec<Range>, text: &str) {
- let tree = open(text.trim());
-
- let mut traversal = TreeTraversal::default();
- traversal.visit(&tree, tree.root);
- let actual_ranges: Vec<_> = traversal
- .nodes
- .into_iter()
- .map(|node| tree.graph[node].range())
- .collect();
-
- assert_eq!(actual_ranges, expected_ranges);
- }
-
- #[test]
- fn empty_document() {
- verify(vec![Range::new_simple(0, 0, 0, 0)], "");
- }
-
- #[test]
- fn comment() {
- verify(
- vec![Range::new_simple(0, 0, 0, 3), Range::new_simple(0, 0, 0, 3)],
- "foo",
- );
- }
-
- #[test]
- fn preamble_no_left() {
- verify(
- vec![Range::new_simple(0, 0, 0, 9), Range::new_simple(0, 0, 0, 9)],
- "@preamble",
- );
- }
-
- #[test]
- fn preamble_no_content() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 10),
- Range::new_simple(0, 0, 0, 10),
- ],
- "@preamble{",
- );
- }
-
- #[test]
- fn preamble_no_right() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 15),
- Range::new_simple(0, 0, 0, 15),
- Range::new_simple(0, 10, 0, 15),
- Range::new_simple(0, 11, 0, 14),
- ],
- r#"@preamble{"foo""#,
- );
- }
-
- #[test]
- fn preamble_complete() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 16),
- Range::new_simple(0, 0, 0, 16),
- Range::new_simple(0, 10, 0, 15),
- Range::new_simple(0, 11, 0, 14),
- ],
- r#"@preamble{"foo"}"#,
- );
- }
-
- #[test]
- fn string_no_left() {
- verify(
- vec![Range::new_simple(0, 0, 0, 7), Range::new_simple(0, 0, 0, 7)],
- r#"@string"#,
- );
- }
-
- #[test]
- fn string_no_name() {
- verify(
- vec![Range::new_simple(0, 0, 0, 8), Range::new_simple(0, 0, 0, 8)],
- r#"@string{"#,
- );
- }
-
- #[test]
- fn string_no_assign() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 11),
- Range::new_simple(0, 0, 0, 11),
- ],
- r#"@string{foo"#,
- );
- }
-
- #[test]
- fn string_no_value() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 13),
- Range::new_simple(0, 0, 0, 13),
- ],
- r#"@string{foo ="#,
- );
- }
-
- #[test]
- fn string_no_right() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 19),
- Range::new_simple(0, 0, 0, 19),
- Range::new_simple(0, 14, 0, 19),
- Range::new_simple(0, 15, 0, 18),
- ],
- r#"@string{foo = "bar""#,
- );
- }
-
- #[test]
- fn string_complete() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 20),
- Range::new_simple(0, 0, 0, 20),
- Range::new_simple(0, 14, 0, 19),
- Range::new_simple(0, 15, 0, 18),
- ],
- r#"@string{foo = "bar"}"#,
- );
- }
-
- #[test]
- fn entry_no_left() {
- verify(
- vec![Range::new_simple(0, 0, 0, 8), Range::new_simple(0, 0, 0, 8)],
- r#"@article"#,
- );
- }
-
- #[test]
- fn entry_no_key() {
- verify(
- vec![Range::new_simple(0, 0, 0, 9), Range::new_simple(0, 0, 0, 9)],
- r#"@article{"#,
- );
- }
-
- #[test]
- fn entry_no_comma() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 12),
- Range::new_simple(0, 0, 0, 12),
- ],
- r#"@article{foo"#,
- );
- }
-
- #[test]
- fn entry_no_right() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 13),
- Range::new_simple(0, 0, 0, 13),
- ],
- r#"@article{foo,"#,
- );
- }
-
- #[test]
- fn entry_parentheses() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 14),
- Range::new_simple(0, 0, 0, 14),
- ],
- r#"@article(foo,)"#,
- );
- }
-
- #[test]
- fn field_no_assign() {
- verify(
- vec![
- Range::new_simple(0, 0, 1, 10),
- Range::new_simple(0, 0, 1, 10),
- Range::new_simple(1, 4, 1, 10),
- ],
- indoc!(
- r#"
- @article{foo,
- author
- "#
- ),
- );
- }
-
- #[test]
- fn field_no_value() {
- verify(
- vec![
- Range::new_simple(0, 0, 1, 12),
- Range::new_simple(0, 0, 1, 12),
- Range::new_simple(1, 4, 1, 12),
- ],
- indoc!(
- r#"
- @article{foo,
- author =
- "#
- ),
- );
- }
-
- #[test]
- fn field_no_comma() {
- verify(
- vec![
- Range::new_simple(0, 0, 1, 16),
- Range::new_simple(0, 0, 1, 16),
- Range::new_simple(1, 4, 1, 16),
- Range::new_simple(1, 13, 1, 16),
- ],
- indoc!(
- r#"
- @article{foo,
- author = bar
- "#
- ),
- );
- }
-
- #[test]
- fn field_complete() {
- verify(
- vec![
- Range::new_simple(0, 0, 2, 1),
- Range::new_simple(0, 0, 2, 1),
- Range::new_simple(1, 4, 1, 17),
- Range::new_simple(1, 13, 1, 16),
- ],
- indoc!(
- r#"
- @article{foo,
- author = bar,
- }
- "#
- ),
- );
- }
-
- #[test]
- fn entry_two_fields() {
- verify(
- vec![
- Range::new_simple(0, 0, 3, 1),
- Range::new_simple(0, 0, 3, 1),
- Range::new_simple(1, 4, 1, 17),
- Range::new_simple(1, 13, 1, 16),
- Range::new_simple(2, 4, 2, 16),
- Range::new_simple(2, 12, 2, 15),
- ],
- indoc!(
- r#"
- @article{foo,
- author = bar,
- title = baz,
- }
- "#
- ),
- );
- }
-
- #[test]
- fn quoted_content_no_children() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 12),
- Range::new_simple(0, 0, 0, 12),
- Range::new_simple(0, 10, 0, 11),
- ],
- r#"@preamble{"}"#,
- );
- }
-
- #[test]
- fn quoted_content_no_right() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 16),
- Range::new_simple(0, 0, 0, 16),
- Range::new_simple(0, 10, 0, 15),
- Range::new_simple(0, 11, 0, 15),
- ],
- r#"@preamble{"word}"#,
- );
- }
-
- #[test]
- fn braced_content_no_children() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 11),
- Range::new_simple(0, 0, 0, 11),
- Range::new_simple(0, 10, 0, 11),
- ],
- r#"@preamble{{"#,
- );
- }
-
- #[test]
- fn braced_content_no_right() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 15),
- Range::new_simple(0, 0, 0, 15),
- Range::new_simple(0, 10, 0, 15),
- Range::new_simple(0, 11, 0, 15),
- ],
- r#"@preamble{{word"#,
- );
- }
-
- #[test]
- fn concat_no_right() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 16),
- Range::new_simple(0, 0, 0, 16),
- Range::new_simple(0, 10, 0, 15),
- Range::new_simple(0, 10, 0, 13),
- ],
- r#"@preamble{foo #}"#,
- );
- }
-
- #[test]
- fn concat_complete() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 20),
- Range::new_simple(0, 0, 0, 20),
- Range::new_simple(0, 10, 0, 19),
- Range::new_simple(0, 10, 0, 13),
- Range::new_simple(0, 16, 0, 19),
- ],
- r#"@preamble{foo # bar}"#,
- );
- }
- }
-}
diff --git a/support/texlab/src/syntax/bibtex/parser.rs b/support/texlab/src/syntax/bibtex/parser.rs
index b23903aeb3..ffdb1c94a0 100644
--- a/support/texlab/src/syntax/bibtex/parser.rs
+++ b/support/texlab/src/syntax/bibtex/parser.rs
@@ -1,401 +1,413 @@
-use super::ast::*;
-use crate::{
- protocol::{Range, RangeExt},
- syntax::text::SyntaxNode,
+use cstree::GreenNodeBuilder;
+
+use super::{
+ lexer::Lexer,
+ SyntaxKind::{self, *},
+ SyntaxNode,
};
-use petgraph::graph::{Graph, NodeIndex};
-use std::iter::Peekable;
-pub struct Parser<I: Iterator<Item = Token>> {
- graph: Graph<Node, ()>,
- tokens: Peekable<I>,
+#[derive(Debug, Clone)]
+pub struct Parse {
+ pub root: SyntaxNode,
+}
+
+struct Parser<'a> {
+ lexer: Lexer<'a>,
+ builder: GreenNodeBuilder<'static, 'static>,
}
-impl<I: Iterator<Item = Token>> Parser<I> {
- pub fn new(tokens: I) -> Self {
+impl<'a> Parser<'a> {
+ pub fn new(lexer: Lexer<'a>) -> Self {
Self {
- tokens: tokens.peekable(),
- graph: Graph::new(),
+ lexer,
+ builder: GreenNodeBuilder::new(),
}
}
- pub fn parse(mut self) -> Tree {
- let mut children = Vec::new();
+ fn peek(&self) -> Option<SyntaxKind> {
+ self.lexer.peek()
+ }
- while let Some(ref token) = self.tokens.peek() {
- match token.kind {
- TokenKind::PreambleKind => children.push(self.preamble()),
- TokenKind::StringKind => children.push(self.string()),
- TokenKind::EntryKind => children.push(self.entry()),
- _ => children.push(self.comment()),
- }
- }
+ fn eat(&mut self) {
+ let (kind, text) = self.lexer.consume().unwrap();
+ self.builder.token(kind.into(), text);
+ }
- let range = if children.is_empty() {
- Range::new_simple(0, 0, 0, 0)
+ fn expect(&mut self, kind: SyntaxKind) {
+ if self.peek() == Some(kind) {
+ self.eat();
+ self.trivia();
} else {
- let start = self.graph[children[0]].start();
- let end = self.graph[children[children.len() - 1]].end();
- Range::new(start, end)
- };
+ self.builder.token(MISSING.into(), "");
+ }
+ }
- let root = self.graph.add_node(Node::Root(Root { range }));
- self.connect(root, &children);
- Tree {
- graph: self.graph,
- root,
+ pub fn parse(mut self) -> Parse {
+ self.builder.start_node(ROOT.into());
+ while let Some(kind) = self.peek() {
+ match kind {
+ PREAMBLE_TYPE => self.preamble(),
+ STRING_TYPE => self.string(),
+ COMMENT_TYPE => self.comment(),
+ ENTRY_TYPE => self.entry(),
+ _ => self.junk(),
+ }
+ }
+ self.builder.finish_node();
+ let (green, resolver) = self.builder.finish();
+ Parse {
+ root: SyntaxNode::new_root_with_resolver(green, resolver.unwrap()),
}
}
- fn preamble(&mut self) -> NodeIndex {
- let ty = self.tokens.next().unwrap();
+ fn trivia(&mut self) {
+ while self.peek() == Some(WHITESPACE) {
+ self.eat();
+ }
+ }
- let left = self.expect2(TokenKind::BeginBrace, TokenKind::BeginParen);
- if left.is_none() {
- return self.graph.add_node(Node::Preamble(Box::new(Preamble {
- range: ty.range(),
- ty,
- left: None,
- right: None,
- })));
+ fn junk(&mut self) {
+ self.builder.start_node(JUNK.into());
+ while self
+ .lexer
+ .peek()
+ .filter(|&kind| {
+ !matches!(
+ kind,
+ PREAMBLE_TYPE | STRING_TYPE | COMMENT_TYPE | ENTRY_TYPE,
+ )
+ })
+ .is_some()
+ {
+ self.eat();
}
+ self.builder.finish_node();
+ }
- if !self.can_match_content() {
- return self.graph.add_node(Node::Preamble(Box::new(Preamble {
- range: Range::new(ty.start(), left.as_ref().unwrap().end()),
- ty,
- left,
- right: None,
- })));
+ fn left_delimiter_or_missing(&mut self) {
+ if self
+ .lexer
+ .peek()
+ .filter(|&kind| matches!(kind, L_CURLY | L_PAREN))
+ .is_some()
+ {
+ self.eat();
+ self.trivia();
+ } else {
+ self.builder.token(MISSING.into(), "");
}
+ }
- let content = self.content();
-
- let right = self.expect2(TokenKind::EndBrace, TokenKind::EndParen);
- let end = right
- .as_ref()
- .map(Token::end)
- .unwrap_or_else(|| self.graph[content].end());
-
- let parent = self.graph.add_node(Node::Preamble(Box::new(Preamble {
- range: Range::new(ty.start(), end),
- ty,
- left,
- right,
- })));
- self.graph.add_edge(parent, content, ());
- parent
- }
-
- fn string(&mut self) -> NodeIndex {
- let ty = self.tokens.next().unwrap();
-
- let left = self.expect2(TokenKind::BeginBrace, TokenKind::BeginParen);
- if left.is_none() {
- return self.graph.add_node(Node::String(Box::new(String {
- range: ty.range(),
- ty,
- left: None,
- name: None,
- assign: None,
- right: None,
- })));
+ fn right_delimiter_or_missing(&mut self) {
+ if self
+ .lexer
+ .peek()
+ .filter(|&kind| matches!(kind, R_CURLY | R_PAREN))
+ .is_some()
+ {
+ self.eat();
+ self.trivia();
+ } else {
+ self.builder.token(MISSING.into(), "");
}
+ }
- let name = self.expect1(TokenKind::Word);
- if name.is_none() {
- return self.graph.add_node(Node::String(Box::new(String {
- range: Range::new(ty.start(), left.as_ref().unwrap().end()),
- ty,
- left,
- name: None,
- assign: None,
- right: None,
- })));
+ fn value_or_missing(&mut self) {
+ if self
+ .lexer
+ .peek()
+ .filter(|&kind| matches!(kind, L_CURLY | QUOTE | WORD))
+ .is_some()
+ {
+ self.value();
+ } else {
+ self.builder.token(MISSING.into(), "");
}
+ }
+
+ fn preamble(&mut self) {
+ self.builder.start_node(PREAMBLE.into());
+ self.eat();
+ self.trivia();
+ self.left_delimiter_or_missing();
+ self.value_or_missing();
+ self.right_delimiter_or_missing();
+ self.builder.finish_node();
+ }
- let assign = self.expect1(TokenKind::Assign);
- if assign.is_none() {
- return self.graph.add_node(Node::String(Box::new(String {
- range: Range::new(ty.start(), name.as_ref().unwrap().end()),
- ty,
- left,
- name,
- assign: None,
- right: None,
- })));
+ fn string(&mut self) {
+ self.builder.start_node(STRING.into());
+ self.eat();
+ self.trivia();
+ self.left_delimiter_or_missing();
+
+ if self.peek() != Some(WORD) {
+ self.builder.token(MISSING.into(), "");
+ self.builder.finish_node();
+ return;
}
+ self.eat();
+ self.trivia();
+ self.expect(EQUALITY_SIGN);
+
+ self.value_or_missing();
+
+ self.right_delimiter_or_missing();
+ self.builder.finish_node();
+ }
+
+ fn comment(&mut self) {
+ self.builder.start_node(COMMENT.into());
+ self.eat();
+ self.builder.finish_node();
+ }
- if !self.can_match_content() {
- return self.graph.add_node(Node::String(Box::new(String {
- range: Range::new(ty.start(), assign.as_ref().unwrap().end()),
- ty,
- left,
- name,
- assign,
- right: None,
- })));
+ fn entry(&mut self) {
+ self.builder.start_node(ENTRY.into());
+ self.eat();
+ self.trivia();
+
+ self.left_delimiter_or_missing();
+
+ if self.peek() != Some(WORD) {
+ self.builder.token(MISSING.into(), "");
+ self.builder.finish_node();
+ return;
}
- let value = self.content();
-
- let right = self.expect2(TokenKind::EndBrace, TokenKind::EndParen);
- let end = right
- .as_ref()
- .map(Token::end)
- .unwrap_or_else(|| self.graph[value].end());
-
- let parent = self.graph.add_node(Node::String(Box::new(String {
- range: Range::new(ty.start(), end),
- ty,
- left,
- name,
- assign,
- right,
- })));
- self.graph.add_edge(parent, value, ());
- parent
- }
-
- fn entry(&mut self) -> NodeIndex {
- let ty = self.tokens.next().unwrap();
-
- let left = self.expect2(TokenKind::BeginBrace, TokenKind::BeginParen);
- if left.is_none() {
- return self.graph.add_node(Node::Entry(Box::new(Entry {
- range: ty.range(),
- ty,
- left: None,
- key: None,
- comma: None,
- right: None,
- })));
+ self.eat();
+
+ while let Some(kind) = self.peek() {
+ match kind {
+ WHITESPACE => self.eat(),
+ WORD => self.field(),
+ COMMA => self.eat(),
+ _ => break,
+ };
}
- let key = self.expect1(TokenKind::Word);
- if key.is_none() {
- return self.graph.add_node(Node::Entry(Box::new(Entry {
- range: Range::new(ty.start(), left.as_ref().unwrap().end()),
- ty,
- left,
- key: None,
- comma: None,
- right: None,
- })));
- }
+ self.right_delimiter_or_missing();
- let comma = self.expect1(TokenKind::Comma);
- if comma.is_none() {
- return self.graph.add_node(Node::Entry(Box::new(Entry {
- range: Range::new(ty.start(), key.as_ref().unwrap().end()),
- ty,
- left,
- key,
- comma: None,
- right: None,
- })));
- }
+ self.builder.finish_node();
+ }
- let mut fields = Vec::new();
- while self.next_of_kind(TokenKind::Word) {
- fields.push(self.field());
- }
+ fn field(&mut self) {
+ self.builder.start_node(FIELD.into());
+ self.eat();
+ self.trivia();
- let right = self.expect2(TokenKind::EndBrace, TokenKind::EndParen);
-
- let end = right
- .as_ref()
- .map(Token::end)
- .or_else(|| fields.last().map(|field| self.graph[*field].end()))
- .unwrap_or_else(|| comma.as_ref().unwrap().end());
- let parent = self.graph.add_node(Node::Entry(Box::new(Entry {
- range: Range::new(ty.start(), end),
- ty,
- left,
- key,
- comma,
- right,
- })));
- self.connect(parent, &fields);
- parent
- }
-
- fn comment(&mut self) -> NodeIndex {
- let token = self.tokens.next().unwrap();
- self.graph.add_node(Node::Comment(Comment { token }))
- }
-
- fn field(&mut self) -> NodeIndex {
- let name = self.tokens.next().unwrap();
-
- let assign = self.expect1(TokenKind::Assign);
- if assign.is_none() {
- return self.graph.add_node(Node::Field(Box::new(Field {
- range: name.range(),
- name,
- assign: None,
- comma: None,
- })));
+ if self.peek() == Some(EQUALITY_SIGN) {
+ self.eat();
+ self.trivia();
+ } else {
+ self.builder.token(MISSING.into(), "");
}
- if !self.can_match_content() {
- return self.graph.add_node(Node::Field(Box::new(Field {
- range: Range::new(name.start(), assign.as_ref().unwrap().end()),
- name,
- assign,
- comma: None,
- })));
+ if self
+ .lexer
+ .peek()
+ .filter(|&kind| matches!(kind, L_CURLY | QUOTE | WORD))
+ .is_some()
+ {
+ self.value();
+ } else {
+ self.builder.token(MISSING.into(), "");
}
- let content = self.content();
-
- let comma = self.expect1(TokenKind::Comma);
-
- let end = comma
- .as_ref()
- .map(Token::end)
- .unwrap_or_else(|| self.graph[content].end());
- let parent = self.graph.add_node(Node::Field(Box::new(Field {
- range: Range::new(name.start(), end),
- name,
- assign,
- comma,
- })));
- self.graph.add_edge(parent, content, ());
- parent
- }
-
- fn content(&mut self) -> NodeIndex {
- let token = self.tokens.next().unwrap();
- let left = match token.kind {
- TokenKind::PreambleKind
- | TokenKind::StringKind
- | TokenKind::EntryKind
- | TokenKind::Word
- | TokenKind::Assign
- | TokenKind::Comma
- | TokenKind::BeginParen
- | TokenKind::EndParen => self.graph.add_node(Node::Word(Word { token })),
- TokenKind::Command => self.graph.add_node(Node::Command(Command { token })),
- TokenKind::Quote => {
- let mut children = Vec::new();
- while self.can_match_content() {
- if self.next_of_kind(TokenKind::Quote) {
- break;
- }
- children.push(self.content());
- }
- let right = self.expect1(TokenKind::Quote);
-
- let end = right
- .as_ref()
- .map(Token::end)
- .or_else(|| children.last().map(|child| self.graph[*child].end()))
- .unwrap_or_else(|| token.end());
- let parent = self.graph.add_node(Node::QuotedContent(QuotedContent {
- range: Range::new(token.start(), end),
- left: token,
- right,
- }));
- self.connect(parent, &children);
- parent
- }
- TokenKind::BeginBrace => {
- let mut children = Vec::new();
- while self.can_match_content() {
- children.push(self.content());
- }
- let right = self.expect1(TokenKind::EndBrace);
-
- let end = right
- .as_ref()
- .map(Token::end)
- .or_else(|| children.last().map(|child| self.graph[*child].end()))
- .unwrap_or_else(|| token.end());
- let parent = self.graph.add_node(Node::BracedContent(BracedContent {
- range: Range::new(token.start(), end),
- left: token,
- right,
- }));
- self.connect(parent, &children);
- parent
+ self.builder.finish_node();
+ }
+
+ fn value(&mut self) {
+ self.builder.start_node(VALUE.into());
+ self.token();
+ while let Some(kind) = self.peek() {
+ match kind {
+ WHITESPACE => self.eat(),
+ L_CURLY | QUOTE | WORD => self.token(),
+ HASH => self.eat(),
+ _ => break,
}
+ }
+ self.builder.finish_node();
+ }
+
+ fn token(&mut self) {
+ self.builder.start_node(TOKEN.into());
+ match self.peek().unwrap() {
+ L_CURLY => self.brace_group(),
+ QUOTE => self.quote_group(),
+ WORD => self.eat(),
_ => unreachable!(),
};
+ self.builder.finish_node();
+ }
- match self.expect1(TokenKind::Concat) {
- Some(operator) => {
- if self.can_match_content() {
- let right = self.content();
- let parent = self.graph.add_node(Node::Concat(Concat {
- range: Range::new(self.graph[left].start(), self.graph[right].end()),
- operator,
- }));
- self.graph.add_edge(parent, left, ());
- self.graph.add_edge(parent, right, ());
- parent
- } else {
- let parent = self.graph.add_node(Node::Concat(Concat {
- range: Range::new(self.graph[left].start(), operator.end()),
- operator,
- }));
- self.graph.add_edge(parent, left, ());
- parent
- }
- }
- None => left,
+ fn brace_group(&mut self) {
+ self.builder.start_node(BRACE_GROUP.into());
+ self.eat();
+
+ while let Some(kind) = self.peek() {
+ match kind {
+ WHITESPACE => self.eat(),
+ PREAMBLE_TYPE => break,
+ STRING_TYPE => break,
+ COMMENT_TYPE => break,
+ ENTRY_TYPE => break,
+ WORD => self.eat(),
+ L_CURLY => self.brace_group(),
+ R_CURLY => break,
+ L_PAREN => self.eat(),
+ R_PAREN => self.eat(),
+ COMMA => self.eat(),
+ HASH => self.eat(),
+ QUOTE => self.eat(),
+ EQUALITY_SIGN => self.eat(),
+ COMMAND_NAME => self.eat(),
+ _ => unreachable!(),
+ };
}
+
+ self.expect(R_CURLY);
+
+ self.builder.finish_node();
}
- fn connect(&mut self, parent: NodeIndex, children: &[NodeIndex]) {
- for child in children {
- self.graph.add_edge(parent, *child, ());
+ fn quote_group(&mut self) {
+ self.builder.start_node(QUOTE_GROUP.into());
+ self.eat();
+
+ while let Some(kind) = self.peek() {
+ match kind {
+ WHITESPACE => self.eat(),
+ PREAMBLE_TYPE => break,
+ STRING_TYPE => break,
+ COMMENT_TYPE => break,
+ ENTRY_TYPE => break,
+ WORD => self.eat(),
+ L_CURLY => self.brace_group(),
+ R_CURLY => break,
+ L_PAREN => self.eat(),
+ R_PAREN => self.eat(),
+ COMMA => self.eat(),
+ HASH => self.eat(),
+ QUOTE => break,
+ EQUALITY_SIGN => self.eat(),
+ COMMAND_NAME => self.eat(),
+ _ => unreachable!(),
+ };
}
+
+ self.expect(QUOTE);
+ self.builder.finish_node();
}
+}
- fn can_match_content(&mut self) -> bool {
- if let Some(ref token) = self.tokens.peek() {
- match token.kind {
- TokenKind::PreambleKind
- | TokenKind::StringKind
- | TokenKind::EntryKind
- | TokenKind::Word
- | TokenKind::Command
- | TokenKind::Assign
- | TokenKind::Comma
- | TokenKind::Quote
- | TokenKind::BeginBrace
- | TokenKind::BeginParen
- | TokenKind::EndParen => true,
- TokenKind::Concat | TokenKind::EndBrace => false,
- }
- } else {
- false
- }
+pub fn parse(text: &str) -> Parse {
+ Parser::new(Lexer::new(text)).parse()
+}
+
+#[cfg(test)]
+mod tests {
+ use insta::assert_debug_snapshot;
+
+ use super::*;
+
+ fn setup(text: &str) -> SyntaxNode {
+ parse(&text.trim().replace("\r", "")).root
}
- fn expect1(&mut self, kind: TokenKind) -> Option<Token> {
- if let Some(ref token) = self.tokens.peek() {
- if token.kind == kind {
- return self.tokens.next();
- }
- }
- None
+ #[test]
+ fn test_empty() {
+ assert_debug_snapshot!(setup(r#""#));
}
- fn expect2(&mut self, kind1: TokenKind, kind2: TokenKind) -> Option<Token> {
- if let Some(ref token) = self.tokens.peek() {
- if token.kind == kind1 || token.kind == kind2 {
- return self.tokens.next();
- }
- }
- None
+ #[test]
+ fn test_junk() {
+ assert_debug_snapshot!(setup(r#"Hello World!"#));
}
- fn next_of_kind(&mut self, kind: TokenKind) -> bool {
- if let Some(token) = self.tokens.peek() {
- token.kind == kind
- } else {
- false
- }
+ #[test]
+ fn test_preamble_complete() {
+ assert_debug_snapshot!(setup(r#"@preamble{ "Hello World" }"#));
+ }
+
+ #[test]
+ fn test_preamble_missing_end() {
+ assert_debug_snapshot!(setup(r#"@preamble{ "Hello World" "#));
+ }
+
+ #[test]
+ fn test_preamble_casing() {
+ assert_debug_snapshot!(setup(r#"@preAmbLe{ "Hello World" }"#));
+ }
+
+ #[test]
+ fn test_string_complete() {
+ assert_debug_snapshot!(setup(r#"@string{foo = {Hello World}}"#));
+ }
+
+ #[test]
+ fn test_string_incomplete() {
+ assert_debug_snapshot!(setup(r#"@string{foo = {Hello World}"#));
+ }
+
+ #[test]
+ fn test_string_concatenation() {
+ assert_debug_snapshot!(setup(
+ r#"@string{foo = {Hello World}} @string{bar = foo # "!"}"#
+ ));
+ }
+
+ #[test]
+ fn test_string_casing() {
+ assert_debug_snapshot!(setup(r#"@STRING{foo = "Hello World"}"#));
+ }
+
+ #[test]
+ fn test_string_missing_quote() {
+ assert_debug_snapshot!(setup(r#"@STRING{foo = "Hello World}"#));
+ }
+
+ #[test]
+ fn test_entry_no_fields() {
+ assert_debug_snapshot!(setup(r#"@article{foo,}"#));
+ }
+
+ #[test]
+ fn test_entry_no_fields_missing_comma() {
+ assert_debug_snapshot!(setup(r#"@article{foo}"#));
+ }
+
+ #[test]
+ fn test_entry_one_field() {
+ assert_debug_snapshot!(setup(r#"@article{foo, author = {Foo Bar}}"#));
+ }
+
+ #[test]
+ fn test_entry_one_field_number_key() {
+ assert_debug_snapshot!(setup(r#"@article{foo2021, author = {Foo Bar}}"#));
+ }
+
+ #[test]
+ fn test_entry_one_field_trailing_comma() {
+ assert_debug_snapshot!(setup(r#"@article{foo, author = {Foo Bar},}"#));
+ }
+
+ #[test]
+ fn test_entry_two_fields() {
+ assert_debug_snapshot!(setup(
+ r#"@article{foo, author = {Foo Bar}, title = {Hello World}}"#
+ ));
+ }
+
+ #[test]
+ fn test_entry_two_fields_incomplete() {
+ assert_debug_snapshot!(setup(r#"@article{foo, author = {Foo Bar}, t}"#));
+ }
+
+ #[test]
+ fn test_entry_complete_parens() {
+ assert_debug_snapshot!(setup(
+ r#"@article(foo, author = {Foo Bar}, title = {Hello})"#
+ ));
}
}
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__empty.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__empty.snap
new file mode 100644
index 0000000000..55e1954d2f
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__empty.snap
@@ -0,0 +1,7 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"\"#)"
+
+---
+ROOT@0..0
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_complete_parens.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_complete_parens.snap
new file mode 100644
index 0000000000..cfd469f161
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_complete_parens.snap
@@ -0,0 +1,40 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@article(foo, author = {Foo Bar}, title = {Hello})\"#)"
+
+---
+ROOT@0..50
+ ENTRY@0..50
+ ENTRY_TYPE@0..8 "@article"
+ L_PAREN@8..9 "("
+ WORD@9..12 "foo"
+ COMMA@12..13 ","
+ WHITESPACE@13..14 " "
+ FIELD@14..32
+ WORD@14..20 "author"
+ WHITESPACE@20..21 " "
+ EQUALITY_SIGN@21..22 "="
+ WHITESPACE@22..23 " "
+ VALUE@23..32
+ TOKEN@23..32
+ BRACE_GROUP@23..32
+ L_CURLY@23..24 "{"
+ WORD@24..27 "Foo"
+ WHITESPACE@27..28 " "
+ WORD@28..31 "Bar"
+ R_CURLY@31..32 "}"
+ COMMA@32..33 ","
+ WHITESPACE@33..34 " "
+ FIELD@34..49
+ WORD@34..39 "title"
+ WHITESPACE@39..40 " "
+ EQUALITY_SIGN@40..41 "="
+ WHITESPACE@41..42 " "
+ VALUE@42..49
+ TOKEN@42..49
+ BRACE_GROUP@42..49
+ L_CURLY@42..43 "{"
+ WORD@43..48 "Hello"
+ R_CURLY@48..49 "}"
+ R_PAREN@49..50 ")"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_no_fields.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_no_fields.snap
new file mode 100644
index 0000000000..a98df7e382
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_no_fields.snap
@@ -0,0 +1,13 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@article{foo,}\"#)"
+
+---
+ROOT@0..14
+ ENTRY@0..14
+ ENTRY_TYPE@0..8 "@article"
+ L_CURLY@8..9 "{"
+ WORD@9..12 "foo"
+ COMMA@12..13 ","
+ R_CURLY@13..14 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_no_fields_missing_comma.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_no_fields_missing_comma.snap
new file mode 100644
index 0000000000..b83037aae0
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_no_fields_missing_comma.snap
@@ -0,0 +1,12 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@article{foo}\"#)"
+
+---
+ROOT@0..13
+ ENTRY@0..13
+ ENTRY_TYPE@0..8 "@article"
+ L_CURLY@8..9 "{"
+ WORD@9..12 "foo"
+ R_CURLY@12..13 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field.snap
new file mode 100644
index 0000000000..704cbbbd47
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field.snap
@@ -0,0 +1,27 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@article{foo, author = {Foo Bar}}\"#)"
+
+---
+ROOT@0..33
+ ENTRY@0..33
+ ENTRY_TYPE@0..8 "@article"
+ L_CURLY@8..9 "{"
+ WORD@9..12 "foo"
+ COMMA@12..13 ","
+ WHITESPACE@13..14 " "
+ FIELD@14..32
+ WORD@14..20 "author"
+ WHITESPACE@20..21 " "
+ EQUALITY_SIGN@21..22 "="
+ WHITESPACE@22..23 " "
+ VALUE@23..32
+ TOKEN@23..32
+ BRACE_GROUP@23..32
+ L_CURLY@23..24 "{"
+ WORD@24..27 "Foo"
+ WHITESPACE@27..28 " "
+ WORD@28..31 "Bar"
+ R_CURLY@31..32 "}"
+ R_CURLY@32..33 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field_number_key.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field_number_key.snap
new file mode 100644
index 0000000000..8f00c5ee1c
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field_number_key.snap
@@ -0,0 +1,27 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@article{foo2021, author = {Foo Bar}}\"#)"
+
+---
+ROOT@0..37
+ ENTRY@0..37
+ ENTRY_TYPE@0..8 "@article"
+ L_CURLY@8..9 "{"
+ WORD@9..16 "foo2021"
+ COMMA@16..17 ","
+ WHITESPACE@17..18 " "
+ FIELD@18..36
+ WORD@18..24 "author"
+ WHITESPACE@24..25 " "
+ EQUALITY_SIGN@25..26 "="
+ WHITESPACE@26..27 " "
+ VALUE@27..36
+ TOKEN@27..36
+ BRACE_GROUP@27..36
+ L_CURLY@27..28 "{"
+ WORD@28..31 "Foo"
+ WHITESPACE@31..32 " "
+ WORD@32..35 "Bar"
+ R_CURLY@35..36 "}"
+ R_CURLY@36..37 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field_trailing_comma.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field_trailing_comma.snap
new file mode 100644
index 0000000000..af8b1593df
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field_trailing_comma.snap
@@ -0,0 +1,28 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@article{foo, author = {Foo Bar},}\"#)"
+
+---
+ROOT@0..34
+ ENTRY@0..34
+ ENTRY_TYPE@0..8 "@article"
+ L_CURLY@8..9 "{"
+ WORD@9..12 "foo"
+ COMMA@12..13 ","
+ WHITESPACE@13..14 " "
+ FIELD@14..32
+ WORD@14..20 "author"
+ WHITESPACE@20..21 " "
+ EQUALITY_SIGN@21..22 "="
+ WHITESPACE@22..23 " "
+ VALUE@23..32
+ TOKEN@23..32
+ BRACE_GROUP@23..32
+ L_CURLY@23..24 "{"
+ WORD@24..27 "Foo"
+ WHITESPACE@27..28 " "
+ WORD@28..31 "Bar"
+ R_CURLY@31..32 "}"
+ COMMA@32..33 ","
+ R_CURLY@33..34 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_two_fields.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_two_fields.snap
new file mode 100644
index 0000000000..f47b3b6917
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_two_fields.snap
@@ -0,0 +1,42 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@article{foo, author = {Foo Bar}, title = {Hello World}}\"#)"
+
+---
+ROOT@0..56
+ ENTRY@0..56
+ ENTRY_TYPE@0..8 "@article"
+ L_CURLY@8..9 "{"
+ WORD@9..12 "foo"
+ COMMA@12..13 ","
+ WHITESPACE@13..14 " "
+ FIELD@14..32
+ WORD@14..20 "author"
+ WHITESPACE@20..21 " "
+ EQUALITY_SIGN@21..22 "="
+ WHITESPACE@22..23 " "
+ VALUE@23..32
+ TOKEN@23..32
+ BRACE_GROUP@23..32
+ L_CURLY@23..24 "{"
+ WORD@24..27 "Foo"
+ WHITESPACE@27..28 " "
+ WORD@28..31 "Bar"
+ R_CURLY@31..32 "}"
+ COMMA@32..33 ","
+ WHITESPACE@33..34 " "
+ FIELD@34..55
+ WORD@34..39 "title"
+ WHITESPACE@39..40 " "
+ EQUALITY_SIGN@40..41 "="
+ WHITESPACE@41..42 " "
+ VALUE@42..55
+ TOKEN@42..55
+ BRACE_GROUP@42..55
+ L_CURLY@42..43 "{"
+ WORD@43..48 "Hello"
+ WHITESPACE@48..49 " "
+ WORD@49..54 "World"
+ R_CURLY@54..55 "}"
+ R_CURLY@55..56 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_two_fields_incomplete.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_two_fields_incomplete.snap
new file mode 100644
index 0000000000..34a9019582
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_two_fields_incomplete.snap
@@ -0,0 +1,33 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@article{foo, author = {Foo Bar}, t}\"#)"
+
+---
+ROOT@0..36
+ ENTRY@0..36
+ ENTRY_TYPE@0..8 "@article"
+ L_CURLY@8..9 "{"
+ WORD@9..12 "foo"
+ COMMA@12..13 ","
+ WHITESPACE@13..14 " "
+ FIELD@14..32
+ WORD@14..20 "author"
+ WHITESPACE@20..21 " "
+ EQUALITY_SIGN@21..22 "="
+ WHITESPACE@22..23 " "
+ VALUE@23..32
+ TOKEN@23..32
+ BRACE_GROUP@23..32
+ L_CURLY@23..24 "{"
+ WORD@24..27 "Foo"
+ WHITESPACE@27..28 " "
+ WORD@28..31 "Bar"
+ R_CURLY@31..32 "}"
+ COMMA@32..33 ","
+ WHITESPACE@33..34 " "
+ FIELD@34..35
+ WORD@34..35 "t"
+ MISSING@35..35 ""
+ MISSING@35..35 ""
+ R_CURLY@35..36 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__junk.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__junk.snap
new file mode 100644
index 0000000000..e77313cabf
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__junk.snap
@@ -0,0 +1,11 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"Hello World!\"#)"
+
+---
+ROOT@0..12
+ JUNK@0..12
+ WORD@0..5 "Hello"
+ WHITESPACE@5..6 " "
+ WORD@6..12 "World!"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_casing.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_casing.snap
new file mode 100644
index 0000000000..f4669b8a01
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_casing.snap
@@ -0,0 +1,21 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@preAmbLe{ \"Hello World\" }\"#)"
+
+---
+ROOT@0..26
+ PREAMBLE@0..26
+ PREAMBLE_TYPE@0..9 "@preAmbLe"
+ L_CURLY@9..10 "{"
+ WHITESPACE@10..11 " "
+ VALUE@11..25
+ TOKEN@11..25
+ QUOTE_GROUP@11..25
+ QUOTE@11..12 "\""
+ WORD@12..17 "Hello"
+ WHITESPACE@17..18 " "
+ WORD@18..23 "World"
+ QUOTE@23..24 "\""
+ WHITESPACE@24..25 " "
+ R_CURLY@25..26 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_complete.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_complete.snap
new file mode 100644
index 0000000000..d1977f2e76
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_complete.snap
@@ -0,0 +1,21 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@preamble{ \"Hello World\" }\"#)"
+
+---
+ROOT@0..26
+ PREAMBLE@0..26
+ PREAMBLE_TYPE@0..9 "@preamble"
+ L_CURLY@9..10 "{"
+ WHITESPACE@10..11 " "
+ VALUE@11..25
+ TOKEN@11..25
+ QUOTE_GROUP@11..25
+ QUOTE@11..12 "\""
+ WORD@12..17 "Hello"
+ WHITESPACE@17..18 " "
+ WORD@18..23 "World"
+ QUOTE@23..24 "\""
+ WHITESPACE@24..25 " "
+ R_CURLY@25..26 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_missing_end.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_missing_end.snap
new file mode 100644
index 0000000000..4ba447a2ae
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_missing_end.snap
@@ -0,0 +1,20 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@preamble{ \"Hello World\" \"#)"
+
+---
+ROOT@0..24
+ PREAMBLE@0..24
+ PREAMBLE_TYPE@0..9 "@preamble"
+ L_CURLY@9..10 "{"
+ WHITESPACE@10..11 " "
+ VALUE@11..24
+ TOKEN@11..24
+ QUOTE_GROUP@11..24
+ QUOTE@11..12 "\""
+ WORD@12..17 "Hello"
+ WHITESPACE@17..18 " "
+ WORD@18..23 "World"
+ QUOTE@23..24 "\""
+ MISSING@24..24 ""
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_casing.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_casing.snap
new file mode 100644
index 0000000000..1c8b1dbea4
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_casing.snap
@@ -0,0 +1,23 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@STRING{foo = \"Hello World\"}\"#)"
+
+---
+ROOT@0..28
+ STRING@0..28
+ STRING_TYPE@0..7 "@STRING"
+ L_CURLY@7..8 "{"
+ WORD@8..11 "foo"
+ WHITESPACE@11..12 " "
+ EQUALITY_SIGN@12..13 "="
+ WHITESPACE@13..14 " "
+ VALUE@14..27
+ TOKEN@14..27
+ QUOTE_GROUP@14..27
+ QUOTE@14..15 "\""
+ WORD@15..20 "Hello"
+ WHITESPACE@20..21 " "
+ WORD@21..26 "World"
+ QUOTE@26..27 "\""
+ R_CURLY@27..28 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_complete.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_complete.snap
new file mode 100644
index 0000000000..506452894d
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_complete.snap
@@ -0,0 +1,23 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@string{foo = {Hello World}}\"#)"
+
+---
+ROOT@0..28
+ STRING@0..28
+ STRING_TYPE@0..7 "@string"
+ L_CURLY@7..8 "{"
+ WORD@8..11 "foo"
+ WHITESPACE@11..12 " "
+ EQUALITY_SIGN@12..13 "="
+ WHITESPACE@13..14 " "
+ VALUE@14..27
+ TOKEN@14..27
+ BRACE_GROUP@14..27
+ L_CURLY@14..15 "{"
+ WORD@15..20 "Hello"
+ WHITESPACE@20..21 " "
+ WORD@21..26 "World"
+ R_CURLY@26..27 "}"
+ R_CURLY@27..28 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_concatenation.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_concatenation.snap
new file mode 100644
index 0000000000..7f457d6964
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_concatenation.snap
@@ -0,0 +1,43 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@string{foo = {Hello World}} @string{bar = foo # \"!\"}\"#)"
+
+---
+ROOT@0..53
+ STRING@0..29
+ STRING_TYPE@0..7 "@string"
+ L_CURLY@7..8 "{"
+ WORD@8..11 "foo"
+ WHITESPACE@11..12 " "
+ EQUALITY_SIGN@12..13 "="
+ WHITESPACE@13..14 " "
+ VALUE@14..27
+ TOKEN@14..27
+ BRACE_GROUP@14..27
+ L_CURLY@14..15 "{"
+ WORD@15..20 "Hello"
+ WHITESPACE@20..21 " "
+ WORD@21..26 "World"
+ R_CURLY@26..27 "}"
+ R_CURLY@27..28 "}"
+ WHITESPACE@28..29 " "
+ STRING@29..53
+ STRING_TYPE@29..36 "@string"
+ L_CURLY@36..37 "{"
+ WORD@37..40 "bar"
+ WHITESPACE@40..41 " "
+ EQUALITY_SIGN@41..42 "="
+ WHITESPACE@42..43 " "
+ VALUE@43..52
+ TOKEN@43..46
+ WORD@43..46 "foo"
+ WHITESPACE@46..47 " "
+ HASH@47..48 "#"
+ WHITESPACE@48..49 " "
+ TOKEN@49..52
+ QUOTE_GROUP@49..52
+ QUOTE@49..50 "\""
+ WORD@50..51 "!"
+ QUOTE@51..52 "\""
+ R_CURLY@52..53 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_incomplete.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_incomplete.snap
new file mode 100644
index 0000000000..685d02a179
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_incomplete.snap
@@ -0,0 +1,23 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@string{foo = {Hello World}\"#)"
+
+---
+ROOT@0..27
+ STRING@0..27
+ STRING_TYPE@0..7 "@string"
+ L_CURLY@7..8 "{"
+ WORD@8..11 "foo"
+ WHITESPACE@11..12 " "
+ EQUALITY_SIGN@12..13 "="
+ WHITESPACE@13..14 " "
+ VALUE@14..27
+ TOKEN@14..27
+ BRACE_GROUP@14..27
+ L_CURLY@14..15 "{"
+ WORD@15..20 "Hello"
+ WHITESPACE@20..21 " "
+ WORD@21..26 "World"
+ R_CURLY@26..27 "}"
+ MISSING@27..27 ""
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_missing_quote.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_missing_quote.snap
new file mode 100644
index 0000000000..8eeefa9bf9
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_missing_quote.snap
@@ -0,0 +1,23 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@STRING{foo = \"Hello World}\"#)"
+
+---
+ROOT@0..27
+ STRING@0..27
+ STRING_TYPE@0..7 "@STRING"
+ L_CURLY@7..8 "{"
+ WORD@8..11 "foo"
+ WHITESPACE@11..12 " "
+ EQUALITY_SIGN@12..13 "="
+ WHITESPACE@13..14 " "
+ VALUE@14..26
+ TOKEN@14..26
+ QUOTE_GROUP@14..26
+ QUOTE@14..15 "\""
+ WORD@15..20 "Hello"
+ WHITESPACE@20..21 " "
+ WORD@21..26 "World"
+ MISSING@26..26 ""
+ R_CURLY@26..27 "}"
+
diff --git a/support/texlab/src/diagnostics/build.rs b/support/texlab/src/syntax/build_log.rs
index 16870e8fd8..65a5938262 100644
--- a/support/texlab/src/diagnostics/build.rs
+++ b/support/texlab/src/syntax/build_log.rs
@@ -1,136 +1,20 @@
-use crate::{
- protocol::{Diagnostic, DiagnosticSeverity, Options, Position, Range, Uri},
- workspace::{Document, Snapshot},
-};
-use futures::lock::Mutex;
+use std::{cmp::Ordering, path::PathBuf};
+
use once_cell::sync::Lazy;
use regex::{Match, Regex};
-use std::{
- cmp::Ordering,
- collections::HashMap,
- io,
- path::{Path, PathBuf},
- str,
- time::SystemTime,
-};
-use tokio::fs;
-
-#[derive(Debug, PartialEq, Eq, Clone)]
-struct LogFile {
- path: PathBuf,
- modified: SystemTime,
-}
-
-#[derive(Debug, Default)]
-pub struct BuildDiagnosticsProvider {
- diagnostics_by_uri: Mutex<HashMap<Uri, Vec<Diagnostic>>>,
- log_files: Mutex<Vec<LogFile>>,
-}
-
-impl BuildDiagnosticsProvider {
- pub async fn get(&self, doc: &Document) -> Vec<Diagnostic> {
- match self.diagnostics_by_uri.lock().await.get(&doc.uri) {
- Some(diagnostics) => diagnostics.to_owned(),
- None => Vec::new(),
- }
- }
-
- pub async fn update(
- &self,
- snapshot: &Snapshot,
- tex_uri: &Uri,
- options: &Options,
- current_dir: &Path,
- ) -> io::Result<bool> {
- let log_uris = snapshot
- .resolve_aux_targets(tex_uri, options, current_dir, "log")
- .unwrap_or_default();
- if let Some(log_path) = log_uris
- .into_iter()
- .filter(|uri| uri.scheme() == "file")
- .filter_map(|uri| uri.to_file_path().ok())
- .find(|path| path.exists())
- {
- let modified = fs::metadata(&log_path).await?.modified()?;
- let mut log_files = self.log_files.lock().await;
- for log_file in log_files.iter_mut() {
- if log_file.path == log_path {
- return if modified > log_file.modified {
- log_file.modified = modified;
- self.update_diagnostics(tex_uri, &log_path).await
- } else {
- Ok(false)
- };
- }
- }
-
- self.update_diagnostics(tex_uri, &log_path).await?;
- log_files.push(LogFile {
- path: log_path,
- modified,
- });
- }
-
- Ok(true)
- }
-
- async fn update_diagnostics(&self, tex_uri: &Uri, log_path: &Path) -> io::Result<bool> {
- let log = String::from_utf8_lossy(&fs::read(log_path).await?).into_owned();
- let mut diagnostics_by_uri = self.diagnostics_by_uri.lock().await;
- diagnostics_by_uri.clear();
- for error in parse_build_log(tex_uri, &log) {
- let diagnostics = diagnostics_by_uri
- .entry(error.uri.clone())
- .or_insert_with(Vec::new);
- diagnostics.push(error.into());
- }
- Ok(true)
- }
-}
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum BuildErrorKind {
+#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord, Hash)]
+pub enum BuildErrorLevel {
Error,
Warning,
}
-#[derive(Debug, PartialEq, Eq, Clone)]
+#[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub struct BuildError {
- pub uri: Uri,
- pub kind: BuildErrorKind,
+ pub relative_path: PathBuf,
+ pub level: BuildErrorLevel,
pub message: String,
- pub line: Option<u64>,
-}
-
-impl BuildError {
- pub fn new(uri: Uri, kind: BuildErrorKind, message: String, line: Option<u64>) -> Self {
- Self {
- uri,
- kind,
- message,
- line,
- }
- }
-}
-
-impl Into<Diagnostic> for BuildError {
- fn into(self) -> Diagnostic {
- let position = Position::new(self.line.unwrap_or(0), 0);
- let severity = match self.kind {
- BuildErrorKind::Error => DiagnosticSeverity::Error,
- BuildErrorKind::Warning => DiagnosticSeverity::Warning,
- };
- let range = Range::new(position, position);
- Diagnostic {
- range,
- severity: Some(severity),
- source: Some("latex".into()),
- code: None,
- message: self.message,
- tags: None,
- related_information: None,
- }
- }
+ pub line: Option<u32>,
}
const MAX_LINE_LENGTH: usize = 79;
@@ -152,27 +36,33 @@ pub static BAD_BOX_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new("(?P<msg>(Ov|Und)erfull \\\\[hv]box[^\r\n]*lines? (?P<line>\\d+)[^\r\n]*)").unwrap()
});
-fn parse_build_log(uri: &Uri, log: &str) -> Vec<BuildError> {
+#[derive(Debug, PartialEq, Eq, Clone, Hash)]
+pub struct Parse {
+ pub errors: Vec<BuildError>,
+}
+
+pub fn parse(log: &str) -> Parse {
let log = prepare_log(log);
let mut ranges: Vec<FileRange> = FILE_REGEX
.find_iter(&log)
- .map(|result| create_file_range(uri.clone(), &log, result))
+ .map(|result| FileRange::create(&log, result))
.collect();
ranges.sort();
- let tex_errors = extract_matches(&log, &uri, &ranges, &TEX_ERROR_REGEX, BuildErrorKind::Error);
- let warnings = extract_matches(&log, &uri, &ranges, &WARNING_REGEX, BuildErrorKind::Warning);
- let bad_boxes = extract_matches(&log, &uri, &ranges, &BAD_BOX_REGEX, BuildErrorKind::Warning);
+ let tex_errors = extract_matches(&log, &ranges, &TEX_ERROR_REGEX, BuildErrorLevel::Error);
+ let warnings = extract_matches(&log, &ranges, &WARNING_REGEX, BuildErrorLevel::Warning);
+ let bad_boxes = extract_matches(&log, &ranges, &BAD_BOX_REGEX, BuildErrorLevel::Warning);
- vec![tex_errors, warnings, bad_boxes].concat()
+ Parse {
+ errors: vec![tex_errors, warnings, bad_boxes].concat(),
+ }
}
fn extract_matches(
log: &str,
- parent_uri: &Uri,
ranges: &[FileRange],
regex: &Regex,
- kind: BuildErrorKind,
+ level: BuildErrorLevel,
) -> Vec<BuildError> {
let mut errors = Vec::new();
for result in regex.find_iter(&log) {
@@ -191,10 +81,14 @@ fn extract_matches(
if let Some(range) = ranges.iter().find(|range| range.contains(result.start())) {
let line = captures
.name("line")
- .map(|result| u64::from_str_radix(result.as_str(), 10).unwrap() - 1);
+ .map(|result| u32::from_str_radix(result.as_str(), 10).unwrap() - 1);
- let uri = range.uri.as_ref().unwrap_or(parent_uri);
- errors.push(BuildError::new(uri.clone(), kind, message, line));
+ errors.push(BuildError {
+ relative_path: range.path.clone(),
+ level,
+ message,
+ line,
+ });
}
}
errors
@@ -230,52 +124,41 @@ fn prepare_log(log: &str) -> String {
new_lines.join("\n")
}
-fn create_file_range(parent: Uri, log: &str, result: Match) -> FileRange {
- let mut balance = 1;
- let mut end = result.start() + 1;
- let chars = (&log[result.start() + 1..]).chars();
- for c in chars {
- if balance <= 0 {
- break;
- }
-
- if c == '(' {
- balance += 1;
- } else if c == ')' {
- balance -= 1;
- }
- end += c.len_utf8();
- }
-
- let captures = FILE_REGEX.captures(result.as_str()).unwrap();
- let base_path = PathBuf::from(parent.path());
- let mut full_path = base_path.clone();
- full_path.push(captures.name("file").unwrap().as_str());
- let uri = if full_path.starts_with(base_path) {
- parent
- .join(captures.name("file").unwrap().as_str())
- .map(Into::into)
- .ok()
- } else {
- None
- };
-
- FileRange::new(uri, result.start(), end)
-}
-
#[derive(Debug, PartialEq, Eq, Clone)]
struct FileRange {
- pub uri: Option<Uri>,
+ pub path: PathBuf,
pub start: usize,
pub end: usize,
}
impl FileRange {
- fn new(uri: Option<Uri>, start: usize, end: usize) -> Self {
- Self { uri, start, end }
+ fn create(log: &str, result: Match) -> Self {
+ let mut balance = 1;
+ let mut end = result.start() + 1;
+ let chars = (&log[result.start() + 1..]).chars();
+ for c in chars {
+ if balance <= 0 {
+ break;
+ }
+
+ if c == '(' {
+ balance += 1;
+ } else if c == ')' {
+ balance -= 1;
+ }
+ end += c.len_utf8();
+ }
+
+ let captures = FILE_REGEX.captures(result.as_str()).unwrap();
+ let path = PathBuf::from(captures.name("file").unwrap().as_str());
+ Self {
+ path,
+ start: result.start(),
+ end,
+ }
}
- fn length(&self) -> usize {
+ fn len(&self) -> usize {
self.end - self.start + 1
}
@@ -285,13 +168,13 @@ impl FileRange {
}
impl Ord for FileRange {
- fn cmp(&self, other: &FileRange) -> Ordering {
- self.length().cmp(&other.length())
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.len().cmp(&other.len())
}
}
impl PartialOrd for FileRange {
- fn partial_cmp(&self, other: &FileRange) -> Option<Ordering> {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
@@ -301,15 +184,10 @@ mod tests {
use super::*;
use indoc::indoc;
- fn create_uri(name: &str) -> Uri {
- let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(name);
- Uri::from_file_path(path.to_str().unwrap()).unwrap()
- }
-
#[test]
- fn bad_box() {
- let log = indoc!(
- r#"
+ fn test_parse_001() {
+ let log = indoc! {
+ r#"
This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/W32TeX) (preloaded format=pdflatex 2018.3.30) 26 DEC 2018 16:50
entering extended mode
restricted \write18 enabled.
@@ -362,7 +240,7 @@ mod tests {
Overfull \hbox (200.00162pt too wide) in paragraph at lines 8--9
[]\OT1/cmr/m/n/10 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
[]
LaTeX Font Info: External font `cmex10' loaded for size
@@ -392,30 +270,33 @@ mod tests {
16 PDF objects out of 1000 (max. 8388607)
10 compressed objects within 1 object stream
0 named destinations out of 1000 (max. 500000)
- 1 words of extra memory for PDF output out of 10000 (max. 10000000)"#
- );
-
- let actual_errors = parse_build_log(&create_uri("parent.tex"), log);
+ 1 words of extra memory for PDF output out of 10000 (max. 10000000)"#,
+ };
- let error1 = BuildError::new(
- create_uri("parent.tex"),
- BuildErrorKind::Warning,
- "Overfull \\hbox (200.00162pt too wide) in paragraph at lines 8--9".into(),
- Some(7),
- );
- let error2 = BuildError::new(
- create_uri("parent.tex"),
- BuildErrorKind::Warning,
- "Overfull \\vbox (3.19998pt too high) detected at line 23".into(),
- Some(22),
+ assert_eq!(
+ parse(&log).errors,
+ vec![
+ BuildError {
+ relative_path: "./parent.tex".into(),
+ level: BuildErrorLevel::Warning,
+ message: "Overfull \\hbox (200.00162pt too wide) in paragraph at lines 8--9"
+ .into(),
+ line: Some(7),
+ },
+ BuildError {
+ relative_path: "./parent.tex".into(),
+ level: BuildErrorLevel::Warning,
+ message: "Overfull \\vbox (3.19998pt too high) detected at line 23".into(),
+ line: Some(22),
+ }
+ ]
);
- assert_eq!(actual_errors, vec![error1, error2]);
}
#[test]
- fn related() {
- let log = indoc!(
- r#"
+ fn test_parse_002() {
+ let log = indoc! {
+ r#"
This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/W32TeX) (preloaded format=pdflatex 2018.3.30) 26 DEC 2018 16:52
entering extended mode
restricted \write18 enabled.
@@ -442,7 +323,7 @@ mod tests {
\bibindent=\dimen102
) (./parent.aux (./child.aux))
\openout1 = `parent.aux'.
-
+
LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 4.
LaTeX Font Info: ... okay on input line 4.
LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 4.
@@ -458,14 +339,14 @@ mod tests {
(./child.tex
! Undefined control sequence.
l.1 \foo
-
+
The control sequence at the end of the top line
of your error message was never \def'ed. If you have
misspelled it (e.g., `\hobx'), type `I' and the correct
spelling (e.g., `I\hbox'). Otherwise just continue,
and I'll forget about whatever was undefined.
-
- ) (./parent.aux) )
+
+ ) (./parent.aux) )
Here is how much of TeX's memory you used:
205 strings out of 492995
2149 string characters out of 6138727
@@ -474,29 +355,29 @@ mod tests {
3640 words of font info for 14 fonts, out of 8000000 for 9000
1141 hyphenation exceptions out of 8191
23i,1n,17p,116b,36s stack positions out of 5000i,500n,10000p,200000b,80000s
-
+
No pages of output.
PDF statistics:
0 PDF objects out of 1000 (max. 8388607)
0 named destinations out of 1000 (max. 500000)
- 1 words of extra memory for PDF output out of 10000 (max. 10000000)"#
- );
-
- let actual_errors = parse_build_log(&create_uri("parent.tex"), log);
+ 1 words of extra memory for PDF output out of 10000 (max. 10000000)"#,
+ };
- let error = BuildError::new(
- create_uri("child.tex"),
- BuildErrorKind::Error,
- "Undefined control sequence.".into(),
- Some(0),
+ assert_eq!(
+ parse(&log).errors,
+ vec![BuildError {
+ relative_path: "./child.tex".into(),
+ level: BuildErrorLevel::Error,
+ message: "Undefined control sequence.".into(),
+ line: Some(0)
+ }]
);
- assert_eq!(actual_errors, vec![error]);
}
#[test]
- fn citation_warning() {
- let log = indoc!(
- r#"
+ fn test_parse_003() {
+ let log = indoc! {
+ r#"
This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/W32TeX) (preloaded format=pdflatex 2018.3.30) 26 DEC 2018 16:51
entering extended mode
restricted \write18 enabled.
@@ -523,7 +404,7 @@ mod tests {
\bibindent=\dimen102
) (./parent.aux)
\openout1 = `parent.aux'.
-
+
LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 4.
LaTeX Font Info: ... okay on input line 4.
LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 4.
@@ -536,17 +417,16 @@ mod tests {
LaTeX Font Info: ... okay on input line 4.
LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 4.
LaTeX Font Info: ... okay on input line 4.
-
-
+
LaTeX Warning: Citation `foo' on page 1 undefined on input line 6.
-
+
[1
-
+
{/TexLive/texmf-var/fonts/map/pdftex/updmap/pdftex.map}] (./parent.aux)
-
+
LaTeX Warning: There were undefined references.
-
- )
+
+ )
Here is how much of TeX's memory you used:
204 strings out of 492995
2142 string characters out of 6138727
@@ -562,30 +442,32 @@ mod tests {
16 PDF objects out of 1000 (max. 8388607)
10 compressed objects within 1 object stream
0 named destinations out of 1000 (max. 500000)
- 1 words of extra memory for PDF output out of 10000 (max. 10000000)"#
- );
-
- let actual_errors = parse_build_log(&create_uri("parent.tex"), log);
+ 1 words of extra memory for PDF output out of 10000 (max. 10000000)"#,
+ };
- let error1 = BuildError::new(
- create_uri("parent.tex"),
- BuildErrorKind::Warning,
- "Citation `foo' on page 1 undefined on input line 6.".into(),
- None,
- );
- let error2 = BuildError::new(
- create_uri("parent.tex"),
- BuildErrorKind::Warning,
- "There were undefined references.".into(),
- None,
+ assert_eq!(
+ parse(&log).errors,
+ vec![
+ BuildError {
+ relative_path: "./parent.tex".into(),
+ level: BuildErrorLevel::Warning,
+ message: "Citation `foo' on page 1 undefined on input line 6.".into(),
+ line: None
+ },
+ BuildError {
+ relative_path: "./parent.tex".into(),
+ level: BuildErrorLevel::Warning,
+ message: "There were undefined references.".into(),
+ line: None,
+ }
+ ]
);
- assert_eq!(actual_errors, vec![error1, error2]);
}
#[test]
- fn package_error() {
- let log = indoc!(
- r#"
+ fn test_parse_004() {
+ let log = indoc! {
+ r#"
This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/W32TeX) (preloaded format=pdflatex 2018.3.30) 26 DEC 2018 16:40
entering extended mode
restricted \write18 enabled.
@@ -613,37 +495,35 @@ mod tests {
)
(/TexLive/texmf-dist/tex/generic/babel/babel.sty
Package: babel 2017/05/19 3.10 The Babel package
-
-
+
! Package babel Error: Unknown option `foo'. Either you misspelled it
(babel) or the language definition file foo.ldf was not found.
-
+
See the babel package documentation for explanation.
Type H <return> for immediate help.
- ...
-
+ ...
+
l.393 \ProcessOptions*
-
+
Valid options are: shorthands=, KeepShorthandsActive,
activeacute, activegrave, noconfigs, safe=, main=, math=
headfoot=, strings=, config=, hyphenmap=, or a language name.
-
-
+
! Package babel Error: You haven't specified a language option.
-
+
See the babel package documentation for explanation.
Type H <return> for immediate help.
- ...
-
+ ...
+
l.426 ...ry to proceed from here, type x to quit.}
-
+
You need to specify a language, either as a global option
or as an optional argument to the \usepackage command;
You shouldn't try to proceed from here, type x to quit.
-
+
) (./parent.aux)
\openout1 = `parent.aux'.
-
+
LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 5.
LaTeX Font Info: ... okay on input line 5.
LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 5.
@@ -656,7 +536,7 @@ mod tests {
LaTeX Font Info: ... okay on input line 5.
LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 5.
LaTeX Font Info: ... okay on input line 5.
- (./parent.aux) )
+ (./parent.aux) )
Here is how much of TeX's memory you used:
304 strings out of 492995
3421 string characters out of 6138727
@@ -665,36 +545,37 @@ mod tests {
3640 words of font info for 14 fonts, out of 8000000 for 9000
1141 hyphenation exceptions out of 8191
23i,1n,17p,116b,36s stack positions out of 5000i,500n,10000p,200000b,80000s
-
+
No pages of output.
PDF statistics:
0 PDF objects out of 1000 (max. 8388607)
0 named destinations out of 1000 (max. 500000)
- 1 words of extra memory for PDF output out of 10000 (max. 10000000)"#
- );
-
- let actual_errors = parse_build_log(&create_uri("parent.tex"), log);
+ 1 words of extra memory for PDF output out of 10000 (max. 10000000)"#,
+ };
- let error1 = BuildError::new(
- create_uri("parent.tex"),
- BuildErrorKind::Error,
- "Package babel Error: Unknown option `foo'. Either you misspelled it or the language definition file foo.ldf was not found."
- .into(),
- Some(392),
- );
- let error2 = BuildError::new(
- create_uri("parent.tex"),
- BuildErrorKind::Error,
- "Package babel Error: You haven't specified a language option.".into(),
- Some(425),
+ assert_eq!(
+ parse(&log).errors,
+ vec![
+ BuildError {
+ relative_path: "/TexLive/texmf-dist/tex/generic/babel/babel.sty".into(),
+ level: BuildErrorLevel::Error,
+ message: "Package babel Error: Unknown option `foo'. Either you misspelled it or the language definition file foo.ldf was not found.".into(),
+ line: Some(392)
+ },
+ BuildError {
+ relative_path: "/TexLive/texmf-dist/tex/generic/babel/babel.sty".into(),
+ level: BuildErrorLevel::Error,
+ message: "Package babel Error: You haven't specified a language option.".into(),
+ line: Some(425)
+ }
+ ]
);
- assert_eq!(actual_errors, vec![error1, error2]);
}
#[test]
- fn package_warning() {
- let log = indoc!(
- r#"
+ fn test_parse_005() {
+ let log = indoc! {
+ r#"
This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/W32TeX) (preloaded format=pdflatex 2018.3.30) 26 DEC 2018 16:45
entering extended mode
restricted \write18 enabled.
@@ -722,10 +603,10 @@ mod tests {
)
(/TexLive/texmf-dist/tex/generic/babel/babel.sty
Package: babel 2017/05/19 3.10 The Babel package
-
+
(/TexLive/texmf-dist/tex/generic/babel-spanish/spanish.ldf
Language: spanish.ldf 2016/03/03 v5.0p Spanish support from the babel system
-
+
(/TexLive/texmf-dist/tex/generic/babel/babel.def
File: babel.def 2017/05/19 3.10 Babel common definitions
\babel@savecnt=\count87
@@ -740,11 +621,11 @@ mod tests {
))
(/TexLive/texmf-dist/tex/latex/biblatex/biblatex.sty
Package: biblatex 2016/12/05 v3.7 programmable bibliographies (PK/JW/AB)
-
+
(/TexLive/texmf-dist/tex/generic/oberdiek/pdftexcmds.sty
Package: pdftexcmds 2017/03/19 v0.25 Utility functions of pdfTeX for LuaTeX (HO
)
-
+
(/TexLive/texmf-dist/tex/generic/oberdiek/infwarerr.sty
Package: infwarerr 2016/05/16 v1.4 Providing info/warning/error messages (HO)
)
@@ -773,10 +654,10 @@ mod tests {
)
(/TexLive/texmf-dist/tex/latex/oberdiek/kvoptions.sty
Package: kvoptions 2016/05/16 v3.12 Key value format for package options (HO)
-
+
(/TexLive/texmf-dist/tex/generic/oberdiek/kvsetkeys.sty
Package: kvsetkeys 2016/05/16 v1.17 Key value parser (HO)
-
+
(/TexLive/texmf-dist/tex/generic/oberdiek/etexcmds.sty
Package: etexcmds 2016/05/16 v1.6 Avoid name clashes with e-TeX commands (HO)
Package etexcmds Info: Could not find \expanded.
@@ -787,7 +668,7 @@ mod tests {
(/TexLive/texmf-dist/tex/latex/logreq/logreq.sty
Package: logreq 2010/08/04 v1.0 xml request logger
\lrq@indent=\count90
-
+
(/TexLive/texmf-dist/tex/latex/logreq/logreq.def
File: logreq.def 2010/08/04 v1.0 logreq spec v1.0
))
@@ -859,7 +740,7 @@ mod tests {
\c@mincompwidth=\count133
Package biblatex Info: Trying to load biblatex default data model...
Package biblatex Info: ... file 'blx-dm.def' found.
-
+
(/TexLive/texmf-dist/tex/latex/biblatex/blx-dm.def)
Package biblatex Info: Trying to load biblatex style data model...
Package biblatex Info: ... file 'ieee.dbx' not found.
@@ -939,13 +820,13 @@ mod tests {
\shorteditorwidth=\skip56
Package biblatex Info: Trying to load compatibility code...
Package biblatex Info: ... file 'blx-compat.def' found.
-
+
(/TexLive/texmf-dist/tex/latex/biblatex/blx-compat.def
File: blx-compat.def 2016/12/05 v3.7 biblatex compatibility (PK/JW/AB)
)
Package biblatex Info: Trying to load generic definitions...
Package biblatex Info: ... file 'biblatex.def' found.
-
+
(/TexLive/texmf-dist/tex/latex/biblatex/biblatex.def
File: biblatex.def 2016/12/05 v3.7 biblatex compatibility (PK/JW/AB)
\c@textcitecount=\count266
@@ -958,22 +839,22 @@ mod tests {
)
Package biblatex Info: Trying to load bibliography style 'ieee'...
Package biblatex Info: ... file 'ieee.bbx' found.
-
+
(/TexLive/texmf-dist/tex/latex/biblatex-ieee/ieee.bbx
File: ieee.bbx 2017/03/27 v1.2d biblatex bibliography style
Package biblatex Info: Trying to load bibliography style 'numeric-comp'...
Package biblatex Info: ... file 'numeric-comp.bbx' found.
-
+
(/TexLive/texmf-dist/tex/latex/biblatex/bbx/numeric-comp.bbx
File: numeric-comp.bbx 2016/12/05 v3.7 biblatex bibliography style (PK/JW/AB)
Package biblatex Info: Trying to load bibliography style 'numeric'...
Package biblatex Info: ... file 'numeric.bbx' found.
-
+
(/TexLive/texmf-dist/tex/latex/biblatex/bbx/numeric.bbx
File: numeric.bbx 2016/12/05 v3.7 biblatex bibliography style (PK/JW/AB)
Package biblatex Info: Trying to load bibliography style 'standard'...
Package biblatex Info: ... file 'standard.bbx' found.
-
+
(/TexLive/texmf-dist/tex/latex/biblatex/bbx/standard.bbx
File: standard.bbx 2016/12/05 v3.7 biblatex bibliography style (PK/JW/AB)
\c@bbx:relatedcount=\count273
@@ -981,12 +862,12 @@ mod tests {
))))
Package biblatex Info: Trying to load citation style 'ieee'...
Package biblatex Info: ... file 'ieee.cbx' found.
-
+
(/TexLive/texmf-dist/tex/latex/biblatex-ieee/ieee.cbx
File: ieee.cbx 2017/03/27 v1.2d biblatex citation style
Package biblatex Info: Trying to load citation style 'numeric-comp'...
Package biblatex Info: ... file 'numeric-comp.cbx' found.
-
+
(/TexLive/texmf-dist/tex/latex/biblatex/cbx/numeric-comp.cbx
File: numeric-comp.cbx 2016/12/05 v3.7 biblatex citation style (PK/JW/AB)
\c@cbx@tempcnta=\count275
@@ -1008,25 +889,25 @@ mod tests {
)
Package biblatex Info: Trying to load configuration file...
Package biblatex Info: ... file 'biblatex.cfg' found.
-
+
(/TexLive/texmf-dist/tex/latex/biblatex/biblatex.cfg
- File: biblatex.cfg
+ File: biblatex.cfg
))
Package biblatex Info: Trying to load language 'spanish'...
Package biblatex Info: ... file 'spanish.lbx' found.
-
+
(/TexLive/texmf-dist/tex/latex/biblatex/lbx/spanish.lbx
File: spanish.lbx 2016/12/05 v3.7 biblatex localization (PK/JW/AB)
)
-
+
Package biblatex Warning: 'babel/polyglossia' detected but 'csquotes' missing.
(biblatex) Loading 'csquotes' recommended.
-
+
\@quotelevel=\count277
\@quotereset=\count278
(./parent.aux)
\openout1 = `parent.aux'.
-
+
LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 6.
LaTeX Font Info: ... okay on input line 6.
LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 6.
@@ -1047,26 +928,25 @@ mod tests {
Package biblatex Info: Automatic encoding selection.
(biblatex) Assuming data encoding 'ascii'.
\openout4 = `parent.bcf'.
-
+
Package biblatex Info: Trying to load bibliographic data...
Package biblatex Info: ... file 'parent.bbl' not found.
-
+
No file parent.bbl.
Package biblatex Info: Reference section=0 on input line 6.
Package biblatex Info: Reference segment=0 on input line 6.
(./parent.aux)
-
+
LaTeX Warning: There were undefined references.
-
-
+
Package biblatex Warning: Please (re)run Biber on the file:
(biblatex) parent
(biblatex) and rerun LaTeX afterwards.
-
+
Package logreq Info: Writing requests to 'parent.run.xml'.
\openout1 = `parent.run.xml'.
-
- )
+
+ )
Here is how much of TeX's memory you used:
7717 strings out of 492995
133301 string characters out of 6138727
@@ -1075,41 +955,42 @@ mod tests {
3640 words of font info for 14 fonts, out of 8000000 for 9000
1141 hyphenation exceptions out of 8191
35i,1n,30p,856b,700s stack positions out of 5000i,500n,10000p,200000b,80000s
-
+
No pages of output.
PDF statistics:
0 PDF objects out of 1000 (max. 8388607)
0 named destinations out of 1000 (max. 500000)
- 1 words of extra memory for PDF output out of 10000 (max. 10000000)"#
- );
-
- let actual_errors = parse_build_log(&create_uri("parent.tex"), log);
+ 1 words of extra memory for PDF output out of 10000 (max. 10000000)"#,
+ };
- let error1 = BuildError::new(
- create_uri("parent.tex"),
- BuildErrorKind::Warning,
- "'babel/polyglossia' detected but 'csquotes' missing. Loading 'csquotes' recommended."
- .to_owned(),
- None,
- );
- let error2 = BuildError::new(
- create_uri("parent.tex"),
- BuildErrorKind::Warning,
- "There were undefined references.".into(),
- None,
- );
- let error3 = BuildError::new(
- create_uri("parent.tex"),
- BuildErrorKind::Warning,
- "Please (re)run Biber on the file: parent and rerun LaTeX afterwards.".into(),
- None,
+ assert_eq!(
+ parse(&log).errors,
+ vec![
+ BuildError {
+ relative_path: "./parent.tex".into(),
+ level: BuildErrorLevel::Warning,
+ message: "'babel/polyglossia' detected but 'csquotes' missing. Loading 'csquotes' recommended.".into(),
+ line: None
+ },
+ BuildError {
+ relative_path: "./parent.tex".into(),
+ level: BuildErrorLevel::Warning,
+ message: "There were undefined references.".into(),
+ line: None,
+ },
+ BuildError {
+ relative_path: "./parent.tex".into(),
+ level: BuildErrorLevel::Warning,
+ message: "Please (re)run Biber on the file: parent and rerun LaTeX afterwards.".into(),
+ line: None
+ }
+ ]
);
- assert_eq!(actual_errors, vec![error1, error2, error3]);
}
#[test]
- fn tex_error() {
- let log = indoc!(
+ fn test_parse_006() {
+ let log = indoc! {
r#"
This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/W32TeX) (preloaded format=pdflatex 2018.3.30) 26 DEC 2018 16:33
entering extended mode
@@ -1138,7 +1019,7 @@ mod tests {
) (./parent.aux
(./child.tex.aux))
\openout1 = `parent.aux'.
-
+
LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 3.
LaTeX Font Info: ... okay on input line 3.
LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 3.
@@ -1152,73 +1033,71 @@ mod tests {
LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 3.
LaTeX Font Info: ... okay on input line 3.
\openout2 = `child.aux'.
-
+
(./child.tex)
! Undefined control sequence.
l.7 \foo
-
+
The control sequence at the end of the top line
of your error message was never \def'ed. If you have
misspelled it (e.g., `\hobx'), type `I' and the correct
spelling (e.g., `I\hbox'). Otherwise just continue,
and I'll forget about whatever was undefined.
-
+
! Missing $ inserted.
- <inserted text>
+ <inserted text>
$
l.8 \bar
-
+
I've inserted a begin-math/end-math symbol since I think
you left one out. Proceed, with fingers crossed.
-
+
LaTeX Font Info: External font `cmex10' loaded for size
(Font) <7> on input line 8.
LaTeX Font Info: External font `cmex10' loaded for size
(Font) <5> on input line 8.
! Undefined control sequence.
l.9 \baz
-
+
The control sequence at the end of the top line
of your error message was never \def'ed. If you have
misspelled it (e.g., `\hobx'), type `I' and the correct
spelling (e.g., `I\hbox'). Otherwise just continue,
and I'll forget about whatever was undefined.
-
+
! Missing { inserted.
- <to be read again>
- \par
- l.10
-
+ <to be read again>
+ \par
+ l.10
+
A left brace was mandatory here, so I've put one in.
You might want to delete and/or insert some corrections
so that I will find a matching right brace soon.
(If you're confused by all this, try typing `I}' now.)
-
+
! Missing $ inserted.
- <inserted text>
+ <inserted text>
$
- l.10
-
+ l.10
+
I've inserted a begin-math/end-math symbol since I think
you left one out. Proceed, with fingers crossed.
-
+
! Missing } inserted.
- <inserted text>
+ <inserted text>
}
- l.10
-
+ l.10
+
I've inserted something that you may have forgotten.
(See the <inserted text> above.)
With luck, this will get me unwedged. But if you
really didn't forget anything, try typing `2' now; then
my insertion and my current dilemma will both disappear.
-
+
[1
-
-
-
+
{/TexLive/texmf-var/fonts/map/pdftex/updmap/pdftex.map}] (./parent.aux
- (./child.aux)) )
+ (./child.aux)) )
Here is how much of TeX's memory you used:
212 strings out of 492995
2238 string characters out of 6138727
@@ -1234,50 +1113,49 @@ mod tests {
12 PDF objects out of 1000 (max. 8388607)
7 compressed objects within 1 object stream
0 named destinations out of 1000 (max. 500000)
- 1 words of extra memory for PDF output out of 10000 (max. 10000000)"#
- );
-
- let actual_errors = parse_build_log(&create_uri("parent.tex"), log);
+ 1 words of extra memory for PDF output out of 10000 (max. 10000000)"#,
+ };
- let error1 = BuildError::new(
- create_uri("parent.tex"),
- BuildErrorKind::Error,
- "Undefined control sequence.".into(),
- Some(6),
- );
- let error2 = BuildError::new(
- create_uri("parent.tex"),
- BuildErrorKind::Error,
- "Missing $ inserted.".into(),
- Some(7),
- );
- let error3 = BuildError::new(
- create_uri("parent.tex"),
- BuildErrorKind::Error,
- "Undefined control sequence.".into(),
- Some(8),
- );
- let error4 = BuildError::new(
- create_uri("parent.tex"),
- BuildErrorKind::Error,
- "Missing { inserted.".into(),
- Some(9),
- );
- let error5 = BuildError::new(
- create_uri("parent.tex"),
- BuildErrorKind::Error,
- "Missing $ inserted.".into(),
- Some(9),
- );
- let error6 = BuildError::new(
- create_uri("parent.tex"),
- BuildErrorKind::Error,
- "Missing } inserted.".into(),
- Some(9),
- );
assert_eq!(
- actual_errors,
- vec![error1, error2, error3, error4, error5, error6]
+ parse(&log).errors,
+ vec![
+ BuildError {
+ relative_path: "./parent.tex".into(),
+ level: BuildErrorLevel::Error,
+ message: "Undefined control sequence.".into(),
+ line: Some(6)
+ },
+ BuildError {
+ relative_path: "./parent.tex".into(),
+ level: BuildErrorLevel::Error,
+ message: "Missing $ inserted.".into(),
+ line: Some(7)
+ },
+ BuildError {
+ relative_path: "./parent.tex".into(),
+ level: BuildErrorLevel::Error,
+ message: "Undefined control sequence.".into(),
+ line: Some(8)
+ },
+ BuildError {
+ relative_path: "./parent.tex".into(),
+ level: BuildErrorLevel::Error,
+ message: "Missing { inserted.".into(),
+ line: Some(9)
+ },
+ BuildError {
+ relative_path: "./parent.tex".into(),
+ level: BuildErrorLevel::Error,
+ message: "Missing $ inserted.".into(),
+ line: Some(9)
+ },
+ BuildError {
+ relative_path: "./parent.tex".into(),
+ level: BuildErrorLevel::Error,
+ message: "Missing } inserted.".into(),
+ line: Some(9)
+ },
+ ]
);
}
}
diff --git a/support/texlab/src/syntax/generic_ast.rs b/support/texlab/src/syntax/generic_ast.rs
deleted file mode 100644
index 3311460c0a..0000000000
--- a/support/texlab/src/syntax/generic_ast.rs
+++ /dev/null
@@ -1,51 +0,0 @@
-use serde::{Deserialize, Serialize};
-use std::ops::Index;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
-pub struct AstNodeIndex(usize);
-
-#[derive(Debug, PartialEq, Eq, Clone, Default)]
-pub struct Ast<T> {
- nodes: Vec<T>,
- edges: Vec<Vec<AstNodeIndex>>,
-}
-
-impl<T> Ast<T> {
- pub fn new() -> Self {
- Self {
- nodes: Vec::new(),
- edges: Vec::new(),
- }
- }
-
- pub fn nodes(&self) -> Vec<AstNodeIndex> {
- let mut nodes = Vec::new();
- for i in 0..self.nodes.len() {
- nodes.push(AstNodeIndex(i));
- }
- nodes
- }
-
- pub fn add_node(&mut self, value: T) -> AstNodeIndex {
- let node = AstNodeIndex(self.nodes.len());
- self.nodes.push(value);
- self.edges.push(Vec::new());
- node
- }
-
- pub fn add_edge(&mut self, parent: AstNodeIndex, child: AstNodeIndex) {
- self.edges[parent.0].push(child);
- }
-
- pub fn children<'a>(&'a self, parent: AstNodeIndex) -> impl Iterator<Item = AstNodeIndex> + 'a {
- self.edges[parent.0].iter().map(|child| *child)
- }
-}
-
-impl<T> Index<AstNodeIndex> for Ast<T> {
- type Output = T;
-
- fn index(&self, index: AstNodeIndex) -> &Self::Output {
- &self.nodes[index.0]
- }
-}
diff --git a/support/texlab/src/syntax/lang_data.rs b/support/texlab/src/syntax/lang_data.rs
deleted file mode 100644
index 4ea7cc44da..0000000000
--- a/support/texlab/src/syntax/lang_data.rs
+++ /dev/null
@@ -1,231 +0,0 @@
-use once_cell::sync::Lazy;
-use serde::{Deserialize, Serialize};
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct LatexEnvironmentCommand {
- pub name: String,
- pub index: usize,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct LatexCitationCommand {
- pub name: String,
- pub index: usize,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub enum LatexLabelReferenceSource {
- Everything,
- Math,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub enum LatexLabelKind {
- Definition,
- Reference(LatexLabelReferenceSource),
-}
-
-impl LatexLabelKind {
- pub fn is_reference(self) -> bool {
- match self {
- LatexLabelKind::Definition => false,
- LatexLabelKind::Reference(_) => true,
- }
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct LatexLabelCommand {
- pub name: String,
- pub index: usize,
- pub kind: LatexLabelKind,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct LatexSectionCommand {
- pub name: String,
- pub index: usize,
- pub level: i32,
- pub prefix: String,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub enum LatexIncludeKind {
- Package,
- Class,
- Latex,
- Bibliography,
- Image,
- Svg,
- Pdf,
- Everything,
-}
-
-impl LatexIncludeKind {
- pub fn extensions(self) -> Option<&'static [&'static str]> {
- match self {
- LatexIncludeKind::Package => Some(&["sty"]),
- LatexIncludeKind::Class => Some(&["cls"]),
- LatexIncludeKind::Latex => Some(&["tex"]),
- LatexIncludeKind::Bibliography => Some(&["bib"]),
- LatexIncludeKind::Image => Some(&["pdf", "png", "jpg", "jpeg", "bmp"]),
- LatexIncludeKind::Svg => Some(&["svg"]),
- LatexIncludeKind::Pdf => Some(&["pdf"]),
- LatexIncludeKind::Everything => None,
- }
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct LatexIncludeCommand {
- pub name: String,
- pub index: usize,
- pub kind: LatexIncludeKind,
- pub include_extension: bool,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct LatexCommandDefinitionCommand {
- pub name: String,
- pub definition_index: usize,
- pub arg_count_index: usize,
- pub implementation_index: usize,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct LatexMathOperatorCommand {
- pub name: String,
- pub definition_index: usize,
- pub implementation_index: usize,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct LatexTheoremDefinitionCommand {
- pub name: String,
- pub index: usize,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct LatexColorCommand {
- pub name: String,
- pub index: usize,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct LatexColorModelCommand {
- pub name: String,
- pub index: usize,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub enum LatexGlossaryEntryKind {
- General,
- Acronym,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct LatexGlossaryEntryDefinitionCommand {
- pub name: String,
- pub label_index: usize,
- pub kind: LatexGlossaryEntryKind,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct LatexGlossaryEntryReferenceCommand {
- pub name: String,
- pub index: usize,
- pub kind: LatexGlossaryEntryKind,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub enum BibtexEntryTypeCategory {
- Misc,
- String,
- Article,
- Book,
- Collection,
- Part,
- Thesis,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct BibtexEntryTypeDoc {
- pub name: String,
- pub category: BibtexEntryTypeCategory,
- pub documentation: Option<String>,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct BibtexFieldDoc {
- pub name: String,
- pub documentation: String,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct LanguageData {
- pub environment_commands: Vec<LatexEnvironmentCommand>,
- pub citation_commands: Vec<LatexCitationCommand>,
- pub label_commands: Vec<LatexLabelCommand>,
- pub section_commands: Vec<LatexSectionCommand>,
- pub include_commands: Vec<LatexIncludeCommand>,
- pub command_definition_commands: Vec<LatexCommandDefinitionCommand>,
- pub math_operator_commands: Vec<LatexMathOperatorCommand>,
- pub theorem_definition_commands: Vec<LatexTheoremDefinitionCommand>,
- pub colors: Vec<String>,
- pub color_commands: Vec<LatexColorCommand>,
- pub color_model_commands: Vec<LatexColorModelCommand>,
- pub glossary_entry_definition_commands: Vec<LatexGlossaryEntryDefinitionCommand>,
- pub glossary_entry_reference_commands: Vec<LatexGlossaryEntryReferenceCommand>,
- pub entry_types: Vec<BibtexEntryTypeDoc>,
- pub fields: Vec<BibtexFieldDoc>,
- pub pgf_libraries: Vec<String>,
- pub tikz_libraries: Vec<String>,
- pub math_environments: Vec<String>,
- pub enum_environments: Vec<String>,
-}
-
-impl LanguageData {
- pub fn find_entry_type(&self, name: &str) -> Option<&BibtexEntryTypeDoc> {
- let name = name.to_lowercase();
- self.entry_types
- .iter()
- .find(|ty| ty.name.to_lowercase() == name)
- }
-
- pub fn entry_type_documentation(&self, name: &str) -> Option<&str> {
- self.find_entry_type(name)
- .and_then(|ty| ty.documentation.as_ref().map(AsRef::as_ref))
- }
-
- pub fn field_documentation(&self, name: &str) -> Option<&str> {
- self.fields
- .iter()
- .find(|field| field.name.to_lowercase() == name.to_lowercase())
- .map(|field| field.documentation.as_ref())
- }
-}
-
-pub static LANGUAGE_DATA: Lazy<LanguageData> = Lazy::new(|| {
- const JSON: &str = include_str!("../../data/lang_data.json");
- serde_json::from_str(JSON).expect("Failed to deserialize language.json")
-});
diff --git a/support/texlab/src/syntax/latex.rs b/support/texlab/src/syntax/latex.rs
new file mode 100644
index 0000000000..7839c1795b
--- /dev/null
+++ b/support/texlab/src/syntax/latex.rs
@@ -0,0 +1,36 @@
+mod analysis;
+mod cst;
+mod kind;
+mod lexer;
+mod parser;
+
+pub use self::{
+ analysis::*,
+ cst::*,
+ kind::SyntaxKind::{self, *},
+ parser::{parse, Parse},
+};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum Language {}
+
+impl cstree::Language for Language {
+ type Kind = SyntaxKind;
+
+ fn kind_from_raw(raw: cstree::SyntaxKind) -> Self::Kind {
+ assert!(raw.0 <= ROOT as u16);
+ unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
+ }
+
+ fn kind_to_raw(kind: Self::Kind) -> cstree::SyntaxKind {
+ kind.into()
+ }
+}
+
+pub type SyntaxNode = cstree::ResolvedNode<Language>;
+
+pub type SyntaxToken = cstree::ResolvedToken<Language>;
+
+pub type SyntaxElement = cstree::ResolvedElement<Language>;
+
+pub type SyntaxElementRef<'a> = cstree::ResolvedElementRef<'a, Language>;
diff --git a/support/texlab/src/syntax/latex/analysis.rs b/support/texlab/src/syntax/latex/analysis.rs
index af8b756f31..063d8e95f3 100644
--- a/support/texlab/src/syntax/latex/analysis.rs
+++ b/support/texlab/src/syntax/latex/analysis.rs
@@ -1,913 +1,36 @@
-use crate::{
- protocol::{Options, Position, Range, RangeExt, Uri},
- syntax::{generic_ast::AstNodeIndex, lang_data::*, latex::ast::*, text::SyntaxNode},
- tex::Resolver,
+mod command;
+mod distro_file;
+mod environment;
+mod explicit_link;
+mod implicit_link;
+mod label_name;
+mod label_number;
+mod theorem;
+mod types;
+
+use crate::syntax::latex;
+
+pub use self::types::*;
+use self::{
+ command::analyze_command,
+ environment::analyze_begin,
+ explicit_link::{analyze_import, analyze_include},
+ implicit_link::analyze_implicit_links,
+ label_name::analyze_label_name,
+ label_number::analyze_label_number,
+ theorem::analyze_theorem_definition,
};
-use itertools::{iproduct, Itertools};
-use serde::{Deserialize, Serialize};
-use std::{borrow::Cow, ops::Deref, path::Path};
-#[derive(Debug, Clone)]
-pub struct SymbolTableParams<'a> {
- pub tree: Tree,
- pub uri: &'a Uri,
- pub resolver: &'a Resolver,
- pub options: &'a Options,
- pub current_dir: &'a Path,
-}
-
-#[derive(Debug, Clone)]
-pub struct SymbolTable {
- pub(crate) tree: Tree,
- pub commands: Vec<AstNodeIndex>,
- pub environments: Vec<Environment>,
- pub is_standalone: bool,
- pub includes: Vec<Include>,
- pub imports: Vec<Import>,
- pub components: Vec<String>,
- pub citations: Vec<Citation>,
- pub command_definitions: Vec<CommandDefinition>,
- pub glossary_entries: Vec<GlossaryEntry>,
- pub equations: Vec<Equation>,
- pub inlines: Vec<Inline>,
- pub math_operators: Vec<MathOperator>,
- pub theorem_definitions: Vec<TheoremDefinition>,
- pub sections: Vec<Section>,
- pub labels: Vec<Label>,
- pub label_numberings: Vec<LabelNumbering>,
- pub captions: Vec<Caption>,
- pub items: Vec<Item>,
-}
-
-impl SymbolTable {
- pub fn analyze(params: SymbolTableParams) -> Self {
- let SymbolTableParams {
- tree,
- uri,
- resolver,
- options,
- current_dir,
- } = params;
-
- let commands: Vec<_> = tree.commands().collect();
- let ctx = SymbolContext {
- tree: &tree,
- commands: &commands,
- uri,
- resolver,
- options,
- current_dir,
- };
-
- let mut environments = None;
- let mut includes = None;
- let mut imports = None;
- let mut citations = None;
- let mut command_definitions = None;
- let mut glossary_entries = None;
- let mut equations = None;
- let mut inlines = None;
- let mut math_operators = None;
- let mut theorem_definitions = None;
- let mut sections = None;
- let mut labels = None;
- let mut label_numberings = None;
- let mut captions = None;
- let mut items = None;
-
- rayon::scope(|s| {
- s.spawn(|_| environments = Some(Environment::parse(ctx)));
- s.spawn(|_| includes = Some(Include::parse(ctx)));
- s.spawn(|_| imports = Some(Import::parse(ctx)));
- s.spawn(|_| citations = Some(Citation::parse(ctx)));
- s.spawn(|_| command_definitions = Some(CommandDefinition::parse(ctx)));
- s.spawn(|_| glossary_entries = Some(GlossaryEntry::parse(ctx)));
- s.spawn(|_| equations = Some(Equation::parse(ctx)));
- s.spawn(|_| inlines = Some(Inline::parse(ctx)));
- s.spawn(|_| math_operators = Some(MathOperator::parse(ctx)));
- s.spawn(|_| theorem_definitions = Some(TheoremDefinition::parse(ctx)));
- s.spawn(|_| sections = Some(Section::parse(ctx)));
- s.spawn(|_| labels = Some(Label::parse(ctx)));
- s.spawn(|_| label_numberings = Some(LabelNumbering::parse(ctx)));
- s.spawn(|_| captions = Some(Caption::parse(ctx)));
- s.spawn(|_| items = Some(Item::parse(ctx)));
- });
-
- let is_standalone = environments
- .as_ref()
- .unwrap()
- .iter()
- .any(|env| env.is_root(&tree));
-
- let components = includes
- .as_ref()
- .unwrap()
- .iter()
- .flat_map(|include| include.components(&tree))
- .collect();
-
- Self {
- tree,
- commands,
- environments: environments.unwrap(),
- is_standalone,
- includes: includes.unwrap(),
- imports: imports.unwrap(),
- components,
- citations: citations.unwrap(),
- command_definitions: command_definitions.unwrap(),
- glossary_entries: glossary_entries.unwrap(),
- equations: equations.unwrap(),
- inlines: inlines.unwrap(),
- math_operators: math_operators.unwrap(),
- theorem_definitions: theorem_definitions.unwrap(),
- sections: sections.unwrap(),
- labels: labels.unwrap(),
- label_numberings: label_numberings.unwrap(),
- captions: captions.unwrap(),
- items: items.unwrap(),
- }
- }
-
- pub fn is_direct_child(&self, env: Environment, pos: Position) -> bool {
- env.range(&self.tree).contains(pos)
- && !self
- .environments
- .iter()
- .filter(|e| e.left.parent != env.left.parent)
- .filter(|e| env.range(&self.tree).contains(e.range(&self.tree).start))
- .any(|e| e.range(&self.tree).contains(pos))
- }
-
- pub fn is_enum_item(&self, enumeration: Environment, item: Item) -> bool {
- let item_range = self.tree[item.parent].range();
- enumeration.range(&self.tree).contains(item_range.start)
- && !self
- .environments
- .iter()
- .filter(|env| env.left.parent != enumeration.left.parent)
- .filter(|env| env.left.is_enum(&self.tree))
- .filter(|env| {
- enumeration
- .range(&self.tree)
- .contains(env.range(&self.tree).start)
- })
- .any(|env| env.range(&self.tree).contains(item_range.start))
- }
-
- pub fn find_label_by_range(&self, range: Range) -> Option<&Label> {
- self.labels
- .iter()
- .filter(|label| label.kind == LatexLabelKind::Definition)
- .filter(|label| label.names(&self).len() == 1)
- .find(|label| range.contains(self[label.parent].range().start))
- }
-
- pub fn find_label_by_environment(&self, env: Environment) -> Option<&Label> {
- self.labels
- .iter()
- .filter(|label| label.kind == LatexLabelKind::Definition)
- .filter(|label| label.names(&self.tree).len() == 1)
- .find(|label| self.is_direct_child(env, self.tree[label.parent].start()))
- }
-}
-
-impl Deref for SymbolTable {
- type Target = Tree;
-
- fn deref(&self) -> &Self::Target {
- &self.tree
- }
-}
-
-#[derive(Debug, Clone, Copy)]
-pub struct SymbolContext<'a> {
- tree: &'a Tree,
- commands: &'a [AstNodeIndex],
- uri: &'a Uri,
- resolver: &'a Resolver,
- options: &'a Options,
- current_dir: &'a Path,
-}
-
-#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
-pub struct EnvironmentDelimiter {
- pub parent: AstNodeIndex,
-}
-
-impl EnvironmentDelimiter {
- pub fn name(self, tree: &Tree) -> Option<&Token> {
- tree.extract_word(self.parent, GroupKind::Group, 0)
- }
-
- pub fn is_math(self, tree: &Tree) -> bool {
- self.is_special(tree, LANGUAGE_DATA.math_environments.iter())
- }
-
- pub fn is_enum(self, tree: &Tree) -> bool {
- self.is_special(tree, LANGUAGE_DATA.enum_environments.iter())
- }
-
- fn is_special<'a, I: Iterator<Item = &'a String>>(self, tree: &Tree, mut values: I) -> bool {
- match self.name(tree) {
- Some(name) => values.any(|env| env == name.text()),
- None => false,
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
-pub struct Environment {
- pub left: EnvironmentDelimiter,
- pub right: EnvironmentDelimiter,
-}
-
-impl Environment {
- pub fn is_root(self, tree: &Tree) -> bool {
- self.left
- .name(tree)
- .iter()
- .chain(self.right.name(tree).iter())
- .any(|name| name.text() == "document")
- }
-
- pub fn range(self, tree: &Tree) -> Range {
- let start = tree[self.left.parent].start();
- let end = tree[self.right.parent].end();
- Range::new(start, end)
- }
-
- fn parse(ctx: SymbolContext) -> Vec<Self> {
- let mut stack = Vec::new();
- let mut envs = Vec::new();
- for parent in ctx.commands {
- if let Some((delim, delim_cmd)) = Self::parse_delimiter(ctx.tree, *parent) {
- if delim_cmd.name.text() == "\\begin" {
- stack.push(delim);
- } else if let Some(left) = stack.pop() {
- envs.push(Self { left, right: delim });
- }
- }
- }
- envs
- }
-
- fn parse_delimiter(
- tree: &Tree,
- parent: AstNodeIndex,
- ) -> Option<(EnvironmentDelimiter, &Command)> {
- let cmd = tree.as_command(parent)?;
- if cmd.name.text() != "\\begin" && cmd.name.text() != "\\end" {
- return None;
- }
-
- let group = tree.extract_group(parent, GroupKind::Group, 0)?;
- if tree.extract_word(parent, GroupKind::Group, 0).is_some()
- || tree.children(group).next().is_none()
- || tree.as_group(group)?.right.is_none()
- {
- Some((EnvironmentDelimiter { parent }, cmd))
- } else {
- None
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct Include {
- pub parent: AstNodeIndex,
- pub arg_index: usize,
- pub kind: LatexIncludeKind,
- pub all_targets: Vec<Vec<Uri>>,
- pub include_extension: bool,
-}
-
-impl Include {
- pub fn paths<'a>(&self, tree: &'a Tree) -> Vec<&'a Token> {
- tree.extract_comma_separated_words(self.parent, GroupKind::Group, self.arg_index)
- .unwrap()
- }
-
- pub fn components<'a>(&self, tree: &'a Tree) -> impl Iterator<Item = String> + 'a {
- let kind = self.kind;
- self.paths(tree)
- .into_iter()
- .filter_map(move |path| match kind {
- LatexIncludeKind::Package => Some(format!("{}.sty", path.text())),
- LatexIncludeKind::Class => Some(format!("{}.cls", path.text())),
- LatexIncludeKind::Latex
- | LatexIncludeKind::Bibliography
- | LatexIncludeKind::Image
- | LatexIncludeKind::Svg
- | LatexIncludeKind::Pdf
- | LatexIncludeKind::Everything => None,
- })
- }
-
- fn parse(ctx: SymbolContext) -> Vec<Self> {
- iproduct!(ctx.commands, LANGUAGE_DATA.include_commands.iter())
- .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc))
- .collect()
- }
-
- fn parse_single(
- ctx: SymbolContext,
- parent: AstNodeIndex,
- desc: &LatexIncludeCommand,
- ) -> Option<Self> {
- let cmd = ctx.tree.as_command(parent)?;
- if cmd.name.text() != desc.name {
- return None;
- }
-
- let mut all_targets = Vec::new();
- let paths = ctx
- .tree
- .extract_comma_separated_words(parent, GroupKind::Group, desc.index)?;
- for path in paths {
- let mut targets = Vec::new();
- let base_url = base_url(ctx)?;
- targets.push(base_url.join(path.text()).ok()?.into());
-
- if let Some(extensions) = desc.kind.extensions() {
- for extension in extensions {
- let path = format!("{}.{}", path.text(), extension);
- targets.push(base_url.join(&path).ok()?.into());
- }
- }
-
- if let Some(target) = Self::resolve_distro_file(ctx, desc, path.text()) {
- targets.push(target);
- }
- all_targets.push(targets);
- }
-
- let include = Self {
- parent,
- arg_index: desc.index,
- kind: desc.kind,
- all_targets,
- include_extension: desc.include_extension,
- };
- Some(include)
- }
-
- fn resolve_distro_file(
- ctx: SymbolContext,
- desc: &LatexIncludeCommand,
- name: &str,
- ) -> Option<Uri> {
- let mut path = ctx.resolver.files_by_name.get(name);
- if let Some(extensions) = desc.kind.extensions() {
- for extension in extensions {
- path = path.or_else(|| {
- let full_name = format!("{}.{}", name, extension);
- ctx.resolver.files_by_name.get(&full_name)
- });
- }
- }
- path.and_then(|p| Uri::from_file_path(p).ok())
- }
-}
-
-fn base_url(ctx: SymbolContext) -> Option<Uri> {
- if let Some(root_directory) = ctx
- .options
- .latex
- .as_ref()
- .and_then(|opts| opts.root_directory.as_ref())
- {
- let file_name = ctx.uri.path_segments()?.last()?;
- let path = ctx.current_dir.join(root_directory).join(file_name);
- Uri::from_file_path(path).ok()
- } else {
- Some(ctx.uri.clone())
- }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct Import {
- pub parent: AstNodeIndex,
- pub targets: Vec<Uri>,
-}
-
-impl Import {
- pub fn dir<'a>(&self, tree: &'a Tree) -> &'a Token {
- tree.extract_word(self.parent, GroupKind::Group, 0).unwrap()
- }
-
- pub fn file<'a>(&self, tree: &'a Tree) -> &'a Token {
- tree.extract_word(self.parent, GroupKind::Group, 1).unwrap()
- }
-
- fn parse(ctx: SymbolContext) -> Vec<Self> {
- ctx.commands
- .iter()
- .filter_map(|parent| Self::parse_single(ctx, *parent))
- .collect()
- }
-
- fn parse_single(ctx: SymbolContext, parent: AstNodeIndex) -> Option<Self> {
- let cmd = ctx.tree.as_command(parent)?;
- if cmd.name.text() != "\\import" && cmd.name.text() != "\\subimport" {
- return None;
- }
-
- let dir = ctx.tree.extract_word(parent, GroupKind::Group, 0)?;
- let file = ctx.tree.extract_word(parent, GroupKind::Group, 1)?;
-
- let mut targets = Vec::new();
- let base_url = base_url(ctx)?.join(dir.text()).ok()?;
- targets.push(base_url.join(file.text()).ok()?.into());
- targets.push(base_url.join(&format!("{}.tex", file.text())).ok()?.into());
- Some(Self { parent, targets })
- }
-}
-
-#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
-pub struct Citation {
- parent: AstNodeIndex,
- arg_index: usize,
-}
-
-impl Citation {
- pub fn keys(self, tree: &Tree) -> Vec<&Token> {
- tree.extract_comma_separated_words(self.parent, GroupKind::Group, self.arg_index)
- .unwrap()
- }
-
- fn parse(ctx: SymbolContext) -> Vec<Self> {
- iproduct!(ctx.commands, LANGUAGE_DATA.citation_commands.iter())
- .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc))
- .collect()
- }
-
- fn parse_single(
- ctx: SymbolContext,
- parent: AstNodeIndex,
- desc: &LatexCitationCommand,
- ) -> Option<Self> {
- let cmd = ctx.tree.as_command(parent)?;
- if cmd.name.text() != desc.name {
- return None;
- }
-
- ctx.tree
- .extract_comma_separated_words(parent, GroupKind::Group, desc.index)?;
-
- Some(Self {
- parent,
- arg_index: desc.index,
- })
- }
-}
-
-#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
-pub struct CommandDefinition {
- pub parent: AstNodeIndex,
- pub definition: AstNodeIndex,
- pub definition_index: usize,
- pub implementation: AstNodeIndex,
- pub implementation_index: usize,
- pub arg_count_index: usize,
-}
-
-impl CommandDefinition {
- pub fn definition_name(self, tree: &Tree) -> &str {
- tree.as_command(self.definition).unwrap().name.text()
- }
-
- fn parse(ctx: SymbolContext) -> Vec<Self> {
- let def = LANGUAGE_DATA.command_definition_commands.iter();
- iproduct!(ctx.commands, def)
- .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc))
- .collect()
- }
-
- fn parse_single(
- ctx: SymbolContext,
- parent: AstNodeIndex,
- desc: &LatexCommandDefinitionCommand,
- ) -> Option<Self> {
- let cmd = ctx.tree.as_command(parent)?;
- if cmd.name.text() != desc.name {
- return None;
- }
-
- let group_kind = GroupKind::Group;
- let implementation =
- ctx.tree
- .extract_group(parent, group_kind, desc.implementation_index)?;
-
- let def_group = ctx
- .tree
- .extract_group(parent, group_kind, desc.definition_index)?;
-
- let mut def_children = ctx.tree.children(def_group);
- let definition = def_children.next()?;
- ctx.tree.as_command(definition)?;
- Some(Self {
- parent,
- definition,
- definition_index: desc.definition_index,
- implementation,
- implementation_index: desc.implementation_index,
- arg_count_index: desc.arg_count_index,
- })
- }
-}
-
-#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
-pub struct GlossaryEntry {
- pub parent: AstNodeIndex,
- pub label_index: usize,
- pub kind: LatexGlossaryEntryKind,
-}
-
-impl GlossaryEntry {
- pub fn label(self, tree: &Tree) -> &Token {
- tree.extract_word(self.parent, GroupKind::Group, self.label_index)
- .unwrap()
- }
-
- fn parse(ctx: SymbolContext) -> Vec<Self> {
- let entry = LANGUAGE_DATA.glossary_entry_definition_commands.iter();
- iproduct!(ctx.commands, entry)
- .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc))
- .collect()
- }
-
- fn parse_single(
- ctx: SymbolContext,
- parent: AstNodeIndex,
- desc: &LatexGlossaryEntryDefinitionCommand,
- ) -> Option<Self> {
- let cmd = ctx.tree.as_command(parent)?;
- if cmd.name.text() != desc.name {
- return None;
- }
-
- ctx.tree
- .extract_word(parent, GroupKind::Group, desc.label_index)?;
-
- Some(Self {
- parent,
- label_index: desc.label_index,
- kind: desc.kind,
- })
- }
-}
-
-#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
-pub struct Equation {
- pub left: AstNodeIndex,
- pub right: AstNodeIndex,
-}
-
-impl Equation {
- pub fn range(self, tree: &Tree) -> Range {
- let start = tree[self.left].start();
- let end = tree[self.right].end();
- Range::new(start, end)
- }
-
- fn parse(ctx: SymbolContext) -> Vec<Self> {
- let mut equations = Vec::new();
- let mut left = None;
- for node in ctx.commands {
- let cmd = ctx.tree.as_command(*node).unwrap();
- let name = cmd.name.text();
- if name == "\\[" || name == "\\(" {
- left = Some(node);
- } else if name == "\\]" || name == "\\)" {
- if let Some(begin) = left {
- equations.push(Self {
- left: *begin,
- right: *node,
- });
- left = None;
- }
- }
- }
- equations
- }
-}
-
-#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
-pub struct Inline {
- pub left: AstNodeIndex,
- pub right: AstNodeIndex,
-}
-
-impl Inline {
- pub fn range(self, tree: &Tree) -> Range {
- let start = tree[self.left].start();
- let end = tree[self.right].end();
- Range::new(start, end)
- }
-
- fn parse(ctx: SymbolContext) -> Vec<Self> {
- let mut inlines = Vec::new();
- let mut left = None;
- for node in ctx
- .tree
- .nodes()
- .into_iter()
- .filter(|node| ctx.tree.as_math(*node).is_some())
- .sorted_by_key(|node| ctx.tree[*node].start())
- {
- if let Some(l) = left {
- inlines.push(Inline {
- left: l,
- right: node,
- });
- left = None;
- } else {
- left = Some(node);
- }
- }
- inlines
- }
-}
-
-#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
-pub struct MathOperator {
- pub parent: AstNodeIndex,
- pub definition: AstNodeIndex,
- pub definition_index: usize,
- pub implementation: AstNodeIndex,
- pub implementation_index: usize,
-}
-
-impl MathOperator {
- pub fn definition_name(self, tree: &Tree) -> &str {
- tree.as_command(self.definition).unwrap().name.text()
- }
-
- fn parse(ctx: SymbolContext) -> Vec<Self> {
- iproduct!(ctx.commands, LANGUAGE_DATA.math_operator_commands.iter())
- .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc))
- .collect()
- }
-
- fn parse_single(
- ctx: SymbolContext,
- parent: AstNodeIndex,
- desc: &LatexMathOperatorCommand,
- ) -> Option<Self> {
- let cmd = ctx.tree.as_command(parent)?;
- if cmd.name.text() != desc.name {
- return None;
- }
-
- let group_kind = GroupKind::Group;
- let def_group = ctx
- .tree
- .extract_group(parent, group_kind, desc.definition_index)?;
- let implementation =
- ctx.tree
- .extract_group(parent, group_kind, desc.implementation_index)?;
-
- let mut def_children = ctx.tree.children(def_group);
- let definition = def_children.next()?;
- Some(Self {
- parent,
- definition,
- definition_index: desc.definition_index,
- implementation,
- implementation_index: desc.implementation_index,
- })
- }
-}
-
-#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
-pub struct TheoremDefinition {
- pub parent: AstNodeIndex,
- pub arg_index: usize,
-}
-
-impl TheoremDefinition {
- pub fn name(self, tree: &Tree) -> &Token {
- tree.extract_word(self.parent, GroupKind::Group, self.arg_index)
- .unwrap()
- }
-
- fn parse(ctx: SymbolContext) -> Vec<Self> {
- let thm = LANGUAGE_DATA.theorem_definition_commands.iter();
- iproduct!(ctx.commands, thm)
- .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc))
- .collect()
- }
-
- fn parse_single(
- ctx: SymbolContext,
- parent: AstNodeIndex,
- desc: &LatexTheoremDefinitionCommand,
- ) -> Option<Self> {
- let cmd = ctx.tree.as_command(parent)?;
- if cmd.name.text() != desc.name {
- return None;
- }
-
- let group_kind = GroupKind::Group;
- ctx.tree.extract_word(parent, group_kind, desc.index)?;
-
- Some(Self {
- parent,
- arg_index: desc.index,
- })
- }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct Section {
- pub parent: AstNodeIndex,
- pub arg_index: usize,
- pub level: i32,
- pub prefix: Cow<'static, str>,
-}
-
-impl Section {
- pub fn print(&self, tree: &Tree) -> Option<String> {
- tree.print_group_content(self.parent, GroupKind::Group, self.arg_index)
- }
-
- fn parse(ctx: SymbolContext) -> Vec<Self> {
- iproduct!(ctx.commands, LANGUAGE_DATA.section_commands.iter())
- .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc))
- .collect()
- }
-
- fn parse_single(
- ctx: SymbolContext,
- parent: AstNodeIndex,
- desc: &'static LatexSectionCommand,
- ) -> Option<Self> {
- let cmd = ctx.tree.as_command(parent)?;
- if cmd.name.text() != desc.name {
- return None;
- }
-
- let group_kind = GroupKind::Group;
- ctx.tree.extract_group(parent, group_kind, desc.index)?;
-
- Some(Self {
- parent,
- arg_index: desc.index,
- level: desc.level,
- prefix: Cow::from(&desc.prefix),
- })
- }
-}
-
-#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
-pub struct Label {
- pub parent: AstNodeIndex,
- pub arg_index: usize,
- pub kind: LatexLabelKind,
-}
-
-impl Label {
- pub fn names(self, tree: &Tree) -> Vec<&Token> {
- tree.extract_comma_separated_words(self.parent, GroupKind::Group, self.arg_index)
- .unwrap()
- }
-
- fn parse(ctx: SymbolContext) -> Vec<Self> {
- iproduct!(ctx.commands, LANGUAGE_DATA.label_commands.iter())
- .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc))
- .collect()
- }
-
- fn parse_single(
- ctx: SymbolContext,
- parent: AstNodeIndex,
- desc: &LatexLabelCommand,
- ) -> Option<Self> {
- let cmd = ctx.tree.as_command(parent)?;
- if cmd.name.text() != desc.name {
- return None;
- }
-
- ctx.tree
- .extract_comma_separated_words(parent, GroupKind::Group, desc.index)?;
-
- Some(Self {
- parent,
- arg_index: desc.index,
- kind: desc.kind,
- })
- }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct LabelNumbering {
- pub parent: AstNodeIndex,
- pub number: String,
-}
-
-impl LabelNumbering {
- pub fn name<'a>(&self, tree: &'a Tree) -> &'a Token {
- tree.extract_word(self.parent, GroupKind::Group, 0).unwrap()
- }
-
- fn parse(ctx: SymbolContext) -> Vec<Self> {
- ctx.commands
- .iter()
- .filter_map(|parent| Self::parse_single(ctx, *parent))
- .collect()
- }
-
- fn parse_single(ctx: SymbolContext, parent: AstNodeIndex) -> Option<Self> {
- let cmd = ctx.tree.as_command(parent)?;
- if cmd.name.text() != "\\newlabel" {
- return None;
- }
-
- ctx.tree.extract_word(parent, GroupKind::Group, 0)?;
-
- let arg = ctx.tree.extract_group(parent, GroupKind::Group, 1)?;
- let mut analyzer = FirstText::default();
- analyzer.visit(ctx.tree, arg);
- Some(Self {
- parent,
- number: analyzer.text?,
- })
- }
-}
-
-#[derive(Debug, Default)]
-struct FirstText {
- text: Option<String>,
-}
-
-impl Visitor for FirstText {
- fn visit(&mut self, tree: &Tree, node: AstNodeIndex) {
- if let Some(text) = tree.as_text(node) {
- self.text = Some(text.words.iter().map(Token::text).join(" "));
- }
-
- if self.text.is_none() {
- tree.walk(self, node);
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
-pub struct Caption {
- pub parent: AstNodeIndex,
- pub arg_index: usize,
-}
-
-impl Caption {
- pub fn print(self, tree: &Tree) -> Option<String> {
- tree.print_group_content(self.parent, GroupKind::Group, self.arg_index)
- }
-
- fn parse(ctx: SymbolContext) -> Vec<Self> {
- ctx.commands
- .iter()
- .flat_map(|parent| Self::parse_single(ctx, *parent))
- .collect()
- }
-
- fn parse_single(ctx: SymbolContext, parent: AstNodeIndex) -> Option<Self> {
- let cmd = ctx.tree.as_command(parent)?;
- if cmd.name.text() != "\\caption" {
- return None;
- }
-
- ctx.tree.extract_group(parent, GroupKind::Group, 0)?;
- Some(Self {
- parent,
- arg_index: 0,
- })
- }
-}
-
-#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
-pub struct Item {
- pub parent: AstNodeIndex,
-}
-
-impl Item {
- pub fn name(self, tree: &Tree) -> Option<String> {
- tree.print_group_content(self.parent, GroupKind::Options, 0)
- }
-
- fn parse(ctx: SymbolContext) -> Vec<Self> {
- ctx.commands
- .iter()
- .filter_map(|parent| Self::parse_single(ctx, *parent))
- .collect()
- }
-
- fn parse_single(ctx: SymbolContext, parent: AstNodeIndex) -> Option<Self> {
- let cmd = ctx.tree.as_command(parent)?;
- if cmd.name.text() != "\\item" {
- return None;
- }
-
- Some(Self { parent })
- }
+pub fn analyze(context: &mut LatexAnalyzerContext, root: &latex::SyntaxNode) {
+ analyze_implicit_links(context);
+ for node in root.descendants() {
+ analyze_command(context, node)
+ .or_else(|| analyze_begin(context, node))
+ .or_else(|| analyze_include(context, node))
+ .or_else(|| analyze_import(context, node))
+ .or_else(|| analyze_label_name(context, node))
+ .or_else(|| analyze_label_number(context, node))
+ .or_else(|| analyze_theorem_definition(context, node));
+ }
+ context.extras.has_document_environment = context.extras.environment_names.contains("document");
}
diff --git a/support/texlab/src/syntax/latex/analysis/command.rs b/support/texlab/src/syntax/latex/analysis/command.rs
new file mode 100644
index 0000000000..9dea4d84ee
--- /dev/null
+++ b/support/texlab/src/syntax/latex/analysis/command.rs
@@ -0,0 +1,12 @@
+use crate::syntax::{latex, CstNode};
+
+use super::LatexAnalyzerContext;
+
+pub fn analyze_command(context: &mut LatexAnalyzerContext, node: &latex::SyntaxNode) -> Option<()> {
+ let command = latex::GenericCommand::cast(node)?;
+ context
+ .extras
+ .command_names
+ .insert(command.name()?.text().into());
+ Some(())
+}
diff --git a/support/texlab/src/syntax/latex/analysis/distro_file.rs b/support/texlab/src/syntax/latex/analysis/distro_file.rs
new file mode 100644
index 0000000000..4c747ce760
--- /dev/null
+++ b/support/texlab/src/syntax/latex/analysis/distro_file.rs
@@ -0,0 +1,71 @@
+use crate::{distro::Resolver, Uri};
+
+pub fn resolve_distro_file(resolver: &Resolver, stem: &str, extensions: &[&str]) -> Option<Uri> {
+ let mut document = resolver.files_by_name.get(stem);
+ for extension in extensions {
+ document = document.or_else(|| {
+ let full_name = format!("{}.{}", stem, extension);
+ resolver.files_by_name.get(full_name.as_str())
+ });
+ }
+ document.and_then(|path| Uri::from_file_path(path).ok())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ #[cfg(windows)]
+ fn test() {
+ let mut resolver = Resolver::default();
+ resolver
+ .files_by_name
+ .insert("foo.tex".into(), "C:/distro/foo.tex".into());
+ resolver
+ .files_by_name
+ .insert("foo.sty".into(), "C:/distro/foo.sty".into());
+ resolver
+ .files_by_name
+ .insert("bar.tex".into(), "C:/distro/bar.tex".into());
+
+ assert_eq!(
+ resolve_distro_file(&resolver, "foo", &["tex"]),
+ Some(Uri::from_file_path("C:/distro/foo.tex").unwrap())
+ );
+
+ assert_eq!(
+ resolve_distro_file(&resolver, "foo", &["sty"]),
+ Some(Uri::from_file_path("C:/distro/foo.sty").unwrap())
+ );
+
+ assert_eq!(resolve_distro_file(&resolver, "foo", &["cls"]), None);
+ }
+
+ #[test]
+ #[cfg(unix)]
+ fn test() {
+ let mut resolver = Resolver::default();
+ resolver
+ .files_by_name
+ .insert("foo.tex".into(), "/distro/foo.tex".into());
+ resolver
+ .files_by_name
+ .insert("foo.sty".into(), "/distro/foo.sty".into());
+ resolver
+ .files_by_name
+ .insert("bar.tex".into(), "/distro/bar.tex".into());
+
+ assert_eq!(
+ resolve_distro_file(&resolver, "foo", &["tex"]),
+ Some(Uri::from_file_path("/distro/foo.tex").unwrap())
+ );
+
+ assert_eq!(
+ resolve_distro_file(&resolver, "foo", &["sty"]),
+ Some(Uri::from_file_path("/distro/foo.sty").unwrap())
+ );
+
+ assert_eq!(resolve_distro_file(&resolver, "foo", &["cls"]), None);
+ }
+}
diff --git a/support/texlab/src/syntax/latex/analysis/environment.rs b/support/texlab/src/syntax/latex/analysis/environment.rs
new file mode 100644
index 0000000000..3b83b40b2f
--- /dev/null
+++ b/support/texlab/src/syntax/latex/analysis/environment.rs
@@ -0,0 +1,11 @@
+use crate::syntax::{latex, CstNode};
+
+use super::LatexAnalyzerContext;
+
+pub fn analyze_begin(context: &mut LatexAnalyzerContext, node: &latex::SyntaxNode) -> Option<()> {
+ let begin = latex::Begin::cast(node)?;
+ let name = begin.name()?.word()?.text();
+ let extras = &mut context.extras;
+ extras.environment_names.insert(name.into());
+ Some(())
+}
diff --git a/support/texlab/src/syntax/latex/analysis/explicit_link.rs b/support/texlab/src/syntax/latex/analysis/explicit_link.rs
new file mode 100644
index 0000000000..363a576aaa
--- /dev/null
+++ b/support/texlab/src/syntax/latex/analysis/explicit_link.rs
@@ -0,0 +1,72 @@
+use std::sync::Arc;
+
+use crate::syntax::{latex, CstNode};
+
+use super::{
+ distro_file::resolve_distro_file, ExplicitLink, ExplicitLinkKind, LatexAnalyzerContext,
+};
+
+pub fn analyze_include(context: &mut LatexAnalyzerContext, node: &latex::SyntaxNode) -> Option<()> {
+ let include = latex::Include::cast(node)?;
+ let kind = match include.syntax().kind() {
+ latex::LATEX_INCLUDE => ExplicitLinkKind::Latex,
+ latex::BIBLATEX_INCLUDE | latex::BIBTEX_INCLUDE => ExplicitLinkKind::Bibtex,
+ latex::PACKAGE_INCLUDE => ExplicitLinkKind::Package,
+ latex::CLASS_INCLUDE => ExplicitLinkKind::Class,
+ _ => return None,
+ };
+
+ let extensions = match kind {
+ ExplicitLinkKind::Latex => &["tex"],
+ ExplicitLinkKind::Bibtex => &["bib"],
+ ExplicitLinkKind::Package => &["sty"],
+ ExplicitLinkKind::Class => &["cls"],
+ };
+
+ for path in include.path_list()?.words() {
+ let stem = path.text();
+ let mut targets = vec![Arc::new(context.base_uri.join(stem).ok()?.into())];
+ for extension in extensions {
+ let path = format!("{}.{}", stem, extension);
+ targets.push(Arc::new(context.base_uri.join(&path).ok()?.into()));
+ }
+
+ resolve_distro_file(&context.inner.resolver.lock().unwrap(), stem, extensions)
+ .into_iter()
+ .for_each(|target| targets.push(Arc::new(target)));
+
+ context.extras.explicit_links.push(ExplicitLink {
+ kind,
+ stem: stem.into(),
+ stem_range: path.text_range(),
+ targets,
+ });
+ }
+
+ Some(())
+}
+
+pub fn analyze_import(context: &mut LatexAnalyzerContext, node: &latex::SyntaxNode) -> Option<()> {
+ let import = latex::Import::cast(node)?;
+
+ let mut targets = Vec::new();
+ let directory = context
+ .base_uri
+ .join(import.directory()?.word()?.text())
+ .ok()?;
+
+ let file = import.file()?.word()?;
+ let stem = file.text();
+ targets.push(Arc::new(directory.join(stem).ok()?.into()));
+ targets.push(Arc::new(
+ directory.join(&format!("{}.tex", stem)).ok()?.into(),
+ ));
+
+ context.extras.explicit_links.push(ExplicitLink {
+ stem: stem.into(),
+ stem_range: file.text_range(),
+ targets,
+ kind: ExplicitLinkKind::Latex,
+ });
+ Some(())
+}
diff --git a/support/texlab/src/syntax/latex/analysis/implicit_link.rs b/support/texlab/src/syntax/latex/analysis/implicit_link.rs
new file mode 100644
index 0000000000..74055b3e31
--- /dev/null
+++ b/support/texlab/src/syntax/latex/analysis/implicit_link.rs
@@ -0,0 +1,41 @@
+use std::sync::Arc;
+
+use crate::Uri;
+
+use super::LatexAnalyzerContext;
+
+pub fn analyze_implicit_links(context: &mut LatexAnalyzerContext) {
+ context.extras.implicit_links.aux = find_by_extension(context, "aux").unwrap_or_default();
+ context.extras.implicit_links.log = find_by_extension(context, "log").unwrap_or_default();
+ context.extras.implicit_links.pdf = find_by_extension(context, "pdf").unwrap_or_default();
+}
+
+fn find_by_extension(context: &LatexAnalyzerContext, extension: &str) -> Option<Vec<Arc<Uri>>> {
+ let mut targets: Vec<Arc<Uri>> = Vec::new();
+ targets.push(Arc::new(context.document_uri.with_extension(extension)?));
+ if context.document_uri.scheme() == "file" {
+ let file_path = context.document_uri.to_file_path().ok()?;
+ let file_stem = file_path.file_stem()?;
+ let aux_name = format!("{}.{}", file_stem.to_str()?, extension);
+
+ let options = context.inner.options.read().unwrap();
+ if let Some(root_dir) = options.root_directory.as_ref() {
+ let path = context
+ .inner
+ .current_directory
+ .join(root_dir)
+ .join(&aux_name);
+ targets.push(Arc::new(Uri::from_file_path(path).ok()?));
+ }
+
+ if let Some(build_dir) = options.aux_directory.as_ref() {
+ let path = context
+ .inner
+ .current_directory
+ .join(build_dir)
+ .join(&aux_name);
+ targets.push(Arc::new(Uri::from_file_path(path).ok()?));
+ }
+ }
+ Some(targets)
+}
diff --git a/support/texlab/src/syntax/latex/analysis/label_name.rs b/support/texlab/src/syntax/latex/analysis/label_name.rs
new file mode 100644
index 0000000000..d0410b953c
--- /dev/null
+++ b/support/texlab/src/syntax/latex/analysis/label_name.rs
@@ -0,0 +1,66 @@
+use latex::LabelReferenceRange;
+
+use crate::syntax::{latex, CstNode};
+
+use super::{LabelName, LatexAnalyzerContext};
+
+pub fn analyze_label_name(
+ context: &mut LatexAnalyzerContext,
+ node: &latex::SyntaxNode,
+) -> Option<()> {
+ analyze_label_definition_name(context, node)
+ .or_else(|| analyze_label_reference_name(context, node))
+ .or_else(|| analyze_label_reference_range_name(context, node))
+}
+
+fn analyze_label_definition_name(
+ context: &mut LatexAnalyzerContext,
+ node: &latex::SyntaxNode,
+) -> Option<()> {
+ let label = latex::LabelDefinition::cast(node)?;
+ let name = label.name()?.word()?;
+ context.extras.label_names.push(LabelName {
+ text: name.text().into(),
+ range: name.text_range(),
+ is_definition: true,
+ });
+ Some(())
+}
+
+fn analyze_label_reference_name(
+ context: &mut LatexAnalyzerContext,
+ node: &latex::SyntaxNode,
+) -> Option<()> {
+ let label = latex::LabelReference::cast(node)?;
+ for name in label.name_list()?.words() {
+ context.extras.label_names.push(LabelName {
+ text: name.text().into(),
+ range: name.text_range(),
+ is_definition: false,
+ });
+ }
+ Some(())
+}
+
+fn analyze_label_reference_range_name(
+ context: &mut LatexAnalyzerContext,
+ node: &latex::SyntaxNode,
+) -> Option<()> {
+ let label = LabelReferenceRange::cast(node)?;
+ if let Some(name1) = label.from().and_then(|name| name.word()) {
+ context.extras.label_names.push(LabelName {
+ text: name1.text().into(),
+ range: name1.text_range(),
+ is_definition: false,
+ });
+ }
+
+ if let Some(name2) = label.to().and_then(|name| name.word()) {
+ context.extras.label_names.push(LabelName {
+ text: name2.text().into(),
+ range: name2.text_range(),
+ is_definition: false,
+ });
+ }
+ Some(())
+}
diff --git a/support/texlab/src/syntax/latex/analysis/label_number.rs b/support/texlab/src/syntax/latex/analysis/label_number.rs
new file mode 100644
index 0000000000..de8d0dc1e1
--- /dev/null
+++ b/support/texlab/src/syntax/latex/analysis/label_number.rs
@@ -0,0 +1,22 @@
+use crate::syntax::{latex, CstNode};
+
+use super::LatexAnalyzerContext;
+
+pub fn analyze_label_number(
+ context: &mut LatexAnalyzerContext,
+ node: &latex::SyntaxNode,
+) -> Option<()> {
+ let number = latex::LabelNumber::cast(node)?;
+ let name = number.name()?.word()?.text().into();
+ let text = number
+ .text()?
+ .syntax()
+ .descendants_with_tokens()
+ .filter_map(|element| element.into_node())
+ .find(|node| node.kind() == latex::TEXT || node.kind() == latex::MIXED_GROUP)?
+ .text()
+ .to_string();
+
+ context.extras.label_numbers_by_name.insert(name, text);
+ Some(())
+}
diff --git a/support/texlab/src/syntax/latex/analysis/theorem.rs b/support/texlab/src/syntax/latex/analysis/theorem.rs
new file mode 100644
index 0000000000..0c365f0ef5
--- /dev/null
+++ b/support/texlab/src/syntax/latex/analysis/theorem.rs
@@ -0,0 +1,23 @@
+use crate::syntax::{
+ latex::{self, HasCurly},
+ CstNode,
+};
+
+use super::{LatexAnalyzerContext, TheoremEnvironment};
+
+pub fn analyze_theorem_definition(
+ context: &mut LatexAnalyzerContext,
+ node: &latex::SyntaxNode,
+) -> Option<()> {
+ let theorem = latex::TheoremDefinition::cast(node)?;
+ let name = theorem.name()?.word()?.text().into();
+ let description = theorem.description()?;
+ let description = description.content_text()?;
+
+ context
+ .extras
+ .theorem_environments
+ .push(TheoremEnvironment { name, description });
+
+ Some(())
+}
diff --git a/support/texlab/src/syntax/latex/analysis/types.rs b/support/texlab/src/syntax/latex/analysis/types.rs
new file mode 100644
index 0000000000..f089721397
--- /dev/null
+++ b/support/texlab/src/syntax/latex/analysis/types.rs
@@ -0,0 +1,73 @@
+use std::sync::Arc;
+
+use cstree::TextRange;
+use rustc_hash::{FxHashMap, FxHashSet};
+use smol_str::SmolStr;
+
+use crate::{ServerContext, Uri};
+
+#[derive(Debug)]
+pub struct LatexAnalyzerContext {
+ pub inner: Arc<ServerContext>,
+ pub document_uri: Arc<Uri>,
+ pub base_uri: Arc<Uri>,
+ pub extras: Extras,
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct Extras {
+ pub implicit_links: ImplicitLinks,
+ pub explicit_links: Vec<ExplicitLink>,
+ pub has_document_environment: bool,
+ pub command_names: FxHashSet<SmolStr>,
+ pub environment_names: FxHashSet<SmolStr>,
+ pub label_names: Vec<LabelName>,
+ pub label_numbers_by_name: FxHashMap<SmolStr, String>,
+ pub theorem_environments: Vec<TheoremEnvironment>,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Default, Hash)]
+pub struct ImplicitLinks {
+ pub aux: Vec<Arc<Uri>>,
+ pub log: Vec<Arc<Uri>>,
+ pub pdf: Vec<Arc<Uri>>,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord, Hash)]
+pub enum ExplicitLinkKind {
+ Package,
+ Class,
+ Latex,
+ Bibtex,
+}
+
+#[derive(Debug, Clone)]
+pub struct ExplicitLink {
+ pub stem: SmolStr,
+ pub stem_range: TextRange,
+ pub targets: Vec<Arc<Uri>>,
+ pub kind: ExplicitLinkKind,
+}
+
+impl ExplicitLink {
+ pub fn as_component_name(&self) -> Option<String> {
+ match self.kind {
+ ExplicitLinkKind::Package => Some(format!("{}.sty", self.stem)),
+ ExplicitLinkKind::Class => Some(format!("{}.cls", self.stem)),
+ ExplicitLinkKind::Latex | ExplicitLinkKind::Bibtex => None,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Default, Hash)]
+pub struct TheoremEnvironment {
+ pub name: SmolStr,
+ pub description: String,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Default, Hash)]
+pub struct LabelName {
+ pub text: SmolStr,
+ pub range: TextRange,
+ pub is_definition: bool,
+}
diff --git a/support/texlab/src/syntax/latex/ast.rs b/support/texlab/src/syntax/latex/ast.rs
deleted file mode 100644
index ab7c6863e1..0000000000
--- a/support/texlab/src/syntax/latex/ast.rs
+++ /dev/null
@@ -1,414 +0,0 @@
-use crate::{
- protocol::{Position, Range, RangeExt},
- syntax::{
- generic_ast::{Ast, AstNodeIndex},
- text::{Span, SyntaxNode},
- },
-};
-use serde::{Deserialize, Serialize};
-use std::ops::Deref;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
-pub enum TokenKind {
- Word,
- Command,
- Math,
- Comma,
- BeginGroup,
- EndGroup,
- BeginOptions,
- EndOptions,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct Token {
- pub span: Span,
- pub kind: TokenKind,
-}
-
-impl Token {
- pub fn new(span: Span, kind: TokenKind) -> Self {
- Self { span, kind }
- }
-
- pub fn text(&self) -> &str {
- &self.span.text
- }
-}
-
-impl SyntaxNode for Token {
- fn range(&self) -> Range {
- self.span.range()
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
-pub struct Root {
- pub range: Range,
-}
-
-impl SyntaxNode for Root {
- fn range(&self) -> Range {
- self.range
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
-pub enum GroupKind {
- Group,
- Options,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct Group {
- pub range: Range,
- pub left: Token,
- pub right: Option<Token>,
- pub kind: GroupKind,
-}
-
-impl SyntaxNode for Group {
- fn range(&self) -> Range {
- self.range
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct Command {
- pub range: Range,
- pub name: Token,
-}
-
-impl Command {
- pub fn short_name_range(&self) -> Range {
- Range::new_simple(
- self.name.start().line,
- self.name.start().character + 1,
- self.name.end().line,
- self.name.end().character,
- )
- }
-}
-
-impl SyntaxNode for Command {
- fn range(&self) -> Range {
- self.range
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct Text {
- pub range: Range,
- pub words: Vec<Token>,
-}
-
-impl SyntaxNode for Text {
- fn range(&self) -> Range {
- self.range
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct Comma {
- pub range: Range,
- pub token: Token,
-}
-
-impl SyntaxNode for Comma {
- fn range(&self) -> Range {
- self.range
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct Math {
- pub range: Range,
- pub token: Token,
-}
-
-impl SyntaxNode for Math {
- fn range(&self) -> Range {
- self.range
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub enum Node {
- Root(Root),
- Group(Group),
- Command(Command),
- Text(Text),
- Comma(Comma),
- Math(Math),
-}
-
-impl SyntaxNode for Node {
- fn range(&self) -> Range {
- match self {
- Self::Root(root) => root.range(),
- Self::Group(group) => group.range(),
- Self::Command(cmd) => cmd.range(),
- Self::Text(text) => text.range(),
- Self::Comma(comma) => comma.range(),
- Self::Math(math) => math.range(),
- }
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub struct Tree {
- pub inner: Ast<Node>,
- pub root: AstNodeIndex,
-}
-
-impl Deref for Tree {
- type Target = Ast<Node>;
-
- fn deref(&self) -> &Self::Target {
- &self.inner
- }
-}
-
-impl Tree {
- pub fn walk<V: Visitor>(&self, visitor: &mut V, parent: AstNodeIndex) {
- for child in self.children(parent) {
- visitor.visit(self, child);
- }
- }
-
- pub fn find(&self, pos: Position) -> Vec<AstNodeIndex> {
- let mut finder = Finder::new(pos);
- finder.visit(self, self.root);
- finder.results
- }
-
- pub fn find_command_by_short_name_range(&self, pos: Position) -> Option<AstNodeIndex> {
- self.find(pos).into_iter().find(|node| {
- self.as_command(*node)
- .filter(|cmd| {
- cmd.name.range().contains(pos) && cmd.name.start().character != pos.character
- })
- .is_some()
- })
- }
-
- pub fn print(&self, node: AstNodeIndex) -> String {
- let start_position = self[node].start();
- let mut printer = Printer::new(start_position);
- printer.visit(self, node);
- printer.output
- }
-
- pub fn commands<'a>(&'a self) -> impl Iterator<Item = AstNodeIndex> + 'a {
- self.inner
- .nodes()
- .into_iter()
- .filter(move |node| self.as_command(*node).is_some())
- }
-
- pub fn as_group(&self, node: AstNodeIndex) -> Option<&Group> {
- if let Node::Group(group) = &self[node] {
- Some(group)
- } else {
- None
- }
- }
-
- pub fn as_command(&self, node: AstNodeIndex) -> Option<&Command> {
- if let Node::Command(cmd) = &self[node] {
- Some(cmd)
- } else {
- None
- }
- }
-
- pub fn as_text(&self, node: AstNodeIndex) -> Option<&Text> {
- if let Node::Text(text) = &self[node] {
- Some(text)
- } else {
- None
- }
- }
-
- pub fn as_math(&self, node: AstNodeIndex) -> Option<&Math> {
- if let Node::Math(math) = &self[node] {
- Some(math)
- } else {
- None
- }
- }
-
- pub fn extract_group(
- &self,
- parent: AstNodeIndex,
- group_kind: GroupKind,
- index: usize,
- ) -> Option<AstNodeIndex> {
- self.children(parent)
- .filter(|child| {
- self.as_group(*child)
- .filter(|group| group.kind == group_kind)
- .is_some()
- })
- .nth(index)
- }
-
- pub fn extract_text(
- &self,
- parent: AstNodeIndex,
- group_kind: GroupKind,
- index: usize,
- ) -> Option<&Text> {
- let group = self.extract_group(parent, group_kind, index)?;
- let mut contents = self.children(group);
- let text = self.as_text(contents.next()?);
- if contents.next().is_none() {
- text
- } else {
- None
- }
- }
-
- pub fn extract_word(
- &self,
- parent: AstNodeIndex,
- group_kind: GroupKind,
- index: usize,
- ) -> Option<&Token> {
- let text = self.extract_text(parent, group_kind, index)?;
- if text.words.len() == 1 {
- Some(&text.words[0])
- } else {
- None
- }
- }
-
- pub fn extract_comma_separated_words(
- &self,
- parent: AstNodeIndex,
- group_kind: GroupKind,
- index: usize,
- ) -> Option<Vec<&Token>> {
- let group = self.extract_group(parent, group_kind, index)?;
- let mut words = Vec::new();
- for child in self.children(group) {
- match &self[child] {
- Node::Root(_) | Node::Group(_) | Node::Command(_) | Node::Math(_) => return None,
- Node::Text(text) => {
- for word in &text.words {
- words.push(word);
- }
- }
- Node::Comma(_) => (),
- }
- }
- Some(words)
- }
-
- pub fn print_group_content(
- &self,
- parent: AstNodeIndex,
- group_kind: GroupKind,
- index: usize,
- ) -> Option<String> {
- let arg = self.extract_group(parent, group_kind, index)?;
- let text = self.print(arg);
- self.as_group(arg)?.right.as_ref()?;
- Some(text[1..text.len() - 1].trim().into())
- }
-}
-
-pub trait Visitor {
- fn visit(&mut self, tree: &Tree, node: AstNodeIndex);
-}
-
-#[derive(Debug)]
-struct Finder {
- position: Position,
- results: Vec<AstNodeIndex>,
-}
-
-impl Finder {
- fn new(position: Position) -> Self {
- Self {
- position,
- results: Vec::new(),
- }
- }
-}
-
-impl Visitor for Finder {
- fn visit(&mut self, tree: &Tree, node: AstNodeIndex) {
- if tree[node].range().contains(self.position) {
- self.results.push(node);
- tree.walk(self, node);
- }
- }
-}
-
-#[derive(Debug)]
-struct Printer {
- output: String,
- position: Position,
-}
-
-impl Printer {
- fn new(start_position: Position) -> Self {
- Self {
- output: String::new(),
- position: start_position,
- }
- }
-
- fn synchronize(&mut self, position: Position) {
- while self.position.line < position.line {
- self.output.push('\n');
- self.position.line += 1;
- self.position.character = 0;
- }
-
- while self.position.character < position.character {
- self.output.push(' ');
- self.position.character += 1;
- }
-
- assert_eq!(self.position, position);
- }
-
- fn print_token(&mut self, token: &Token) {
- self.synchronize(token.start());
- self.output.push_str(token.text());
- self.position.character += token.end().character - token.start().character;
- self.synchronize(token.end());
- }
-}
-
-impl Visitor for Printer {
- fn visit(&mut self, tree: &Tree, node: AstNodeIndex) {
- match &tree[node] {
- Node::Root(_) => tree.walk(self, node),
- Node::Group(group) => {
- self.print_token(&group.left);
- tree.walk(self, node);
- if let Some(right) = &group.right {
- self.print_token(right);
- }
- }
- Node::Command(cmd) => {
- self.print_token(&cmd.name);
- tree.walk(self, node);
- }
- Node::Text(text) => {
- for word in &text.words {
- self.print_token(word);
- }
- }
- Node::Comma(comma) => {
- self.print_token(&comma.token);
- }
- Node::Math(math) => {
- self.print_token(&math.token);
- }
- }
- }
-}
diff --git a/support/texlab/src/syntax/latex/cst.rs b/support/texlab/src/syntax/latex/cst.rs
new file mode 100644
index 0000000000..0b839b706d
--- /dev/null
+++ b/support/texlab/src/syntax/latex/cst.rs
@@ -0,0 +1,639 @@
+use cstree::TextRange;
+
+use crate::syntax::CstNode;
+
+use super::{Language, SyntaxKind::*, SyntaxNode, SyntaxToken};
+
+macro_rules! cst_node {
+ ($name:ident, $($kind:pat),+) => {
+ #[derive(Clone)]
+ #[repr(transparent)]
+ pub struct $name<'a>(&'a SyntaxNode);
+
+ impl<'a> CstNode<'a> for $name<'a> {
+ type Lang = Language;
+
+ fn cast(node: &'a cstree::ResolvedNode<Self::Lang>) -> Option<Self>
+ where
+ Self: Sized,
+ {
+ match node.kind() {
+ $($kind => Some(Self(node)),)+
+ _ => None,
+ }
+ }
+
+ fn syntax(&self) -> &'a cstree::ResolvedNode<Self::Lang> {
+ &self.0
+ }
+
+ fn small_range(&self) -> TextRange {
+ let full_range = self.syntax().text_range();
+ let start = full_range.start();
+ let mut token = self.syntax().last_token();
+ while let Some(current) = token {
+ if !matches!(current.kind(), WHITESPACE | COMMENT) {
+ return TextRange::new(start, current.text_range().end());
+ }
+ token = current.prev_token();
+ }
+ TextRange::new(start, start)
+ }
+ }
+ };
+}
+
+cst_node!(Text, TEXT);
+
+pub trait HasCurly<'a>: CstNode<'a, Lang = Language> {
+ fn left_curly(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == L_CURLY)
+ }
+
+ fn right_curly(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == R_CURLY)
+ }
+
+ fn content_text(&self) -> Option<String> {
+ self.left_curly()?;
+ self.right_curly()?;
+ let mut text = String::new();
+ for child in self
+ .syntax()
+ .descendants_with_tokens()
+ .filter_map(|child| child.into_token())
+ .filter(|token| !matches!(token.kind(), COMMENT))
+ {
+ text.push_str(child.text());
+ }
+ let text = text.trim_end();
+ let text = text[1..text.len() - 1].trim().to_string();
+
+ Some(text)
+ }
+}
+
+cst_node!(CurlyGroup, CURLY_GROUP);
+
+impl<'a> HasCurly<'a> for CurlyGroup<'a> {}
+
+impl<'a> CurlyGroup<'a> {}
+
+pub trait HasBrack<'a>: CstNode<'a, Lang = Language> {
+ fn left_brack(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == L_BRACK)
+ }
+
+ fn right_brack(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == R_BRACK)
+ }
+
+ fn content_text(&self) -> Option<String> {
+ self.left_brack()?;
+ self.right_brack()?;
+ let mut text = String::new();
+ for child in self
+ .syntax()
+ .descendants_with_tokens()
+ .filter_map(|child| child.into_token())
+ .filter(|token| !matches!(token.kind(), COMMENT))
+ {
+ text.push_str(child.text());
+ }
+ let text = text.trim_end();
+ let text = text[1..text.len() - 1].trim().to_string();
+
+ Some(text)
+ }
+}
+
+cst_node!(BrackGroup, BRACK_GROUP);
+
+impl<'a> BrackGroup<'a> {}
+
+impl<'a> HasBrack<'a> for BrackGroup<'a> {}
+
+pub trait HasParen<'a>: CstNode<'a, Lang = Language> {
+ fn left_paren(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == L_PAREN)
+ }
+
+ fn right_paren(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == R_PAREN)
+ }
+}
+
+cst_node!(ParenGroup, PAREN_GROUP);
+
+impl<'a> HasParen<'a> for ParenGroup<'a> {}
+
+cst_node!(MixedGroup, MIXED_GROUP);
+
+impl<'a> MixedGroup<'a> {
+ pub fn left_delim(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| matches!(node.kind().into(), L_BRACK | L_PAREN))
+ }
+
+ pub fn right_delim(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| matches!(node.kind().into(), R_BRACK | R_PAREN))
+ }
+}
+
+cst_node!(CurlyGroupWord, CURLY_GROUP_WORD);
+
+impl<'a> HasCurly<'a> for CurlyGroupWord<'a> {}
+
+impl<'a> CurlyGroupWord<'a> {
+ pub fn word(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == WORD)
+ }
+}
+
+cst_node!(BrackGroupWord, BRACK_GROUP_WORD);
+
+impl<'a> HasBrack<'a> for BrackGroupWord<'a> {}
+
+impl<'a> BrackGroupWord<'a> {
+ pub fn word(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == WORD)
+ }
+}
+
+cst_node!(CurlyGroupWordList, CURLY_GROUP_WORD_LIST);
+
+impl<'a> HasCurly<'a> for CurlyGroupWordList<'a> {}
+
+impl<'a> CurlyGroupWordList<'a> {
+ pub fn words(&self) -> impl Iterator<Item = &'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .filter(|node| node.kind() == WORD)
+ }
+}
+
+cst_node!(CurlyGroupCommand, CURLY_GROUP_COMMAND);
+
+impl<'a> HasCurly<'a> for CurlyGroupCommand<'a> {}
+
+impl<'a> CurlyGroupCommand<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == GENERIC_COMMAND_NAME)
+ }
+}
+
+cst_node!(Key, KEY);
+
+impl<'a> Key<'a> {
+ pub fn words(&self) -> impl Iterator<Item = &'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .filter(|node| node.kind() == WORD)
+ }
+}
+
+cst_node!(Value, VALUE);
+
+cst_node!(KeyValuePair, KEY_VALUE_PAIR);
+
+impl<'a> KeyValuePair<'a> {
+ pub fn key(&self) -> Option<Key<'a>> {
+ self.syntax().children().find_map(Key::cast)
+ }
+
+ pub fn value(&self) -> Option<Value<'a>> {
+ self.syntax().children().find_map(Value::cast)
+ }
+}
+
+cst_node!(KeyValueBody, KEY_VALUE_BODY);
+
+impl<'a> KeyValueBody<'a> {
+ pub fn pairs(&self) -> impl Iterator<Item = KeyValuePair<'a>> {
+ self.syntax().children().filter_map(KeyValuePair::cast)
+ }
+}
+
+pub trait HasKeyValueBody<'a>: CstNode<'a, Lang = Language> {
+ fn body(&self) -> Option<KeyValueBody<'a>> {
+ self.syntax().children().find_map(KeyValueBody::cast)
+ }
+}
+
+cst_node!(CurlyGroupKeyValue, CURLY_GROUP_KEY_VALUE);
+
+impl<'a> HasCurly<'a> for CurlyGroupKeyValue<'a> {}
+
+impl<'a> HasKeyValueBody<'a> for CurlyGroupKeyValue<'a> {}
+
+cst_node!(BrackGroupKeyValue, BRACK_GROUP_KEY_VALUE);
+
+impl<'a> HasBrack<'a> for BrackGroupKeyValue<'a> {}
+
+impl<'a> HasKeyValueBody<'a> for BrackGroupKeyValue<'a> {}
+
+cst_node!(Formula, FORMULA);
+
+cst_node!(GenericCommand, GENERIC_COMMAND);
+
+impl<'a> GenericCommand<'a> {
+ pub fn name(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == GENERIC_COMMAND_NAME)
+ }
+}
+
+cst_node!(Equation, EQUATION);
+
+cst_node!(Begin, BEGIN);
+
+impl<'a> Begin<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax().first_token()
+ }
+
+ pub fn name(&self) -> Option<CurlyGroupWord<'a>> {
+ self.syntax().children().find_map(CurlyGroupWord::cast)
+ }
+
+ pub fn options(&self) -> Option<BrackGroup<'a>> {
+ self.syntax().children().find_map(BrackGroup::cast)
+ }
+}
+
+cst_node!(End, END);
+
+impl<'a> End<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax().first_token()
+ }
+
+ pub fn name(&self) -> Option<CurlyGroupWord<'a>> {
+ self.syntax().children().find_map(CurlyGroupWord::cast)
+ }
+}
+
+cst_node!(Environment, ENVIRONMENT);
+
+impl<'a> Environment<'a> {
+ pub fn begin(&self) -> Option<Begin<'a>> {
+ self.syntax().children().find_map(Begin::cast)
+ }
+
+ pub fn end(&self) -> Option<End<'a>> {
+ self.syntax().children().find_map(End::cast)
+ }
+}
+
+cst_node!(
+ Section,
+ PART,
+ CHAPTER,
+ SECTION,
+ SUBSECTION,
+ SUBSUBSECTION,
+ PARAGRAPH,
+ SUBPARAGRAPH
+);
+
+impl<'a> Section<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax().first_token()
+ }
+
+ pub fn name(&self) -> Option<CurlyGroup<'a>> {
+ self.syntax().children().find_map(CurlyGroup::cast)
+ }
+}
+
+cst_node!(EnumItem, ENUM_ITEM);
+
+impl<'a> EnumItem<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax().first_token()
+ }
+
+ pub fn label(&self) -> Option<BrackGroup<'a>> {
+ self.syntax().children().find_map(BrackGroup::cast)
+ }
+}
+
+cst_node!(Caption, CAPTION);
+
+impl<'a> Caption<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax().first_token()
+ }
+
+ pub fn short(&self) -> Option<BrackGroup<'a>> {
+ self.syntax().children().find_map(BrackGroup::cast)
+ }
+
+ pub fn long(&self) -> Option<CurlyGroup<'a>> {
+ self.syntax().children().find_map(CurlyGroup::cast)
+ }
+}
+
+cst_node!(Citation, CITATION);
+
+impl<'a> Citation<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax().first_token()
+ }
+
+ pub fn prenote(&self) -> Option<BrackGroup<'a>> {
+ self.syntax().children().find_map(BrackGroup::cast)
+ }
+
+ pub fn postnote(&self) -> Option<BrackGroup<'a>> {
+ self.syntax().children().filter_map(BrackGroup::cast).nth(1)
+ }
+
+ pub fn key_list(&self) -> Option<CurlyGroupWordList<'a>> {
+ self.syntax().children().find_map(CurlyGroupWordList::cast)
+ }
+}
+
+cst_node!(
+ Include,
+ PACKAGE_INCLUDE,
+ CLASS_INCLUDE,
+ LATEX_INCLUDE,
+ BIBLATEX_INCLUDE,
+ BIBTEX_INCLUDE,
+ GRAPHICS_INCLUDE,
+ SVG_INCLUDE,
+ INKSCAPE_INCLUDE,
+ VERBATIM_INCLUDE
+);
+
+impl<'a> Include<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax().first_token()
+ }
+
+ pub fn path_list(&self) -> Option<CurlyGroupWordList<'a>> {
+ self.syntax().children().find_map(CurlyGroupWordList::cast)
+ }
+}
+
+cst_node!(Import, IMPORT);
+
+impl<'a> Import<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax().first_token()
+ }
+
+ pub fn directory(&self) -> Option<CurlyGroupWord<'a>> {
+ self.syntax().children().find_map(CurlyGroupWord::cast)
+ }
+
+ pub fn file(&self) -> Option<CurlyGroupWord<'a>> {
+ self.syntax()
+ .children()
+ .filter_map(CurlyGroupWord::cast)
+ .nth(1)
+ }
+}
+
+cst_node!(LabelDefinition, LABEL_DEFINITION);
+
+impl<'a> LabelDefinition<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax().first_token()
+ }
+
+ pub fn name(&self) -> Option<CurlyGroupWord<'a>> {
+ self.syntax().children().find_map(CurlyGroupWord::cast)
+ }
+}
+
+cst_node!(LabelReference, LABEL_REFERENCE);
+
+impl<'a> LabelReference<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax().first_token()
+ }
+
+ pub fn name_list(&self) -> Option<CurlyGroupWordList<'a>> {
+ self.syntax().children().find_map(CurlyGroupWordList::cast)
+ }
+}
+
+cst_node!(LabelReferenceRange, LABEL_REFERENCE_RANGE);
+
+impl<'a> LabelReferenceRange<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax().first_token()
+ }
+
+ pub fn from(&self) -> Option<CurlyGroupWord<'a>> {
+ self.syntax().children().find_map(CurlyGroupWord::cast)
+ }
+
+ pub fn to(&self) -> Option<CurlyGroupWord<'a>> {
+ self.syntax()
+ .children()
+ .filter_map(CurlyGroupWord::cast)
+ .nth(1)
+ }
+}
+
+cst_node!(LabelNumber, LABEL_NUMBER);
+
+impl<'a> LabelNumber<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax().first_token()
+ }
+
+ pub fn name(&self) -> Option<CurlyGroupWord<'a>> {
+ self.syntax().children().find_map(CurlyGroupWord::cast)
+ }
+
+ pub fn text(&self) -> Option<CurlyGroup<'a>> {
+ self.syntax().children().find_map(CurlyGroup::cast)
+ }
+}
+
+cst_node!(TheoremDefinition, THEOREM_DEFINITION);
+
+impl<'a> TheoremDefinition<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax().first_token()
+ }
+
+ pub fn name(&self) -> Option<CurlyGroupWord<'a>> {
+ self.syntax().children().find_map(CurlyGroupWord::cast)
+ }
+
+ pub fn description(&self) -> Option<CurlyGroup<'a>> {
+ self.syntax().children().find_map(CurlyGroup::cast)
+ }
+}
+
+cst_node!(CommandDefinition, COMMAND_DEFINITION, MATH_OPERATOR);
+
+impl<'a> CommandDefinition<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax().first_token()
+ }
+
+ pub fn name(&self) -> Option<CurlyGroupCommand<'a>> {
+ self.syntax().children().find_map(CurlyGroupCommand::cast)
+ }
+
+ pub fn implementation(&self) -> Option<CurlyGroup<'a>> {
+ self.syntax().children().find_map(CurlyGroup::cast)
+ }
+}
+
+cst_node!(AcronymReference, ACRONYM_REFERENCE);
+
+impl<'a> AcronymReference<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax().first_token()
+ }
+}
+
+cst_node!(AcronymDefinition, ACRONYM_DEFINITION);
+
+impl<'a> AcronymDefinition<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax().first_token()
+ }
+
+ pub fn name(&self) -> Option<CurlyGroupWord<'a>> {
+ self.syntax().children().find_map(CurlyGroupWord::cast)
+ }
+}
+
+cst_node!(AcronymDeclaration, ACRONYM_DECLARATION);
+
+impl<'a> AcronymDeclaration<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax().first_token()
+ }
+
+ pub fn name(&self) -> Option<CurlyGroupWord<'a>> {
+ self.syntax().children().find_map(CurlyGroupWord::cast)
+ }
+}
+
+cst_node!(ColorDefinition, COLOR_DEFINITION);
+
+impl<'a> ColorDefinition<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax().first_token()
+ }
+
+ pub fn name(&self) -> Option<CurlyGroupWord<'a>> {
+ self.syntax().children().find_map(CurlyGroupWord::cast)
+ }
+
+ pub fn model(&self) -> Option<CurlyGroupWord<'a>> {
+ self.syntax()
+ .children()
+ .filter_map(CurlyGroupWord::cast)
+ .nth(1)
+ }
+
+ pub fn spec(&self) -> Option<CurlyGroup<'a>> {
+ self.syntax().children().find_map(CurlyGroup::cast)
+ }
+}
+
+cst_node!(ColorSetDefinition, COLOR_SET_DEFINITION);
+
+impl<'a> ColorSetDefinition<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax().first_token()
+ }
+
+ pub fn model_list(&self) -> Option<CurlyGroupWordList<'a>> {
+ self.syntax().children().find_map(CurlyGroupWordList::cast)
+ }
+}
+
+cst_node!(ColorReference, COLOR_REFERENCE);
+
+impl<'a> ColorReference<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax().first_token()
+ }
+
+ pub fn name(&self) -> Option<CurlyGroupWord<'a>> {
+ self.syntax().children().find_map(CurlyGroupWord::cast)
+ }
+}
+
+cst_node!(GlossaryEntryReference, GLOSSARY_ENTRY_REFERENCE);
+
+impl<'a> GlossaryEntryReference<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax().first_token()
+ }
+
+ pub fn name(&self) -> Option<CurlyGroupWord<'a>> {
+ self.syntax().children().find_map(CurlyGroupWord::cast)
+ }
+}
+
+cst_node!(GlossaryEntryDefinition, GLOSSARY_ENTRY_DEFINITION);
+
+impl<'a> GlossaryEntryDefinition<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax().first_token()
+ }
+
+ pub fn name(&self) -> Option<CurlyGroupWord<'a>> {
+ self.syntax().children().find_map(CurlyGroupWord::cast)
+ }
+}
+
+cst_node!(TikzLibraryImport, TIKZ_LIBRARY_IMPORT);
+
+impl<'a> TikzLibraryImport<'a> {
+ pub fn command(&self) -> Option<&'a SyntaxToken> {
+ self.syntax().first_token()
+ }
+
+ pub fn name_list(&self) -> Option<CurlyGroupWordList<'a>> {
+ self.syntax().children().find_map(CurlyGroupWordList::cast)
+ }
+}
diff --git a/support/texlab/src/syntax/latex/kind.rs b/support/texlab/src/syntax/latex/kind.rs
new file mode 100644
index 0000000000..fcab393380
--- /dev/null
+++ b/support/texlab/src/syntax/latex/kind.rs
@@ -0,0 +1,180 @@
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
+#[allow(non_camel_case_types)]
+#[repr(u16)]
+pub enum SyntaxKind {
+ ERROR = 0,
+ MISSING,
+
+ WHITESPACE,
+ COMMENT,
+ L_CURLY,
+ R_CURLY,
+ L_BRACK,
+ R_BRACK,
+ L_PAREN,
+ R_PAREN,
+ PARAMETER,
+ COMMA,
+ EQUALITY_SIGN,
+ WORD,
+ DOLLAR,
+ GENERIC_COMMAND_NAME,
+ BEGIN_ENVIRONMENT_NAME,
+ END_ENVIRONMENT_NAME,
+ BEGIN_EQUATION_NAME,
+ END_EQUATION_NAME,
+ PART_NAME,
+ CHAPTER_NAME,
+ SECTION_NAME,
+ SUBSECTION_NAME,
+ SUBSUBSECTION_NAME,
+ PARAGRAPH_NAME,
+ SUBPARAGRAPH_NAME,
+ ENUM_ITEM_NAME,
+ CAPTION_NAME,
+ CITATION_NAME,
+ PACKAGE_INCLUDE_NAME,
+ CLASS_INCLUDE_NAME,
+ LATEX_INCLUDE_NAME,
+ BIBLATEX_INCLUDE_NAME,
+ BIBTEX_INCLUDE_NAME,
+ GRAPHICS_INCLUDE_NAME,
+ SVG_INCLUDE_NAME,
+ INKSCAPE_INCLUDE_NAME,
+ VERBATIM_INCLUDE_NAME,
+ IMPORT_NAME,
+ LABEL_DEFINITION_NAME,
+ LABEL_REFERENCE_NAME,
+ LABEL_REFERENCE_RANGE_NAME,
+ LABEL_NUMBER_NAME,
+ COMMAND_DEFINITION_NAME,
+ MATH_OPERATOR_NAME,
+ GLOSSARY_ENTRY_DEFINITION_NAME,
+ GLOSSARY_ENTRY_REFERENCE_NAME,
+ ACRONYM_DEFINITION_NAME,
+ ACRONYM_DECLARATION_NAME,
+ ACRONYM_REFERENCE_NAME,
+ THEOREM_DEFINITION_NAME,
+ COLOR_REFERENCE_NAME,
+ COLOR_DEFINITION_NAME,
+ COLOR_SET_DEFINITION_NAME,
+ TIKZ_LIBRARY_IMPORT_NAME,
+ ENVIRONMENT_DEFINIITION_NAME,
+
+ PREAMBLE,
+ TEXT,
+ KEY,
+ VALUE,
+ KEY_VALUE_PAIR,
+ KEY_VALUE_BODY,
+ CURLY_GROUP,
+ CURLY_GROUP_WORD,
+ CURLY_GROUP_WORD_LIST,
+ CURLY_GROUP_COMMAND,
+ CURLY_GROUP_KEY_VALUE,
+ BRACK_GROUP,
+ BRACK_GROUP_WORD,
+ BRACK_GROUP_KEY_VALUE,
+ PAREN_GROUP,
+ MIXED_GROUP,
+ GENERIC_COMMAND,
+ ENVIRONMENT,
+ BEGIN,
+ END,
+ EQUATION,
+ PART,
+ CHAPTER,
+ SECTION,
+ SUBSECTION,
+ SUBSUBSECTION,
+ PARAGRAPH,
+ SUBPARAGRAPH,
+ ENUM_ITEM,
+ FORMULA,
+ CAPTION,
+ CITATION,
+ PACKAGE_INCLUDE,
+ CLASS_INCLUDE,
+ LATEX_INCLUDE,
+ BIBLATEX_INCLUDE,
+ BIBTEX_INCLUDE,
+ GRAPHICS_INCLUDE,
+ SVG_INCLUDE,
+ INKSCAPE_INCLUDE,
+ VERBATIM_INCLUDE,
+ IMPORT,
+ LABEL_DEFINITION,
+ LABEL_REFERENCE,
+ LABEL_REFERENCE_RANGE,
+ LABEL_NUMBER,
+ COMMAND_DEFINITION,
+ MATH_OPERATOR,
+ GLOSSARY_ENTRY_DEFINITION,
+ GLOSSARY_ENTRY_REFERENCE,
+ ACRONYM_DEFINITION,
+ ACRONYM_DECLARATION,
+ ACRONYM_REFERENCE,
+ THEOREM_DEFINITION,
+ COLOR_REFERENCE,
+ COLOR_DEFINITION,
+ COLOR_SET_DEFINITION,
+ TIKZ_LIBRARY_IMPORT,
+ ENVIRONMENT_DEFINITION,
+ ROOT,
+}
+
+impl SyntaxKind {
+ pub fn is_command_name(&self) -> bool {
+ use SyntaxKind::*;
+ matches!(
+ self,
+ GENERIC_COMMAND_NAME
+ | BEGIN_ENVIRONMENT_NAME
+ | END_ENVIRONMENT_NAME
+ | BEGIN_EQUATION_NAME
+ | END_EQUATION_NAME
+ | PART_NAME
+ | CHAPTER_NAME
+ | SECTION_NAME
+ | SUBSECTION_NAME
+ | SUBSUBSECTION_NAME
+ | PARAGRAPH_NAME
+ | SUBPARAGRAPH_NAME
+ | ENUM_ITEM_NAME
+ | CAPTION_NAME
+ | CITATION_NAME
+ | PACKAGE_INCLUDE_NAME
+ | CLASS_INCLUDE_NAME
+ | LATEX_INCLUDE_NAME
+ | BIBLATEX_INCLUDE_NAME
+ | BIBTEX_INCLUDE_NAME
+ | GRAPHICS_INCLUDE_NAME
+ | SVG_INCLUDE_NAME
+ | INKSCAPE_INCLUDE_NAME
+ | VERBATIM_INCLUDE_NAME
+ | IMPORT_NAME
+ | LABEL_DEFINITION_NAME
+ | LABEL_REFERENCE_NAME
+ | LABEL_REFERENCE_RANGE_NAME
+ | LABEL_NUMBER_NAME
+ | COMMAND_DEFINITION_NAME
+ | MATH_OPERATOR_NAME
+ | GLOSSARY_ENTRY_DEFINITION_NAME
+ | GLOSSARY_ENTRY_REFERENCE_NAME
+ | ACRONYM_DEFINITION_NAME
+ | ACRONYM_DECLARATION_NAME
+ | ACRONYM_REFERENCE_NAME
+ | THEOREM_DEFINITION_NAME
+ | COLOR_REFERENCE_NAME
+ | COLOR_DEFINITION_NAME
+ | COLOR_SET_DEFINITION_NAME
+ | TIKZ_LIBRARY_IMPORT_NAME
+ )
+ }
+}
+
+impl From<SyntaxKind> for cstree::SyntaxKind {
+ fn from(kind: SyntaxKind) -> Self {
+ Self(kind as u16)
+ }
+}
diff --git a/support/texlab/src/syntax/latex/lexer.rs b/support/texlab/src/syntax/latex/lexer.rs
index e14f1f6d85..7cd94ec897 100644
--- a/support/texlab/src/syntax/latex/lexer.rs
+++ b/support/texlab/src/syntax/latex/lexer.rs
@@ -1,176 +1,255 @@
-use super::ast::{Token, TokenKind};
-use crate::syntax::text::CharStream;
+use logos::Logos;
-#[derive(Debug)]
+use super::kind::SyntaxKind;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord, Logos)]
+#[allow(non_camel_case_types)]
+#[repr(u16)]
+enum Token {
+ #[regex(r"\s+")]
+ WHITESPACE = 2,
+
+ #[regex(r"%[^\r\n]*")]
+ COMMENT,
+
+ #[token("{")]
+ L_CURLY,
+
+ #[token("}")]
+ R_CURLY,
+
+ #[token("[")]
+ L_BRACK,
+
+ #[token("]")]
+ R_BRACK,
+
+ #[token("(")]
+ L_PAREN,
+
+ #[token(")")]
+ R_PAREN,
+
+ #[regex(r"#\d?")]
+ PARAMETER,
+
+ #[token(",")]
+ COMMA,
+
+ #[token("=")]
+ EQUALITY_SIGN,
+
+ #[regex(r"[^\s\\%\{\},\$\[\]\(\)=\#]+")]
+ #[error]
+ WORD,
+
+ #[regex(r"\$\$?")]
+ DOLLAR,
+
+ #[regex(r"\\([^\r\n]|[@a-zA-Z:_]+\*?)?")]
+ GENERIC_COMMAND_NAME,
+
+ #[regex(r"\\begin")]
+ BEGIN_ENVIRONMENT_NAME,
+
+ #[regex(r"\\end")]
+ END_ENVIRONMENT_NAME,
+
+ #[regex(r"\\\[")]
+ BEGIN_EQUATION_NAME,
+
+ #[regex(r"\\\]")]
+ END_EQUATION_NAME,
+
+ #[regex(r"\\part\*?")]
+ PART_NAME,
+
+ #[regex(r"\\chapter\*?")]
+ CHAPTER_NAME,
+
+ #[regex(r"\\section\*?")]
+ SECTION_NAME,
+
+ #[regex(r"\\subsection\*?")]
+ SUBSECTION_NAME,
+
+ #[regex(r"\\subsubsection\*?")]
+ SUBSUBSECTION_NAME,
+
+ #[regex(r"\\paragraph\*?")]
+ PARAGRAPH_NAME,
+
+ #[regex(r"\\subparagraph\*?")]
+ SUBPARAGRAPH_NAME,
+
+ #[regex(r"\\item")]
+ ENUM_ITEM_NAME,
+
+ #[regex(r"\\caption")]
+ CAPTION_NAME,
+
+ #[regex(r"\\cite|\\cite\*|\\Cite|\\nocite|\\citet|\\citep|\\citet\*|\\citep\*|\\citeauthor|\\citeauthor\*|\\Citeauthor|\\Citeauthor\*|\\citetitle|\\citetitle\*|\\citeyear|\\citeyear\*|\\citedate|\\citedate\*|\\citeurl|\\fullcite|\\citeyearpar|\\citealt|\\citealp|\\citetext|\\parencite|\\parencite\*|\\Parencite|\\footcite|\\footfullcite|\\footcitetext|\\textcite|\\Textcite|\\smartcite|\\Smartcite|\\supercite|\\autocite|\\Autocite|\\autocite\*|\\Autocite\*|\\volcite|\\Volcite|\\pvolcite|\\Pvolcite|\\fvolcite|\\ftvolcite|\\svolcite|\\Svolcite|\\tvolcite|\\Tvolcite|\\avolcite|\\Avolcite|\\notecite|\\notecite|\\pnotecite|\\Pnotecite|\\fnotecite|\\citeA|\\citeA\*")]
+ CITATION_NAME,
+
+ #[regex(r"\\usepackage|\\RequirePackage")]
+ PACKAGE_INCLUDE_NAME,
+
+ #[regex(r"\\documentclass")]
+ CLASS_INCLUDE_NAME,
+
+ #[regex(r"\\include|\\subfileinclude|\\input|\\subfile")]
+ LATEX_INCLUDE_NAME,
+
+ #[regex(r"\\addbibresource")]
+ BIBLATEX_INCLUDE_NAME,
+
+ #[regex(r"\\bibliography")]
+ BIBTEX_INCLUDE_NAME,
+
+ #[regex(r"\\includegraphics")]
+ GRAPHICS_INCLUDE_NAME,
+
+ #[regex(r"\\includesvg")]
+ SVG_INCLUDE_NAME,
+
+ #[regex(r"\\includeinkscape")]
+ INKSCAPE_INCLUDE_NAME,
+
+ #[regex(r"\\verbatiminput|\\VerbatimInput")]
+ VERBATIM_INCLUDE_NAME,
+
+ #[regex(r"\\import|\\subimport|\\inputfrom|\\subimportfrom|\\includefrom|\\subincludefrom")]
+ IMPORT_NAME,
+
+ #[regex(r"\\label")]
+ LABEL_DEFINITION_NAME,
+
+ #[regex(r"\\ref|\\vref|\\Vref|\\autoref|\\pageref|\\cref|\\Cref|\\cref*|\\Cref*|\\namecref|\\nameCref|\\lcnamecref|\\namecrefs|\\nameCrefs|\\lcnamecrefs|\\labelcref|\\labelcpageref|\\eqref")]
+ LABEL_REFERENCE_NAME,
+
+ #[regex(r"\\crefrange\*?|\\Crefrange\*?")]
+ LABEL_REFERENCE_RANGE_NAME,
+
+ #[regex(r"\\newlabel")]
+ LABEL_NUMBER_NAME,
+
+ #[regex(r"\\newcommand\*?|\\renewcommand|\\DeclareRobustCommand")]
+ COMMAND_DEFINITION_NAME,
+
+ #[regex(r"\\DeclareMathOperator\*?")]
+ MATH_OPERATOR_NAME,
+
+ #[regex(r"\\newglossaryentry")]
+ GLOSSARY_ENTRY_DEFINITION_NAME,
+
+ #[regex(r"\\gls|\\Gls|\\GLS|\\glspl|\\Glspl|\\GLSpl|\\glsdisp|\\glslink|\\glstext|\\Glstext|\\GLStext|\\glsfirst|\\Glsfirst|\\GLSfirst|\\glsplural|\\Glsplural|\\GLSplural|\\glsfirstplural|\\Glsfirstplural|\\GLSfirstplural|\\glsname|\\Glsname|\\GLSname|\\glssymbol|\\Glssymbol|\\glsdesc|\\Glsdesc|\\GLSdesc|\\glsuseri|\\Glsuseri|\\GLSuseri|\\glsuserii|\\Glsuserii|\\GLSuserii|\\glsuseriii|\\Glsuseriii|\\GLSuseriii|\\glsuseriv|\\Glsuseriv|\\GLSuseriv|\\glsuserv|\\Glsuserv|\\GLSuserv|\\glsuservi|\\Glsuservi|\\GLSuservi")]
+ GLOSSARY_ENTRY_REFERENCE_NAME,
+
+ #[regex(r"\\newacronym")]
+ ACRONYM_DEFINITION_NAME,
+
+ #[regex(r"\\DeclareAcronym")]
+ ACRONYM_DECLARATION_NAME,
+
+ #[regex(r"\\acrshort|\\Acrshort|\\ACRshort|\\acrshortpl|\\Acrshortpl|\\ACRshortpl|\\acrlong|\\Acrlong|\\ACRlong|\\acrlongpl|\\Acrlongpl|\\ACRlongpl|\\acrfull|\\Acrfull|\\ACRfull|\\acrfullpl|\\Acrfullpl|\\ACRfullpl|\\acs|\\Acs|\\acsp|\\Acsp|\\acl|\\Acl|\\aclp|\\Aclp|\\acf|\\Acf|\\acfp|\\Acfp|\\ac|\\Ac|\\acp|\\glsentrylong|\\Glsentrylong|\\glsentrylongpl|\\Glsentrylongpl|\\glsentryshort|\\Glsentryshort|\\glsentryshortpl|\\Glsentryshortpl|\\glsentryfullpl|\\Glsentryfullpl")]
+ ACRONYM_REFERENCE_NAME,
+
+ #[regex(r"\\newtheorem|\\declaretheorem")]
+ THEOREM_DEFINITION_NAME,
+
+ #[regex(r"\\color|\\colorbox|\\textcolor|\\pagecolor")]
+ COLOR_REFERENCE_NAME,
+
+ #[regex(r"\\definecolor")]
+ COLOR_DEFINITION_NAME,
+
+ #[regex(r"\\definecolorset")]
+ COLOR_SET_DEFINITION_NAME,
+
+ #[regex(r"\\usepgflibrary|\\usetikzlibrary")]
+ TIKZ_LIBRARY_IMPORT_NAME,
+
+ #[regex(r"\\newenvironment|\\newenvironment*")]
+ ENVIRONMENT_DEFINITION_NAME,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Lexer<'a> {
- stream: CharStream<'a>,
+ tokens: Vec<(SyntaxKind, &'a str)>,
}
impl<'a> Lexer<'a> {
pub fn new(text: &'a str) -> Self {
- Self {
- stream: CharStream::new(text),
+ let mut tokens = Vec::new();
+ let mut lexer = Token::lexer(text);
+ while let Some(kind) = lexer.next() {
+ tokens.push((
+ unsafe { std::mem::transmute::<Token, SyntaxKind>(kind) },
+ lexer.slice(),
+ ));
}
+ tokens.reverse();
+ Self { tokens }
}
- fn single_char(&mut self, kind: TokenKind) -> Token {
- self.stream.start_span();
- self.stream.next();
- let span = self.stream.end_span();
- Token::new(span, kind)
+ pub fn peek(&self) -> Option<SyntaxKind> {
+ self.tokens.last().map(|(kind, _)| *kind)
}
- fn math(&mut self) -> Token {
- self.stream.start_span();
- self.stream.next();
- if self.stream.satifies(|c| *c == '$') {
- self.stream.next();
- }
- let span = self.stream.end_span();
- Token::new(span, TokenKind::Math)
+ pub fn eat(&mut self) -> Option<(SyntaxKind, &'a str)> {
+ self.tokens.pop()
}
+}
- fn command(&mut self) -> Token {
- let span = self.stream.command();
- Token::new(span, TokenKind::Command)
- }
+#[cfg(test)]
+mod tests {
+ use insta::assert_debug_snapshot;
- fn word(&mut self) -> Token {
- self.stream.start_span();
- self.stream.next();
- while self.stream.satifies(|c| is_word_char(*c)) {
- self.stream.next();
- }
+ use super::*;
- let span = self.stream.end_span();
- Token::new(span, TokenKind::Word)
+ fn verify(text: &str) -> Vec<(SyntaxKind, &str)> {
+ let mut tokens = Lexer::new(text).tokens;
+ tokens.reverse();
+ tokens
}
-}
-impl<'a> Iterator for Lexer<'a> {
- type Item = Token;
-
- fn next(&mut self) -> Option<Token> {
- loop {
- match self.stream.peek() {
- Some('%') => {
- self.stream.skip_rest_of_line();
- }
- Some('{') => {
- return Some(self.single_char(TokenKind::BeginGroup));
- }
- Some('}') => {
- return Some(self.single_char(TokenKind::EndGroup));
- }
- Some('[') => {
- return Some(self.single_char(TokenKind::BeginOptions));
- }
- Some(']') => {
- return Some(self.single_char(TokenKind::EndOptions));
- }
- Some('$') => {
- return Some(self.math());
- }
- Some(',') => {
- return Some(self.single_char(TokenKind::Comma));
- }
- Some('\\') => {
- return Some(self.command());
- }
- Some(c) => {
- if c.is_whitespace() {
- self.stream.next();
- } else {
- return Some(self.word());
- }
- }
- None => {
- return None;
- }
- }
- }
+ #[test]
+ fn test_empty() {
+ assert_debug_snapshot!(verify(r#""#));
}
-}
-
-fn is_word_char(c: char) -> bool {
- !c.is_whitespace()
- && c != '%'
- && c != '{'
- && c != '}'
- && c != '['
- && c != ']'
- && c != '\\'
- && c != '$'
- && c != ','
-}
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- protocol::{Position, Range},
- syntax::text::Span,
- };
-
- fn verify<'a>(lexer: &mut Lexer<'a>, line: u64, character: u64, text: &str, kind: TokenKind) {
- let start = Position::new(line, character);
- let end = Position::new(line, character + text.chars().count() as u64);
- let range = Range::new(start, end);
- let span = Span::new(range, text.to_owned());
- let token = Token::new(span, kind);
- assert_eq!(Some(token), lexer.next());
+ #[test]
+ fn test_delimiters() {
+ assert_debug_snapshot!(verify(r#"{foo} (bar) [baz, qux = foo-bar]"#));
}
#[test]
- fn word() {
- let mut lexer = Lexer::new("foo bar baz");
- verify(&mut lexer, 0, 0, "foo", TokenKind::Word);
- verify(&mut lexer, 0, 4, "bar", TokenKind::Word);
- verify(&mut lexer, 0, 8, "baz", TokenKind::Word);
- assert_eq!(None, lexer.next());
+ fn test_command_with_parameter() {
+ assert_debug_snapshot!(verify(r#"\newcommand{\id}[1]{#1}"#));
}
#[test]
- fn command() {
- let mut lexer = Lexer::new("\\foo\\bar@baz\n\\foo*");
- verify(&mut lexer, 0, 0, "\\foo", TokenKind::Command);
- verify(&mut lexer, 0, 4, "\\bar@baz", TokenKind::Command);
- verify(&mut lexer, 1, 0, "\\foo*", TokenKind::Command);
- assert_eq!(None, lexer.next());
+ fn test_command_with_star() {
+ assert_debug_snapshot!(verify(r#"\section*{Foo}"#));
}
#[test]
- fn escape_sequence() {
- let mut lexer = Lexer::new("\\%\\**");
- verify(&mut lexer, 0, 0, "\\%", TokenKind::Command);
- verify(&mut lexer, 0, 2, "\\*", TokenKind::Command);
- verify(&mut lexer, 0, 4, "*", TokenKind::Word);
- assert_eq!(None, lexer.next());
+ fn test_escape_sequence() {
+ assert_debug_snapshot!(verify(r#"\% hello"#));
}
#[test]
- fn group_delimiter() {
- let mut lexer = Lexer::new("{}[]");
- verify(&mut lexer, 0, 0, "{", TokenKind::BeginGroup);
- verify(&mut lexer, 0, 1, "}", TokenKind::EndGroup);
- verify(&mut lexer, 0, 2, "[", TokenKind::BeginOptions);
- verify(&mut lexer, 0, 3, "]", TokenKind::EndOptions);
- assert_eq!(None, lexer.next());
+ fn test_formula() {
+ assert_debug_snapshot!(verify(r#"$ f(x) = y $$"#));
}
#[test]
- fn math() {
- let mut lexer = Lexer::new("$$ $ $");
- verify(&mut lexer, 0, 0, "$$", TokenKind::Math);
- verify(&mut lexer, 0, 3, "$", TokenKind::Math);
- verify(&mut lexer, 0, 5, "$", TokenKind::Math);
- assert_eq!(None, lexer.next());
+ fn test_comment() {
+ assert_debug_snapshot!(verify("hello %world\r\ntest %test"));
}
#[test]
- fn line_comment() {
- let mut lexer = Lexer::new(" %foo \nfoo");
- verify(&mut lexer, 1, 0, "foo", TokenKind::Word);
- assert_eq!(None, lexer.next());
+ fn test_invalid_parameter() {
+ assert_debug_snapshot!(verify(r#"#"#))
}
}
diff --git a/support/texlab/src/syntax/latex/mod.rs b/support/texlab/src/syntax/latex/mod.rs
deleted file mode 100644
index cee83ab04e..0000000000
--- a/support/texlab/src/syntax/latex/mod.rs
+++ /dev/null
@@ -1,686 +0,0 @@
-mod analysis;
-mod ast;
-mod lexer;
-mod parser;
-
-pub use self::{analysis::*, ast::*};
-
-use self::{lexer::Lexer, parser::Parser};
-use crate::{
- protocol::{Options, Uri},
- tex::Resolver,
-};
-use std::path::Path;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub struct OpenParams<'a> {
- pub text: &'a str,
- pub uri: &'a Uri,
- pub resolver: &'a Resolver,
- pub options: &'a Options,
- pub current_dir: &'a Path,
-}
-
-pub fn open(params: OpenParams) -> SymbolTable {
- let OpenParams {
- text,
- uri,
- resolver,
- options,
- current_dir,
- } = params;
-
- let lexer = Lexer::new(text);
- let parser = Parser::new(lexer);
- let tree = parser.parse();
-
- let params = SymbolTableParams {
- tree,
- uri,
- resolver,
- options,
- current_dir,
- };
- SymbolTable::analyze(params)
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- protocol::{Options, Range, RangeExt, Uri},
- syntax::{generic_ast::AstNodeIndex, text::SyntaxNode},
- tex::Resolver,
- };
- use indoc::indoc;
- use std::env;
-
- fn open_simple(text: &str) -> SymbolTable {
- open(OpenParams {
- text: text.trim(),
- uri: &Uri::parse("http://www.foo.com/bar.tex").unwrap(),
- resolver: &Resolver::default(),
- options: &Options::default(),
- current_dir: &env::current_dir().unwrap(),
- })
- }
-
- #[derive(Debug, Default)]
- struct TreeTraversal {
- nodes: Vec<AstNodeIndex>,
- }
-
- impl Visitor for TreeTraversal {
- fn visit(&mut self, tree: &Tree, node: AstNodeIndex) {
- self.nodes.push(node);
- tree.walk(self, node);
- }
- }
-
- mod range {
- use super::*;
-
- fn verify(expected_ranges: Vec<Range>, text: &str) {
- let table = open_simple(text);
-
- let mut traversal = TreeTraversal::default();
- traversal.visit(&table.tree, table.tree.root);
- let actual_ranges: Vec<_> = traversal
- .nodes
- .into_iter()
- .map(|node| table[node].range())
- .collect();
- assert_eq!(actual_ranges, expected_ranges);
- }
-
- #[test]
- fn command() {
- verify(
- vec![
- Range::new_simple(0, 0, 2, 14),
- Range::new_simple(0, 0, 0, 23),
- Range::new_simple(0, 14, 0, 23),
- Range::new_simple(0, 15, 0, 22),
- Range::new_simple(1, 0, 1, 20),
- Range::new_simple(1, 11, 1, 20),
- Range::new_simple(1, 12, 1, 19),
- Range::new_simple(2, 0, 2, 14),
- Range::new_simple(2, 4, 2, 9),
- Range::new_simple(2, 5, 2, 8),
- Range::new_simple(2, 9, 2, 14),
- Range::new_simple(2, 10, 2, 13),
- ],
- indoc!(
- r#"
- \documentclass{article}
- \usepackage{amsmath}
- \foo[bar]{baz}
- "#
- ),
- );
- }
-
- #[test]
- fn text() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 11),
- Range::new_simple(0, 0, 0, 11),
- ],
- indoc!(
- r#"
- foo bar baz
- "#
- ),
- );
- }
-
- #[test]
- fn text_bracket() {
- verify(
- vec![Range::new_simple(0, 0, 0, 5), Range::new_simple(0, 0, 0, 5)],
- indoc!(
- r#"
- ]foo[
- "#
- ),
- );
- }
-
- #[test]
- fn group() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 15),
- Range::new_simple(0, 0, 0, 15),
- Range::new_simple(0, 2, 0, 5),
- Range::new_simple(0, 6, 0, 13),
- Range::new_simple(0, 8, 0, 11),
- ],
- indoc!(
- r#"
- { foo { bar } }
- "#
- ),
- );
- }
-
- #[test]
- fn group_incomplete() {
- verify(
- vec![Range::new_simple(0, 1, 0, 2), Range::new_simple(0, 1, 0, 2)],
- indoc!(
- r#"
- }{
- "#
- ),
- );
- }
-
- #[test]
- fn math() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 9),
- Range::new_simple(0, 0, 0, 1),
- Range::new_simple(0, 2, 0, 7),
- Range::new_simple(0, 8, 0, 9),
- ],
- indoc!(
- r#"
- $ x = 1 $
- "#
- ),
- );
- }
-
- #[test]
- fn comma() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 8),
- Range::new_simple(0, 0, 0, 3),
- Range::new_simple(0, 3, 0, 4),
- Range::new_simple(0, 5, 0, 8),
- ],
- indoc!(
- r#"
- foo, bar
- "#
- ),
- );
- }
- }
-
- mod command {
- use super::*;
-
- fn verify(expected_names: Vec<&str>, text: &str) {
- let table = open(OpenParams {
- text,
- uri: &Uri::parse("http://www.foo.com/bar.tex").unwrap(),
- resolver: &Resolver::default(),
- options: &Options::default(),
- current_dir: &env::current_dir().unwrap(),
- });
-
- let actual_names: Vec<_> = table
- .commands
- .iter()
- .map(|node| table.tree.as_command(*node).unwrap().name.text())
- .collect();
-
- assert_eq!(actual_names, expected_names);
- }
-
- #[test]
- fn basic() {
- verify(
- vec!["\\documentclass", "\\usepackage", "\\begin", "\\end"],
- indoc!(
- r#"
- \documentclass{article}
- \usepackage{amsmath}
- \begin{document}
- Hello World
- \end{document}
- "#
- ),
- );
- }
-
- #[test]
- fn star() {
- verify(
- vec!["\\section*", "\\subsection*"],
- indoc!(
- r#"
- \section*{Foo}
- \subsection**{Bar}
- "#
- ),
- );
- }
-
- #[test]
- fn at() {
- verify(vec!["\\foo@bar"], indoc!(r#"\foo@bar"#));
- }
-
- #[test]
- fn escape() {
- verify(vec!["\\%"], indoc!(r#"\%foo"#))
- }
- }
-
- mod environment {
- use super::*;
-
- fn verify(expected_names: Vec<(&str, &str)>, text: &str) {
- let table = open_simple(text);
- let actual_names: Vec<_> = table
- .environments
- .iter()
- .map(|env| {
- (
- env.left
- .name(&table.tree)
- .map(Token::text)
- .unwrap_or_default(),
- env.right
- .name(&table.tree)
- .map(Token::text)
- .unwrap_or_default(),
- )
- })
- .collect();
-
- assert_eq!(actual_names, expected_names);
- }
-
- #[test]
- fn nested() {
- verify(
- vec![("b", "b"), ("a", "a")],
- indoc!(
- r#"
- \begin{a}
- \begin{b}
- \end{b}
- \end{a}
- "#
- ),
- );
- }
-
- #[test]
- fn empty_name() {
- verify(
- vec![("a", ""), ("", "b")],
- indoc!(
- r#"
- \begin{a}
- \end{}
- \begin{}
- \end{b}
- "#
- ),
- );
- }
-
- #[test]
- fn incomplete() {
- verify(
- Vec::new(),
- indoc!(
- r#"
- \end{a}
- \begin{a}
- "#
- ),
- );
- }
-
- #[test]
- fn standalone_true() {
- let table = open_simple(r#"\begin{document}\end{document}"#);
- assert!(table.is_standalone);
- }
-
- #[test]
- fn standalone_false() {
- let table = open_simple(r#"\begin{doc}\end{doc}"#);
- assert!(!table.is_standalone);
- }
- }
-
- mod include {
- use super::*;
-
- fn verify(expected_targets: Vec<Vec<&str>>, resolver: Resolver, text: &str) {
- let table = open(OpenParams {
- text,
- uri: &Uri::parse("http://www.foo.com/dir1/dir2/foo.tex").unwrap(),
- resolver: &resolver,
- options: &Options::default(),
- current_dir: &env::current_dir().unwrap(),
- });
-
- assert_eq!(table.includes.len(), 1);
- let include = &table.includes[0];
- let actual_targets: Vec<Vec<&str>> = include
- .all_targets
- .iter()
- .map(|targets| targets.iter().map(|target| target.as_str()).collect())
- .collect();
-
- assert_eq!(actual_targets, expected_targets);
- }
-
- #[test]
- fn same_directory() {
- verify(
- vec![vec![
- "http://www.foo.com/dir1/dir2/bar",
- "http://www.foo.com/dir1/dir2/bar.tex",
- ]],
- Resolver::default(),
- indoc!(r#"\include{bar}"#),
- );
- }
-
- #[test]
- fn two_paths() {
- verify(
- vec![
- vec![
- "http://www.foo.com/dir1/dir2/bar.tex",
- "http://www.foo.com/dir1/dir2/bar.tex.tex",
- ],
- vec![
- "http://www.foo.com/dir1/dir2/baz.tex",
- "http://www.foo.com/dir1/dir2/baz.tex.tex",
- ],
- ],
- Resolver::default(),
- indoc!(r#"\input{bar.tex, ./baz.tex}"#),
- );
- }
-
- #[test]
- fn sub_directory() {
- verify(
- vec![vec![
- "http://www.foo.com/dir1/dir2/dir3/bar",
- "http://www.foo.com/dir1/dir2/dir3/bar.tex",
- ]],
- Resolver::default(),
- indoc!(r#"\include{dir3/bar}"#),
- );
- }
-
- #[test]
- fn parent_directory() {
- verify(
- vec![vec![
- "http://www.foo.com/dir1/bar",
- "http://www.foo.com/dir1/bar.tex",
- ]],
- Resolver::default(),
- indoc!(r#"\include{../bar}"#),
- );
- }
-
- #[test]
- fn distro_file() {
- let mut resolver = Resolver::default();
- let path = env::current_dir().unwrap().join("biblatex-examples.bib");
- resolver
- .files_by_name
- .insert("biblatex-examples.bib".into(), path.clone());
- verify(
- vec![vec![
- "http://www.foo.com/dir1/dir2/biblatex-examples.bib",
- "http://www.foo.com/dir1/dir2/biblatex-examples.bib.bib",
- Uri::from_file_path(&path).unwrap().as_str(),
- ]],
- resolver,
- indoc!(r#"\addbibresource{biblatex-examples.bib}"#),
- );
- }
-
- #[test]
- fn component() {
- let table = open(OpenParams {
- text: indoc!(
- r#"
- \documentclass{article}
- \usepackage{amsmath}
- \usepackage{geometry, lipsum}
- "#
- ),
- uri: &Uri::parse("http://www.foo.com/bar.tex").unwrap(),
- resolver: &Resolver::default(),
- options: &Options::default(),
- current_dir: &env::current_dir().unwrap(),
- });
- assert_eq!(
- table.components,
- vec!["article.cls", "amsmath.sty", "geometry.sty", "lipsum.sty"]
- );
- }
- }
-
- #[test]
- fn citation() {
- let table = open_simple(indoc!(
- r#"
- \cite{key1}
- \cite{key2, key3}
- \nocite{*}
- "#
- ));
-
- let expected_keys = vec![vec!["key1"], vec!["key2", "key3"], vec!["*"]];
-
- let actual_keys: Vec<Vec<&str>> = table
- .citations
- .iter()
- .map(|cit| cit.keys(&table.tree).into_iter().map(Token::text).collect())
- .collect();
-
- assert_eq!(actual_keys, expected_keys);
- }
-
- #[test]
- fn command_definition() {
- let table = open_simple(indoc!(
- r#"
- \newcommand{\foo}{Foo}
- \newcommand[2]{\bar}{Bar}
- \renewcommand{\baz}{Baz}
- \qux
- "#
- ));
-
- let expected_cmds = vec!["\\foo", "\\bar", "\\baz"];
-
- let actual_cmds: Vec<&str> = table
- .command_definitions
- .iter()
- .map(|def| def.definition_name(&table.tree))
- .collect();
-
- assert_eq!(actual_cmds, expected_cmds);
- }
-
- #[test]
- fn glossary_entry() {
- let table = open_simple(indoc!(
- r#"
- \newglossaryentry{foo}{...}
- \newacronym{bar}{...}
- "#
- ));
-
- let expected_entries = vec!["foo", "bar"];
-
- let actual_entries: Vec<&str> = table
- .glossary_entries
- .iter()
- .map(|entry| entry.label(&table.tree).text())
- .collect();
-
- assert_eq!(actual_entries, expected_entries);
- }
-
- #[test]
- fn equation() {
- let table = open_simple(indoc!(
- r#"
- \[
- e^{i \pi} + 1 = 0
- \]
- \] \[
- "#
- ));
-
- assert_eq!(table.equations.len(), 1);
- }
-
- #[test]
- fn inline() {
- let table = open_simple(indoc!(
- r#"
- $ x $
- $
- "#
- ));
-
- assert_eq!(table.inlines.len(), 1);
- }
-
- #[test]
- fn math_operator() {
- let table = open_simple(indoc!(
- r#"
- \DeclareMathOperator{\foo}{foo}
- "#
- ));
-
- assert_eq!(table.math_operators.len(), 1);
- assert_eq!(
- table.math_operators[0].definition_name(&table.tree),
- "\\foo"
- );
- }
-
- #[test]
- fn theorem_definition() {
- let table = open_simple(indoc!(
- r#"
- \newtheorem{lemma}{Lemma}
- "#
- ));
-
- assert_eq!(table.theorem_definitions.len(), 1);
- assert_eq!(
- table.theorem_definitions[0].name(&table.tree).text(),
- "lemma"
- );
- }
-
- #[test]
- fn section() {
- let table = open_simple(indoc!(
- r#"
- \section{Introduction to \LaTeX}
- \subsection*{Foo
- "#
- ));
- assert_eq!(table.sections.len(), 2);
- assert_eq!(
- table.sections[0].print(&table.tree).unwrap(),
- "Introduction to \\LaTeX"
- );
- assert_eq!(table.sections[1].print(&table.tree), None);
- }
-
- #[test]
- fn label() {
- let table = open_simple(indoc!(
- r#"
- \label{foo}
- \ref{bar, baz}
- "#
- ));
-
- let expected_names = vec![vec!["foo"], vec!["bar", "baz"]];
-
- let actual_names: Vec<Vec<&str>> = table
- .labels
- .iter()
- .map(|label| {
- label
- .names(&table.tree)
- .into_iter()
- .map(Token::text)
- .collect()
- })
- .collect();
-
- assert_eq!(actual_names, expected_names);
- }
-
- #[test]
- fn label_numbering() {
- let table = open_simple(indoc!(
- r#"
- \newlabel{foo}{{1}{1}}
- "#
- ));
-
- assert_eq!(table.label_numberings.len(), 1);
- assert_eq!(table.label_numberings[0].name(&table.tree).text(), "foo");
- assert_eq!(table.label_numberings[0].number, "1");
- }
-
- #[test]
- fn caption() {
- let table = open_simple(indoc!(
- r#"
- \caption{Foo \LaTeX Bar}
- "#
- ));
-
- assert_eq!(table.captions.len(), 1);
- assert_eq!(
- table.captions[0].print(&table.tree).unwrap(),
- "Foo \\LaTeX Bar"
- );
- }
-
- #[test]
- fn item_without_name() {
- let table = open_simple(indoc!(
- r#"
- \item
- "#
- ));
-
- assert_eq!(table.items.len(), 1);
- assert_eq!(table.items[0].name(&table.tree), None);
- }
-
- #[test]
- fn item_with_name() {
- let table = open_simple(indoc!(
- r#"
- \item[foo bar]
- "#
- ));
-
- assert_eq!(table.items.len(), 1);
- assert_eq!(table.items[0].name(&table.tree).unwrap(), "foo bar");
- }
-}
diff --git a/support/texlab/src/syntax/latex/parser.rs b/support/texlab/src/syntax/latex/parser.rs
index 4ace4fbcd6..4333235ba5 100644
--- a/support/texlab/src/syntax/latex/parser.rs
+++ b/support/texlab/src/syntax/latex/parser.rs
@@ -1,186 +1,1709 @@
-use super::ast::*;
-use crate::{
- protocol::{Range, RangeExt},
- syntax::{
- generic_ast::{Ast, AstNodeIndex},
- text::SyntaxNode,
- },
+use cstree::GreenNodeBuilder;
+
+use super::{
+ lexer::Lexer,
+ SyntaxKind::{self, *},
+ SyntaxNode,
};
-use std::iter::Peekable;
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-enum Scope {
- Root,
- Group,
- Options,
+#[derive(Clone)]
+pub struct Parse {
+ pub root: SyntaxNode,
+}
+
+#[derive(Debug, Clone, Copy)]
+struct ParserContext {
+ allow_environment: bool,
+ allow_comma: bool,
+}
+
+impl Default for ParserContext {
+ fn default() -> Self {
+ Self {
+ allow_environment: true,
+ allow_comma: true,
+ }
+ }
}
#[derive(Debug)]
-pub struct Parser<I: Iterator<Item = Token>> {
- tree: Ast<Node>,
- tokens: Peekable<I>,
+struct Parser<'a> {
+ lexer: Lexer<'a>,
+ builder: GreenNodeBuilder<'static, 'static>,
}
-impl<I: Iterator<Item = Token>> Parser<I> {
- pub fn new(tokens: I) -> Self {
+impl<'a> Parser<'a> {
+ pub fn new(text: &'a str) -> Self {
Self {
- tree: Ast::new(),
- tokens: tokens.peekable(),
+ lexer: Lexer::new(text),
+ builder: GreenNodeBuilder::new(),
}
}
- pub fn parse(mut self) -> Tree {
- let children = self.content(Scope::Root);
+ fn eat(&mut self) {
+ let (kind, text) = self.lexer.eat().unwrap();
+ self.builder.token(kind.into(), text);
+ }
- let range = if children.is_empty() {
- Range::new_simple(0, 0, 0, 0)
+ fn peek(&self) -> Option<SyntaxKind> {
+ self.lexer.peek()
+ }
+
+ fn expect(&mut self, kind: SyntaxKind) {
+ if self.peek() == Some(kind) {
+ self.eat();
+ self.trivia();
} else {
- let start = self.tree[children[0]].start();
- let end = self.tree[children[children.len() - 1]].end();
- Range::new(start, end)
- };
+ self.builder.token(MISSING.into(), "");
+ }
+ }
- let root = self.tree.add_node(Node::Root(Root { range }));
- self.connect(root, &children);
- Tree {
- inner: self.tree,
- root,
+ fn expect2(&mut self, kind1: SyntaxKind, kind2: SyntaxKind) {
+ if self
+ .peek()
+ .filter(|&kind| kind == kind1 || kind == kind2)
+ .is_some()
+ {
+ self.eat();
+ self.trivia();
+ } else {
+ self.builder.token(MISSING.into(), "");
}
}
- fn content(&mut self, scope: Scope) -> Vec<AstNodeIndex> {
- let mut children = Vec::new();
- while let Some(ref token) = self.tokens.peek() {
- match token.kind {
- TokenKind::Word | TokenKind::BeginOptions => {
- children.push(self.text(scope));
- }
- TokenKind::Command => {
- children.push(self.command());
- }
- TokenKind::Comma => {
- children.push(self.comma());
- }
- TokenKind::Math => {
- children.push(self.math());
- }
- TokenKind::BeginGroup => {
- children.push(self.group(GroupKind::Group));
- }
- TokenKind::EndGroup => {
- if scope == Scope::Root {
- self.tokens.next();
- } else {
- return children;
- }
- }
- TokenKind::EndOptions => {
- if scope == Scope::Options {
- return children;
+ fn trivia(&mut self) {
+ while self
+ .peek()
+ .filter(|&kind| matches!(kind, WHITESPACE | COMMENT))
+ .is_some()
+ {
+ self.eat();
+ }
+ }
+
+ pub fn parse(mut self) -> Parse {
+ self.builder.start_node(ROOT.into());
+ self.preamble();
+ while self.peek().is_some() {
+ self.content(ParserContext::default());
+ }
+ self.builder.finish_node();
+ let (green_node, interner) = self.builder.finish();
+ Parse {
+ root: SyntaxNode::new_root_with_resolver(green_node, interner.unwrap()),
+ }
+ }
+
+ fn content(&mut self, context: ParserContext) {
+ match self.peek().unwrap() {
+ WHITESPACE | COMMENT => self.eat(),
+ L_CURLY if context.allow_environment => self.curly_group(),
+ L_CURLY => self.curly_group_without_environments(),
+ L_BRACK | L_PAREN => self.mixed_group(),
+ R_CURLY | R_BRACK | R_PAREN => {
+ self.builder.start_node(ERROR.into());
+ self.eat();
+ self.builder.finish_node();
+ }
+ PARAMETER => self.eat(),
+ WORD | COMMA => self.text(context),
+ EQUALITY_SIGN => self.eat(),
+ DOLLAR => self.formula(),
+ GENERIC_COMMAND_NAME => self.generic_command(),
+ BEGIN_ENVIRONMENT_NAME if context.allow_environment => self.environment(),
+ BEGIN_ENVIRONMENT_NAME => self.generic_command(),
+ END_ENVIRONMENT_NAME => self.generic_command(),
+ BEGIN_EQUATION_NAME => self.equation(),
+ END_EQUATION_NAME => self.generic_command(),
+ MISSING | ERROR => self.eat(),
+ PART_NAME => self.part(),
+ CHAPTER_NAME => self.chapter(),
+ SECTION_NAME => self.section(),
+ SUBSECTION_NAME => self.subsection(),
+ SUBSUBSECTION_NAME => self.subsubsection(),
+ PARAGRAPH_NAME => self.paragraph(),
+ SUBPARAGRAPH_NAME => self.subparagraph(),
+ ENUM_ITEM_NAME => self.enum_item(),
+ CAPTION_NAME => self.caption(),
+ CITATION_NAME => self.citation(),
+ PACKAGE_INCLUDE_NAME => self.package_include(),
+ CLASS_INCLUDE_NAME => self.class_include(),
+ LATEX_INCLUDE_NAME => self.latex_include(),
+ BIBLATEX_INCLUDE_NAME => self.biblatex_include(),
+ BIBTEX_INCLUDE_NAME => self.bibtex_include(),
+ GRAPHICS_INCLUDE_NAME => self.graphics_include(),
+ SVG_INCLUDE_NAME => self.svg_include(),
+ INKSCAPE_INCLUDE_NAME => self.inkscape_include(),
+ VERBATIM_INCLUDE_NAME => self.verbatim_include(),
+ IMPORT_NAME => self.import(),
+ LABEL_DEFINITION_NAME => self.label_definition(),
+ LABEL_REFERENCE_NAME => self.label_reference(),
+ LABEL_REFERENCE_RANGE_NAME => self.label_reference_range(),
+ LABEL_NUMBER_NAME => self.label_number(),
+ COMMAND_DEFINITION_NAME => self.command_definition(),
+ MATH_OPERATOR_NAME => self.math_operator(),
+ GLOSSARY_ENTRY_DEFINITION_NAME => self.glossary_entry_definition(),
+ GLOSSARY_ENTRY_REFERENCE_NAME => self.glossary_entry_reference(),
+ ACRONYM_DEFINITION_NAME => self.acronym_definition(),
+ ACRONYM_DECLARATION_NAME => self.acronym_declaration(),
+ ACRONYM_REFERENCE_NAME => self.acronym_reference(),
+ THEOREM_DEFINITION_NAME => self.theorem_definition(),
+ COLOR_REFERENCE_NAME => self.color_reference(),
+ COLOR_DEFINITION_NAME => self.color_definition(),
+ COLOR_SET_DEFINITION_NAME => self.color_set_definition(),
+ TIKZ_LIBRARY_IMPORT_NAME => self.tikz_library_import(),
+ ENVIRONMENT_DEFINIITION_NAME => self.environment_definition(),
+ _ => unreachable!(),
+ }
+ }
+
+ fn text(&mut self, context: ParserContext) {
+ self.builder.start_node(TEXT.into());
+ self.eat();
+ while self
+ .peek()
+ .filter(|&kind| {
+ matches!(kind, WHITESPACE | COMMENT | WORD | COMMA)
+ && (context.allow_comma || kind != COMMA)
+ })
+ .is_some()
+ {
+ self.eat();
+ }
+ self.builder.finish_node();
+ }
+
+ fn group_with_token(
+ &mut self,
+ node_kind: SyntaxKind,
+ content_kind: SyntaxKind,
+ right_kind: SyntaxKind,
+ ) {
+ self.builder.start_node(node_kind.into());
+ self.eat();
+ self.trivia();
+ self.expect(content_kind);
+ self.expect(right_kind);
+ self.builder.finish_node();
+ }
+
+ fn curly_group(&mut self) {
+ self.builder.start_node(CURLY_GROUP.into());
+ self.eat();
+ while self
+ .peek()
+ .filter(|&kind| !matches!(kind, R_CURLY | END_ENVIRONMENT_NAME))
+ .is_some()
+ {
+ self.content(ParserContext::default());
+ }
+ self.expect(R_CURLY);
+ self.builder.finish_node();
+ }
+
+ fn curly_group_without_environments(&mut self) {
+ self.builder.start_node(CURLY_GROUP.into());
+ self.eat();
+ while self
+ .peek()
+ .filter(|&kind| !matches!(kind, R_CURLY))
+ .is_some()
+ {
+ self.content(ParserContext {
+ allow_environment: false,
+ allow_comma: true,
+ });
+ }
+ self.expect(R_CURLY);
+ self.builder.finish_node();
+ }
+
+ fn curly_group_word(&mut self) {
+ self.group_with_token(CURLY_GROUP_WORD, WORD, R_CURLY);
+ }
+
+ fn curly_group_word_list(&mut self) {
+ self.builder.start_node(CURLY_GROUP_WORD_LIST.into());
+ self.eat();
+
+ while self
+ .peek()
+ .filter(|&kind| matches!(kind, WHITESPACE | COMMENT | WORD | COMMA))
+ .is_some()
+ {
+ self.eat();
+ }
+
+ self.expect(R_CURLY);
+ self.builder.finish_node();
+ }
+
+ fn curly_group_command(&mut self) {
+ self.group_with_token(CURLY_GROUP_COMMAND, GENERIC_COMMAND_NAME, R_CURLY);
+ }
+
+ fn brack_group(&mut self) {
+ self.builder.start_node(BRACK_GROUP.into());
+ self.eat();
+ while self
+ .peek()
+ .filter(|&kind| {
+ !matches!(
+ kind,
+ R_CURLY
+ | R_BRACK
+ | PART_NAME
+ | CHAPTER_NAME
+ | SECTION_NAME
+ | SUBSECTION_NAME
+ | PARAGRAPH_NAME
+ | SUBPARAGRAPH_NAME
+ | ENUM_ITEM_NAME
+ | END_ENVIRONMENT_NAME
+ )
+ })
+ .is_some()
+ {
+ self.content(ParserContext::default());
+ }
+ self.expect(R_BRACK);
+ self.builder.finish_node();
+ }
+
+ fn brack_group_word(&mut self) {
+ self.group_with_token(BRACK_GROUP_WORD, WORD, R_BRACK);
+ }
+
+ // fn paren_group(&mut self) {
+ // self.builder.start_node(PAREN_GROUP.into());
+ // self.eat();
+ // while self
+ // .peek()
+ // .filter(|&kind| {
+ // !matches!(
+ // kind,
+ // R_CURLY
+ // | R_BRACK
+ // | R_PAREN
+ // | PART_NAME
+ // | CHAPTER_NAME
+ // | SECTION_NAME
+ // | SUBSECTION_NAME
+ // | PARAGRAPH_NAME
+ // | SUBPARAGRAPH_NAME
+ // | ENUM_ITEM_NAME
+ // | END_ENVIRONMENT_NAME
+ // )
+ // })
+ // .is_some()
+ // {
+ // self.content();
+ // }
+ // self.expect(R_PAREN);
+ // self.builder.finish_node();
+ // }
+
+ fn mixed_group(&mut self) {
+ self.builder.start_node(MIXED_GROUP.into());
+ self.eat();
+ self.trivia();
+ while self
+ .peek()
+ .filter(|&kind| {
+ !matches!(
+ kind,
+ R_CURLY
+ | R_BRACK
+ | R_PAREN
+ | PART_NAME
+ | CHAPTER_NAME
+ | SECTION_NAME
+ | SUBSECTION_NAME
+ | PARAGRAPH_NAME
+ | SUBPARAGRAPH_NAME
+ | ENUM_ITEM_NAME
+ | END_ENVIRONMENT_NAME
+ )
+ })
+ .is_some()
+ {
+ self.content(ParserContext::default());
+ }
+ self.expect2(R_BRACK, R_PAREN);
+ self.builder.finish_node();
+ }
+
+ fn key(&mut self) {
+ self.builder.start_node(KEY.into());
+ self.eat();
+ while self
+ .peek()
+ .filter(|&kind| matches!(kind, WHITESPACE | COMMENT | WORD))
+ .is_some()
+ {
+ self.eat();
+ }
+ self.builder.finish_node();
+ }
+
+ fn value(&mut self) {
+ self.builder.start_node(VALUE.into());
+ self.content(ParserContext {
+ allow_environment: true,
+ allow_comma: false,
+ });
+ self.builder.finish_node();
+ }
+
+ fn key_value_pair(&mut self) {
+ self.builder.start_node(KEY_VALUE_PAIR.into());
+ self.key();
+ if self.peek() == Some(EQUALITY_SIGN) {
+ self.eat();
+ self.trivia();
+ if self
+ .peek()
+ .filter(|&kind| {
+ !matches!(
+ kind,
+ END_ENVIRONMENT_NAME | R_CURLY | R_BRACK | R_PAREN | COMMA
+ )
+ })
+ .is_some()
+ {
+ self.value();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+ }
+
+ self.builder.finish_node();
+ }
+
+ fn key_value_body(&mut self) {
+ self.builder.start_node(KEY_VALUE_BODY.into());
+ while let Some(kind) = self.peek() {
+ match kind {
+ WHITESPACE | COMMENT => self.eat(),
+ WORD => {
+ self.key_value_pair();
+ if self.peek() == Some(COMMA) {
+ self.eat();
} else {
- children.push(self.text(scope));
+ break;
}
}
+ _ => break,
}
}
- children
- }
-
- fn group(&mut self, kind: GroupKind) -> AstNodeIndex {
- let left = self.tokens.next().unwrap();
- let scope = match kind {
- GroupKind::Group => Scope::Group,
- GroupKind::Options => Scope::Options,
- };
-
- let children = self.content(scope);
- let right_kind = match kind {
- GroupKind::Group => TokenKind::EndGroup,
- GroupKind::Options => TokenKind::EndOptions,
- };
-
- let right = if self.next_of_kind(right_kind) {
- self.tokens.next()
- } else {
- None
- };
-
- let end = right
- .as_ref()
- .map(SyntaxNode::end)
- .or_else(|| children.last().map(|child| self.tree[*child].end()))
- .unwrap_or_else(|| left.end());
- let range = Range::new(left.start(), end);
-
- let node = self.tree.add_node(Node::Group(Group {
- range,
- left,
- kind,
- right,
- }));
- self.connect(node, &children);
- node
- }
-
- fn command(&mut self) -> AstNodeIndex {
- let name = self.tokens.next().unwrap();
- let mut children = Vec::new();
- while let Some(token) = self.tokens.peek() {
- match token.kind {
- TokenKind::BeginGroup => children.push(self.group(GroupKind::Group)),
- TokenKind::BeginOptions => children.push(self.group(GroupKind::Options)),
+ self.builder.finish_node();
+ }
+
+ fn group_key_value(&mut self, node_kind: SyntaxKind, right_kind: SyntaxKind) {
+ self.builder.start_node(node_kind.into());
+ self.eat();
+ self.trivia();
+ self.key_value_body();
+ self.expect(right_kind);
+ self.builder.finish_node();
+ }
+
+ fn curly_group_key_value(&mut self) {
+ self.group_key_value(CURLY_GROUP_KEY_VALUE, R_CURLY);
+ }
+
+ fn brack_group_key_value(&mut self) {
+ self.group_key_value(BRACK_GROUP_KEY_VALUE, R_BRACK);
+ }
+
+ fn formula(&mut self) {
+ self.builder.start_node(FORMULA.into());
+ self.eat();
+ self.trivia();
+ while self
+ .peek()
+ .filter(|&kind| !matches!(kind, R_CURLY | END_ENVIRONMENT_NAME | DOLLAR))
+ .is_some()
+ {
+ self.content(ParserContext::default());
+ }
+ self.expect(DOLLAR);
+ self.builder.finish_node();
+ }
+
+ fn generic_command(&mut self) {
+ self.builder.start_node(GENERIC_COMMAND.into());
+ self.eat();
+ while let Some(kind) = self.peek() {
+ match kind {
+ WHITESPACE | COMMENT => self.eat(),
+ L_CURLY => self.curly_group(),
+ L_BRACK | L_PAREN => self.mixed_group(),
_ => break,
}
}
+ self.builder.finish_node();
+ }
+
+ fn equation(&mut self) {
+ self.builder.start_node(EQUATION.into());
+ self.eat();
+ while self
+ .peek()
+ .filter(|&kind| !matches!(kind, END_ENVIRONMENT_NAME | R_CURLY | END_EQUATION_NAME))
+ .is_some()
+ {
+ self.content(ParserContext::default());
+ }
+ self.expect(END_EQUATION_NAME);
+ self.builder.finish_node();
+ }
- let end = children
- .last()
- .map(|child| self.tree[*child].end())
- .unwrap_or_else(|| name.end());
- let range = Range::new(name.start(), end);
+ fn begin(&mut self) {
+ self.builder.start_node(BEGIN.into());
+ self.eat();
+ self.trivia();
- let node = self.tree.add_node(Node::Command(Command { range, name }));
- self.connect(node, &children);
- node
+ if self.peek() == Some(L_CURLY) {
+ self.curly_group_word();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ if self.peek() == Some(L_BRACK) {
+ self.brack_group();
+ }
+ self.builder.finish_node();
}
- fn text(&mut self, scope: Scope) -> AstNodeIndex {
- let mut words = Vec::new();
- while let Some(ref token) = self.tokens.peek() {
- let kind = token.kind;
- let opts = kind == TokenKind::EndOptions && scope != Scope::Options;
- if kind == TokenKind::Word || kind == TokenKind::BeginOptions || opts {
- words.push(self.tokens.next().unwrap());
- } else {
- break;
- }
+ fn end(&mut self) {
+ self.builder.start_node(END.into());
+ self.eat();
+ self.trivia();
+
+ if self.peek() == Some(L_CURLY) {
+ self.curly_group_word();
+ } else {
+ self.builder.token(MISSING.into(), "");
}
- let range = Range::new(words[0].start(), words[words.len() - 1].end());
- self.tree.add_node(Node::Text(Text { range, words }))
+
+ self.builder.finish_node();
}
- fn comma(&mut self) -> AstNodeIndex {
- let token = self.tokens.next().unwrap();
- let range = token.range();
- self.tree.add_node(Node::Comma(Comma { range, token }))
+ fn environment(&mut self) {
+ self.builder.start_node(ENVIRONMENT.into());
+ self.begin();
+
+ while self
+ .peek()
+ .filter(|&kind| kind != END_ENVIRONMENT_NAME)
+ .is_some()
+ {
+ self.content(ParserContext::default());
+ }
+
+ if self.peek() == Some(END_ENVIRONMENT_NAME) {
+ self.end();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+ self.builder.finish_node();
}
- fn math(&mut self) -> AstNodeIndex {
- let token = self.tokens.next().unwrap();
- let range = token.range();
- self.tree.add_node(Node::Math(Math { range, token }))
+ fn preamble(&mut self) {
+ self.builder.start_node(PREAMBLE.into());
+ while self
+ .peek()
+ .filter(|&kind| kind != END_ENVIRONMENT_NAME)
+ .is_some()
+ {
+ self.content(ParserContext::default());
+ }
+ self.builder.finish_node();
}
- fn connect(&mut self, parent: AstNodeIndex, children: &[AstNodeIndex]) {
- for child in children {
- self.tree.add_edge(parent, *child);
+ fn part(&mut self) {
+ self.builder.start_node(PART.into());
+ self.eat();
+ self.trivia();
+
+ if self.peek() == Some(L_CURLY) {
+ self.curly_group();
+ } else {
+ self.builder.token(MISSING.into(), "");
}
+
+ while self
+ .peek()
+ .filter(|&kind| !matches!(kind, END_ENVIRONMENT_NAME | R_CURLY | PART_NAME))
+ .is_some()
+ {
+ self.content(ParserContext::default());
+ }
+ self.builder.finish_node();
}
- fn next_of_kind(&mut self, kind: TokenKind) -> bool {
- self.tokens
+ fn chapter(&mut self) {
+ self.builder.start_node(CHAPTER.into());
+ self.eat();
+ self.trivia();
+
+ if self.peek() == Some(L_CURLY) {
+ self.curly_group();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ while self
.peek()
- .filter(|token| token.kind == kind)
+ .filter(|&kind| {
+ !matches!(
+ kind,
+ END_ENVIRONMENT_NAME | R_CURLY | PART_NAME | CHAPTER_NAME
+ )
+ })
.is_some()
+ {
+ self.content(ParserContext::default());
+ }
+ self.builder.finish_node();
+ }
+
+ fn section(&mut self) {
+ self.builder.start_node(SECTION.into());
+ self.eat();
+ self.trivia();
+
+ if self.peek() == Some(L_CURLY) {
+ self.curly_group();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ while self
+ .peek()
+ .filter(|&kind| {
+ !matches!(
+ kind,
+ END_ENVIRONMENT_NAME | R_CURLY | PART_NAME | CHAPTER_NAME | SECTION_NAME
+ )
+ })
+ .is_some()
+ {
+ self.content(ParserContext::default());
+ }
+ self.builder.finish_node();
+ }
+
+ fn subsection(&mut self) {
+ self.builder.start_node(SUBSECTION.into());
+ self.eat();
+ self.trivia();
+
+ if self.peek() == Some(L_CURLY) {
+ self.curly_group();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ while self
+ .peek()
+ .filter(|&kind| {
+ !matches!(
+ kind,
+ END_ENVIRONMENT_NAME
+ | R_CURLY
+ | PART_NAME
+ | CHAPTER_NAME
+ | SECTION_NAME
+ | SUBSECTION_NAME
+ )
+ })
+ .is_some()
+ {
+ self.content(ParserContext::default());
+ }
+ self.builder.finish_node();
+ }
+
+ fn subsubsection(&mut self) {
+ self.builder.start_node(SUBSUBSECTION.into());
+ self.eat();
+ self.trivia();
+
+ if self.peek() == Some(L_CURLY) {
+ self.curly_group();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ while self
+ .peek()
+ .filter(|&kind| {
+ !matches!(
+ kind,
+ END_ENVIRONMENT_NAME
+ | R_CURLY
+ | PART_NAME
+ | CHAPTER_NAME
+ | SECTION_NAME
+ | SUBSECTION_NAME
+ | SUBSUBSECTION_NAME
+ )
+ })
+ .is_some()
+ {
+ self.content(ParserContext::default());
+ }
+ self.builder.finish_node();
+ }
+
+ fn paragraph(&mut self) {
+ self.builder.start_node(PARAGRAPH.into());
+ self.eat();
+ self.trivia();
+
+ if self.peek() == Some(L_CURLY) {
+ self.curly_group();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ while self
+ .peek()
+ .filter(|&kind| {
+ !matches!(
+ kind,
+ END_ENVIRONMENT_NAME
+ | R_CURLY
+ | PART_NAME
+ | CHAPTER_NAME
+ | SECTION_NAME
+ | SUBSECTION_NAME
+ | SUBSUBSECTION_NAME
+ | PARAGRAPH_NAME
+ )
+ })
+ .is_some()
+ {
+ self.content(ParserContext::default());
+ }
+ self.builder.finish_node();
+ }
+
+ fn subparagraph(&mut self) {
+ self.builder.start_node(SUBPARAGRAPH.into());
+ self.eat();
+ self.trivia();
+
+ if self.peek() == Some(L_CURLY) {
+ self.curly_group();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ while self
+ .peek()
+ .filter(|&kind| {
+ !matches!(
+ kind,
+ END_ENVIRONMENT_NAME
+ | R_CURLY
+ | PART_NAME
+ | CHAPTER_NAME
+ | SECTION_NAME
+ | SUBSECTION_NAME
+ | SUBSUBSECTION_NAME
+ | PARAGRAPH_NAME
+ | SUBPARAGRAPH_NAME
+ )
+ })
+ .is_some()
+ {
+ self.content(ParserContext::default());
+ }
+ self.builder.finish_node();
+ }
+
+ fn enum_item(&mut self) {
+ self.builder.start_node(ENUM_ITEM.into());
+ self.eat();
+ self.trivia();
+
+ if self.peek() == Some(L_BRACK) {
+ self.brack_group();
+ }
+
+ while self
+ .peek()
+ .filter(|&kind| {
+ !matches!(
+ kind,
+ END_ENVIRONMENT_NAME
+ | R_CURLY
+ | PART_NAME
+ | CHAPTER_NAME
+ | SECTION_NAME
+ | SUBSECTION_NAME
+ | SUBSUBSECTION_NAME
+ | PARAGRAPH_NAME
+ | SUBPARAGRAPH_NAME
+ | ENUM_ITEM_NAME
+ )
+ })
+ .is_some()
+ {
+ self.content(ParserContext::default());
+ }
+ self.builder.finish_node();
+ }
+
+ fn caption(&mut self) {
+ self.builder.start_node(CAPTION.into());
+ self.eat();
+ self.trivia();
+
+ if self.peek() == Some(L_BRACK) {
+ self.brack_group();
+ }
+
+ if self.peek() == Some(L_CURLY) {
+ self.curly_group();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ self.builder.finish_node();
+ }
+
+ fn citation(&mut self) {
+ self.builder.start_node(CITATION.into());
+ self.eat();
+ self.trivia();
+ for _ in 0..2 {
+ if self.lexer.peek() == Some(L_BRACK) {
+ self.brack_group();
+ }
+ }
+
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_word_list();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ self.builder.finish_node();
+ }
+
+ fn generic_include(&mut self, kind: SyntaxKind, options: bool) {
+ self.builder.start_node(kind.into());
+ self.eat();
+ self.trivia();
+ if options && self.lexer.peek() == Some(L_BRACK) {
+ self.brack_group_key_value();
+ }
+
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_word_list();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ self.builder.finish_node();
+ }
+
+ fn package_include(&mut self) {
+ self.generic_include(PACKAGE_INCLUDE, true);
+ }
+
+ fn class_include(&mut self) {
+ self.generic_include(CLASS_INCLUDE, true);
+ }
+
+ fn latex_include(&mut self) {
+ self.generic_include(LATEX_INCLUDE, false);
+ }
+
+ fn biblatex_include(&mut self) {
+ self.generic_include(BIBLATEX_INCLUDE, true);
+ }
+
+ fn bibtex_include(&mut self) {
+ self.generic_include(BIBTEX_INCLUDE, false);
+ }
+
+ fn graphics_include(&mut self) {
+ self.generic_include(GRAPHICS_INCLUDE, true);
+ }
+
+ fn svg_include(&mut self) {
+ self.generic_include(SVG_INCLUDE, true);
+ }
+
+ fn inkscape_include(&mut self) {
+ self.generic_include(INKSCAPE_INCLUDE, true);
+ }
+
+ fn verbatim_include(&mut self) {
+ self.generic_include(VERBATIM_INCLUDE, false);
+ }
+
+ fn import(&mut self) {
+ self.builder.start_node(IMPORT.into());
+ self.eat();
+ self.trivia();
+
+ for _ in 0..2 {
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_word();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+ }
+
+ self.builder.finish_node();
+ }
+
+ fn label_definition(&mut self) {
+ self.builder.start_node(LABEL_DEFINITION.into());
+ self.eat();
+ self.trivia();
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_word();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+ self.builder.finish_node();
+ }
+
+ fn label_reference(&mut self) {
+ self.builder.start_node(LABEL_REFERENCE.into());
+ self.eat();
+ self.trivia();
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_word_list();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+ self.builder.finish_node();
+ }
+
+ fn label_reference_range(&mut self) {
+ self.builder.start_node(LABEL_REFERENCE_RANGE.into());
+ self.eat();
+ self.trivia();
+
+ for _ in 0..2 {
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_word();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+ }
+
+ self.builder.finish_node();
+ }
+
+ fn label_number(&mut self) {
+ self.builder.start_node(LABEL_NUMBER.into());
+ self.eat();
+ self.trivia();
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_word();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group();
+ self.builder.token(MISSING.into(), "");
+ }
+
+ self.builder.finish_node();
+ }
+
+ fn command_definition(&mut self) {
+ self.builder.start_node(COMMAND_DEFINITION.into());
+ self.eat();
+ self.trivia();
+
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_command();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ if self.lexer.peek() == Some(L_BRACK) {
+ self.brack_group_word();
+ }
+
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ self.builder.finish_node();
+ }
+
+ fn math_operator(&mut self) {
+ self.builder.start_node(MATH_OPERATOR.into());
+ self.eat();
+ self.trivia();
+
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_command();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ self.builder.finish_node();
+ }
+
+ fn glossary_entry_definition(&mut self) {
+ self.builder.start_node(GLOSSARY_ENTRY_DEFINITION.into());
+ self.eat();
+ self.trivia();
+
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_word();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_key_value();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ self.builder.finish_node();
+ }
+
+ fn glossary_entry_reference(&mut self) {
+ self.builder.start_node(GLOSSARY_ENTRY_REFERENCE.into());
+ self.eat();
+ self.trivia();
+
+ if self.lexer.peek() == Some(L_BRACK) {
+ self.brack_group_key_value();
+ }
+
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_word();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ self.builder.finish_node();
+ }
+
+ fn acronym_definition(&mut self) {
+ self.builder.start_node(ACRONYM_DEFINITION.into());
+ self.eat();
+ self.trivia();
+
+ if self.lexer.peek() == Some(L_BRACK) {
+ self.brack_group_key_value();
+ }
+
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_word();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ for _ in 0..2 {
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+ }
+
+ self.builder.finish_node();
+ }
+
+ fn acronym_declaration(&mut self) {
+ self.builder.start_node(ACRONYM_DECLARATION.into());
+ self.eat();
+ self.trivia();
+
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_word();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_key_value();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ self.builder.finish_node();
+ }
+
+ fn acronym_reference(&mut self) {
+ self.builder.start_node(ACRONYM_REFERENCE.into());
+ self.eat();
+ self.trivia();
+
+ if self.lexer.peek() == Some(L_BRACK) {
+ self.brack_group_key_value();
+ }
+
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_word();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ self.builder.finish_node();
+ }
+
+ fn theorem_definition(&mut self) {
+ self.builder.start_node(THEOREM_DEFINITION.into());
+ self.eat();
+ self.trivia();
+
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_word();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ if self.lexer.peek() == Some(L_BRACK) {
+ self.brack_group_word();
+ }
+
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ if self.lexer.peek() == Some(L_BRACK) {
+ self.brack_group_word();
+ }
+
+ self.builder.finish_node();
+ }
+
+ fn color_reference(&mut self) {
+ self.builder.start_node(COLOR_REFERENCE.into());
+ self.eat();
+ self.trivia();
+
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_word();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ self.builder.finish_node();
+ }
+
+ fn color_definition(&mut self) {
+ self.builder.start_node(COLOR_DEFINITION.into());
+ self.eat();
+ self.trivia();
+
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_word();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_word();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ self.builder.finish_node();
+ }
+
+ fn color_set_definition(&mut self) {
+ self.builder.start_node(COLOR_SET_DEFINITION.into());
+ self.eat();
+ self.trivia();
+
+ if self.lexer.peek() == Some(L_BRACK) {
+ self.brack_group_word();
+ }
+
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_word_list();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ for _ in 0..3 {
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_word();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+ }
+
+ self.builder.finish_node();
+ }
+
+ fn tikz_library_import(&mut self) {
+ self.builder.start_node(TIKZ_LIBRARY_IMPORT.into());
+ self.eat();
+ self.trivia();
+
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_word_list();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ self.builder.finish_node();
+ }
+
+ fn environment_definition(&mut self) {
+ self.builder.start_node(ENVIRONMENT_DEFINITION.into());
+ self.eat();
+ self.trivia();
+
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_word();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+
+ if self.lexer.peek() == Some(L_BRACK) {
+ self.brack_group_word();
+ }
+
+ for _ in 0..2 {
+ if self.lexer.peek() == Some(L_CURLY) {
+ self.curly_group_without_environments();
+ } else {
+ self.builder.token(MISSING.into(), "");
+ }
+ }
+
+ self.builder.finish_node();
+ }
+}
+
+pub fn parse(text: &str) -> Parse {
+ Parser::new(text).parse()
+}
+
+#[cfg(test)]
+mod tests {
+ use insta::assert_debug_snapshot;
+
+ use super::*;
+
+ fn setup(text: &str) -> SyntaxNode {
+ parse(&text.trim().replace("\r", "")).root
+ }
+
+ #[test]
+ fn test_empty() {
+ assert_debug_snapshot!(setup(r#""#));
+ }
+
+ #[test]
+ fn test_hello_world() {
+ assert_debug_snapshot!(setup(r#"Hello World!"#));
+ }
+
+ #[test]
+ fn test_generic_command_empty() {
+ assert_debug_snapshot!(setup(r#"\foo"#));
+ }
+
+ #[test]
+ fn test_generic_command_escape() {
+ assert_debug_snapshot!(setup(r#"\#"#));
+ }
+
+ #[test]
+ fn test_generic_command_args() {
+ assert_debug_snapshot!(setup(r#"\foo{bar}[qux]"#));
+ }
+
+ #[test]
+ fn test_inline() {
+ assert_debug_snapshot!(setup(r#"$x \in [0, \infty)$"#));
+ }
+
+ #[test]
+ fn test_inline_double_dollar() {
+ assert_debug_snapshot!(setup(r#"$$x \in [0, \infty)$$"#));
+ }
+
+ #[test]
+ fn test_brace_group_simple() {
+ assert_debug_snapshot!(setup(r#"{hello world}"#));
+ }
+
+ #[test]
+ fn test_brace_group_missing_end() {
+ assert_debug_snapshot!(setup(r#"{hello world"#));
+ }
+
+ #[test]
+ fn test_unmatched_braces() {
+ assert_debug_snapshot!(setup(r#"}{"#));
+ }
+
+ #[test]
+ fn test_unmatched_brackets() {
+ assert_debug_snapshot!(setup(r#"]["#));
+ }
+
+ #[test]
+ fn test_unmatched_brackets_with_group() {
+ assert_debug_snapshot!(setup(r#"{][}"#));
+ }
+
+ #[test]
+ fn test_escaped_brackets() {
+ assert_debug_snapshot!(setup(r#"{[}{]}"#));
+ }
+
+ #[test]
+ fn test_parameter() {
+ assert_debug_snapshot!(setup(r#"#1"#));
+ }
+
+ #[test]
+ fn test_parameter_error() {
+ assert_debug_snapshot!(setup(r#"#"#));
+ }
+
+ #[test]
+ fn test_environment_simple() {
+ assert_debug_snapshot!(setup(r#"\begin{foo} Hello World \end{bar}"#));
+ }
+
+ #[test]
+ fn test_environment_nested() {
+ assert_debug_snapshot!(setup(r#"\begin{foo} \begin{qux} \end{baz} \end{bar}"#));
+ }
+
+ #[test]
+ fn test_environment_nested_missing_braces() {
+ assert_debug_snapshot!(setup(
+ r#"\begin{foo \begin{qux Hello World \end{baz} \end{bar"#
+ ));
+ }
+
+ #[test]
+ fn test_structure_siblings() {
+ assert_debug_snapshot!(setup(r#"\section{Foo} Foo \section{Bar} Bar"#));
+ }
+
+ #[test]
+ fn test_structure_nested() {
+ assert_debug_snapshot!(setup(
+ r#"\part{1}\chapter{2}\section{3}\subsection{4}\subsubsection{5}\paragraph{6}\subparagraph{7}"#
+ ));
+ }
+
+ #[test]
+ fn test_structure_enum_item() {
+ assert_debug_snapshot!(setup(
+ r#"\begin{enumerate} \item 1 \item[2] 2 \item 3 \end{enumerate}"#
+ ));
+ }
+
+ #[test]
+ fn test_structure_invalid_nesting() {
+ assert_debug_snapshot!(setup(r#"\section{Foo} \chapter{Bar}"#));
+ }
+
+ #[test]
+ fn test_equation() {
+ assert_debug_snapshot!(setup(r#"\[ foo bar \]"#));
+ }
+
+ #[test]
+ fn test_equation_missing_end() {
+ assert_debug_snapshot!(setup(r#"\begin{a} \[ foo bar \end{b}"#));
+ }
+
+ #[test]
+ fn test_equation_missing_begin() {
+ assert_debug_snapshot!(setup(r#"\begin{a} foo bar \] \end{b}"#));
+ }
+
+ #[test]
+ fn test_caption_minimal() {
+ assert_debug_snapshot!(setup(r#"\caption{Foo \Bar Baz}"#));
+ }
+
+ #[test]
+ fn test_caption_minimal_error() {
+ assert_debug_snapshot!(setup(r#"\caption{Foo \Bar Baz"#));
+ }
+
+ #[test]
+ fn test_caption() {
+ assert_debug_snapshot!(setup(r#"\caption[qux]{Foo \Bar Baz}"#));
+ }
+
+ #[test]
+ fn test_caption_error() {
+ assert_debug_snapshot!(setup(r#"\caption[qux]{Foo \Bar Baz"#));
+ }
+
+ #[test]
+ fn test_caption_figure() {
+ assert_debug_snapshot!(setup(r#"\begin{figure}\caption{Foo}\end{figure}"#));
+ }
+
+ #[test]
+ fn test_citation_empty() {
+ assert_debug_snapshot!(setup(r#"\cite{}"#));
+ }
+
+ #[test]
+ fn test_citation_simple() {
+ assert_debug_snapshot!(setup(r#"\cite{foo}"#));
+ }
+
+ #[test]
+ fn test_citation_multiple_keys() {
+ assert_debug_snapshot!(setup(r#"\cite{foo, bar}"#));
+ }
+
+ #[test]
+ fn test_citation_star() {
+ assert_debug_snapshot!(setup(r#"\nocite{*}"#));
+ }
+
+ #[test]
+ fn test_citation_prenote() {
+ assert_debug_snapshot!(setup(r#"\cite[foo]{bar}"#));
+ }
+
+ #[test]
+ fn test_citation_prenote_postnote() {
+ assert_debug_snapshot!(setup(r#"\cite[foo][bar]{baz}"#));
+ }
+
+ #[test]
+ fn test_citation_missing_brace() {
+ assert_debug_snapshot!(setup(r#"\cite{foo"#));
+ }
+
+ #[test]
+ fn test_citation_redundant_comma() {
+ assert_debug_snapshot!(setup(r#"\cite{,foo,}"#));
+ }
+
+ #[test]
+ fn test_package_include_empty() {
+ assert_debug_snapshot!(setup(r#"\usepackage{}"#));
+ }
+
+ #[test]
+ fn test_package_include_simple() {
+ assert_debug_snapshot!(setup(r#"\usepackage{amsmath}"#));
+ }
+
+ #[test]
+ fn test_package_include_multiple() {
+ assert_debug_snapshot!(setup(r#"\usepackage{amsmath, lipsum}"#));
+ }
+
+ #[test]
+ fn test_package_include_options() {
+ assert_debug_snapshot!(setup(r#"\usepackage[foo = bar, baz, qux]{amsmath}"#));
+ }
+
+ #[test]
+ fn test_class_include_empty() {
+ assert_debug_snapshot!(setup(r#"\documentclass{}"#));
+ }
+
+ #[test]
+ fn test_class_include_simple() {
+ assert_debug_snapshot!(setup(r#"\documentclass{article}"#));
+ }
+
+ #[test]
+ fn test_class_include_options() {
+ assert_debug_snapshot!(setup(r#"\documentclass[foo = bar, baz, qux]{article}"#));
+ }
+
+ #[test]
+ fn test_latex_include_simple() {
+ assert_debug_snapshot!(setup(r#"\include{foo/bar}"#));
+ }
+
+ #[test]
+ fn test_latex_input_simple() {
+ assert_debug_snapshot!(setup(r#"\input{foo/bar.tex}"#));
+ }
+
+ #[test]
+ fn test_biblatex_include_simple() {
+ assert_debug_snapshot!(setup(r#"\addbibresource{foo/bar.bib}"#));
+ }
+
+ #[test]
+ fn test_biblatex_include_options() {
+ assert_debug_snapshot!(setup(r#"\addbibresource[foo=bar, baz]{foo/bar.bib}"#));
+ }
+
+ #[test]
+ fn test_bibtex_include_simple() {
+ assert_debug_snapshot!(setup(r#"\bibliography{foo/bar}"#));
+ }
+
+ #[test]
+ fn test_graphics_include_simple() {
+ assert_debug_snapshot!(setup(r#"\includegraphics{foo/bar.pdf}"#));
+ }
+
+ #[test]
+ fn test_graphics_include_options() {
+ assert_debug_snapshot!(setup(r#"\includegraphics[scale=.5]{foo/bar.pdf}"#));
+ }
+
+ #[test]
+ fn test_svg_include_simple() {
+ assert_debug_snapshot!(setup(r#"\includesvg{foo/bar.svg}"#));
+ }
+
+ #[test]
+ fn test_svg_include_options() {
+ assert_debug_snapshot!(setup(r#"\includesvg[scale=.5]{foo/bar.svg}"#));
+ }
+
+ #[test]
+ fn test_inkscape_include_simple() {
+ assert_debug_snapshot!(setup(r#"\includesvg{foo/bar}"#));
+ }
+
+ #[test]
+ fn test_inkscape_include_options() {
+ assert_debug_snapshot!(setup(r#"\includesvg[scale=.5]{foo/bar}"#));
+ }
+
+ #[test]
+ fn test_verbatim_include_simple() {
+ assert_debug_snapshot!(setup(r#"\verbatiminput{foo/bar.txt}"#));
+ }
+
+ #[test]
+ fn test_import_simple() {
+ assert_debug_snapshot!(setup(r#"\import{foo}{bar}"#));
+ }
+
+ #[test]
+ fn test_import_incomplete() {
+ assert_debug_snapshot!(setup(r#"\import{foo"#));
+ }
+
+ #[test]
+ fn test_label_definition_simple() {
+ assert_debug_snapshot!(setup(r#"\label{foo}"#));
+ }
+
+ #[test]
+ fn test_label_reference_simple() {
+ assert_debug_snapshot!(setup(r#"\ref{foo}"#));
+ }
+
+ #[test]
+ fn test_label_reference_multiple() {
+ assert_debug_snapshot!(setup(r#"\ref{foo, bar}"#));
+ }
+
+ #[test]
+ fn test_equation_label_reference_simple() {
+ assert_debug_snapshot!(setup(r#"\eqref{foo}"#));
+ }
+
+ #[test]
+ fn test_label_reference_range_simple() {
+ assert_debug_snapshot!(setup(r#"\crefrange{foo}{bar}"#));
+ }
+
+ #[test]
+ fn test_label_reference_range_incomplete() {
+ assert_debug_snapshot!(setup(r#"\crefrange{foo}"#));
+ }
+
+ #[test]
+ fn test_label_reference_range_error() {
+ assert_debug_snapshot!(setup(r#"\crefrange{foo{bar}"#));
+ }
+
+ #[test]
+ fn test_label_number() {
+ assert_debug_snapshot!(setup(r#"\newlabel{foo}{{1.1}}"#));
+ }
+
+ #[test]
+ fn test_command_definition_simple() {
+ assert_debug_snapshot!(setup(r#"\newcommand[1]{\id}{#1}"#));
+ }
+
+ #[test]
+ fn test_command_definition_no_argc() {
+ assert_debug_snapshot!(setup(r#"\newcommand{\foo}{foo}"#));
+ }
+
+ #[test]
+ fn test_command_definition_no_impl() {
+ assert_debug_snapshot!(setup(r#"\newcommand{\foo}"#));
+ }
+
+ #[test]
+ fn test_command_definition_no_impl_error() {
+ assert_debug_snapshot!(setup(r#"\newcommand{\foo"#));
+ }
+
+ #[test]
+ fn test_math_operator_simple() {
+ assert_debug_snapshot!(setup(r#"\DeclareMathOperator{\foo}{foo}"#));
+ }
+
+ #[test]
+ fn test_math_operator_no_impl() {
+ assert_debug_snapshot!(setup(r#"\DeclareMathOperator{\foo}"#));
+ }
+
+ #[test]
+ fn test_glossary_entry_definition_simple() {
+ assert_debug_snapshot!(setup(r#"\newglossaryentry{foo}{bar = baz, qux,}"#));
+ }
+
+ #[test]
+ fn test_glossary_entry_reference_simple() {
+ assert_debug_snapshot!(setup(r#"\gls{foo}"#));
+ }
+
+ #[test]
+ fn test_glossary_entry_reference_options() {
+ assert_debug_snapshot!(setup(r#"\gls[foo = bar, qux]{baz}"#));
+ }
+
+ #[test]
+ fn test_acroynm_definition_simple() {
+ assert_debug_snapshot!(setup(r#"\newacronym{fpsLabel}{FPS}{Frame per Second}"#));
+ }
+
+ #[test]
+ fn test_acroynm_definition_options() {
+ assert_debug_snapshot!(setup(
+ r#"\newacronym[longplural={Frames per Second}]{fpsLabel}{FPS}{Frame per Second}"#
+ ));
+ }
+
+ #[test]
+ fn test_acroynm_reference_simple() {
+ assert_debug_snapshot!(setup(r#"\acrshort{fpsLabel}"#));
+ }
+
+ #[test]
+ fn test_acroynm_reference_options() {
+ assert_debug_snapshot!(setup(r#"\acrshort[foo=bar,baz]{fpsLabel}"#));
+ }
+
+ #[test]
+ fn test_theorem_definition_only_name() {
+ assert_debug_snapshot!(setup(r#"\newtheorem{foo}"#));
+ }
+
+ #[test]
+ fn test_theorem_definition_name_with_description() {
+ assert_debug_snapshot!(setup(r#"\newtheorem{foo}{Foo}"#));
+ }
+
+ #[test]
+ fn test_theorem_definition_name_with_description_and_counter() {
+ assert_debug_snapshot!(setup(r#"\newtheorem{foo}[bar]{Foo}"#));
+ }
+
+ #[test]
+ fn test_theorem_definition_name_with_counter() {
+ assert_debug_snapshot!(setup(r#"\newtheorem{foo}[bar]"#));
+ }
+
+ #[test]
+ fn test_theorem_definition_full() {
+ assert_debug_snapshot!(setup(r#"\newtheorem{foo}[bar]{Foo}[baz]"#));
+ }
+
+ #[test]
+ fn test_color_reference_simple() {
+ assert_debug_snapshot!(setup(r#"\color{black}"#));
+ }
+
+ #[test]
+ fn test_color_definition_simple() {
+ assert_debug_snapshot!(setup(r#"\definecolor{foo}{rgb}{255,168,0}"#));
+ }
+
+ #[test]
+ fn test_color_set_definition_simple() {
+ assert_debug_snapshot!(setup(r#"\definecolorset[ty]{rgb,HTML}{foo}{bar}{baz}"#));
+ }
+
+ #[test]
+ fn test_color_set_definition_error1() {
+ assert_debug_snapshot!(setup(r#"\definecolorset[ty]{rgb,HTML}{foo}{bar}"#));
+ }
+
+ #[test]
+ fn test_color_set_definition_error2() {
+ assert_debug_snapshot!(setup(r#"\definecolorset{rgb,HTML}{foo}"#));
+ }
+
+ #[test]
+ fn test_color_set_definition_error3() {
+ assert_debug_snapshot!(setup(r#"\definecolorset{rgb,HTML}"#));
+ }
+
+ #[test]
+ fn test_color_set_definition_error4() {
+ assert_debug_snapshot!(setup(r#"\definecolorset"#));
+ }
+
+ #[test]
+ fn test_pgf_library_import_simple() {
+ assert_debug_snapshot!(setup(r#"\usepgflibrary{foo}"#));
+ }
+
+ #[test]
+ fn test_tikz_library_import_simple() {
+ assert_debug_snapshot!(setup(r#"\usetikzlibrary{foo}"#));
+ }
+
+ #[test]
+ fn test_environment_definition() {
+ assert_debug_snapshot!(setup(r#"\newenvironment{bar}[1]{\begin{foo}}{\end{foo}}"#));
+ }
+
+ #[test]
+ fn test_acronym_declaration() {
+ assert_debug_snapshot!(setup(
+ r#"\DeclareAcronym{eg}{short = e.g,long = for example,tag = abbrev}"#
+ ));
}
}
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__command_with_parameter.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__command_with_parameter.snap
new file mode 100644
index 0000000000..9cff735e99
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__command_with_parameter.snap
@@ -0,0 +1,47 @@
+---
+source: src/syntax/latex/lexer.rs
+expression: "verify(r#\"\\newcommand{\\id}[1]{#1}\"#)"
+
+---
+[
+ (
+ COMMAND_DEFINITION_NAME,
+ "\\newcommand",
+ ),
+ (
+ L_CURLY,
+ "{",
+ ),
+ (
+ GENERIC_COMMAND_NAME,
+ "\\id",
+ ),
+ (
+ R_CURLY,
+ "}",
+ ),
+ (
+ L_BRACK,
+ "[",
+ ),
+ (
+ WORD,
+ "1",
+ ),
+ (
+ R_BRACK,
+ "]",
+ ),
+ (
+ L_CURLY,
+ "{",
+ ),
+ (
+ PARAMETER,
+ "#1",
+ ),
+ (
+ R_CURLY,
+ "}",
+ ),
+]
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__command_with_star.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__command_with_star.snap
new file mode 100644
index 0000000000..41310edaa1
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__command_with_star.snap
@@ -0,0 +1,23 @@
+---
+source: src/syntax/latex/lexer.rs
+expression: "verify(r#\"\\section*{Foo}\"#)"
+
+---
+[
+ (
+ SECTION_NAME,
+ "\\section*",
+ ),
+ (
+ L_CURLY,
+ "{",
+ ),
+ (
+ WORD,
+ "Foo",
+ ),
+ (
+ R_CURLY,
+ "}",
+ ),
+]
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__comment.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__comment.snap
new file mode 100644
index 0000000000..2186c957c5
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__comment.snap
@@ -0,0 +1,35 @@
+---
+source: src/syntax/latex/lexer.rs
+expression: "verify(\"hello %world\\r\\ntest %test\")"
+
+---
+[
+ (
+ WORD,
+ "hello",
+ ),
+ (
+ WHITESPACE,
+ " ",
+ ),
+ (
+ COMMENT,
+ "%world",
+ ),
+ (
+ WHITESPACE,
+ "\r\n",
+ ),
+ (
+ WORD,
+ "test",
+ ),
+ (
+ WHITESPACE,
+ " ",
+ ),
+ (
+ COMMENT,
+ "%test",
+ ),
+]
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__delimiters.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__delimiters.snap
new file mode 100644
index 0000000000..36bec0a276
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__delimiters.snap
@@ -0,0 +1,79 @@
+---
+source: src/syntax/latex/lexer.rs
+expression: "verify(r#\"{foo} (bar) [baz, qux = foo-bar]\"#)"
+
+---
+[
+ (
+ L_CURLY,
+ "{",
+ ),
+ (
+ WORD,
+ "foo",
+ ),
+ (
+ R_CURLY,
+ "}",
+ ),
+ (
+ WHITESPACE,
+ " ",
+ ),
+ (
+ L_PAREN,
+ "(",
+ ),
+ (
+ WORD,
+ "bar",
+ ),
+ (
+ R_PAREN,
+ ")",
+ ),
+ (
+ WHITESPACE,
+ " ",
+ ),
+ (
+ L_BRACK,
+ "[",
+ ),
+ (
+ WORD,
+ "baz",
+ ),
+ (
+ COMMA,
+ ",",
+ ),
+ (
+ WHITESPACE,
+ " ",
+ ),
+ (
+ WORD,
+ "qux",
+ ),
+ (
+ WHITESPACE,
+ " ",
+ ),
+ (
+ EQUALITY_SIGN,
+ "=",
+ ),
+ (
+ WHITESPACE,
+ " ",
+ ),
+ (
+ WORD,
+ "foo-bar",
+ ),
+ (
+ R_BRACK,
+ "]",
+ ),
+]
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__empty.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__empty.snap
new file mode 100644
index 0000000000..61735b5087
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__empty.snap
@@ -0,0 +1,6 @@
+---
+source: src/syntax/latex/lexer.rs
+expression: "verify(r#\"\"#)"
+
+---
+[]
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__escape_sequence.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__escape_sequence.snap
new file mode 100644
index 0000000000..4dbc1bb8f5
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__escape_sequence.snap
@@ -0,0 +1,19 @@
+---
+source: src/syntax/latex/lexer.rs
+expression: "verify(r#\"\\% hello\"#)"
+
+---
+[
+ (
+ GENERIC_COMMAND_NAME,
+ "\\%",
+ ),
+ (
+ WHITESPACE,
+ " ",
+ ),
+ (
+ WORD,
+ "hello",
+ ),
+]
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__formula.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__formula.snap
new file mode 100644
index 0000000000..3fb7079384
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__formula.snap
@@ -0,0 +1,55 @@
+---
+source: src/syntax/latex/lexer.rs
+expression: "verify(r#\"$ f(x) = y $$\"#)"
+
+---
+[
+ (
+ DOLLAR,
+ "$",
+ ),
+ (
+ WHITESPACE,
+ " ",
+ ),
+ (
+ WORD,
+ "f",
+ ),
+ (
+ L_PAREN,
+ "(",
+ ),
+ (
+ WORD,
+ "x",
+ ),
+ (
+ R_PAREN,
+ ")",
+ ),
+ (
+ WHITESPACE,
+ " ",
+ ),
+ (
+ EQUALITY_SIGN,
+ "=",
+ ),
+ (
+ WHITESPACE,
+ " ",
+ ),
+ (
+ WORD,
+ "y",
+ ),
+ (
+ WHITESPACE,
+ " ",
+ ),
+ (
+ DOLLAR,
+ "$$",
+ ),
+]
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__invalid_parameter.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__invalid_parameter.snap
new file mode 100644
index 0000000000..adadfcf0ef
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__invalid_parameter.snap
@@ -0,0 +1,11 @@
+---
+source: src/syntax/latex/lexer.rs
+expression: "verify(r#\"#\"#)"
+
+---
+[
+ (
+ PARAMETER,
+ "#",
+ ),
+]
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acronym_declaration.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acronym_declaration.snap
new file mode 100644
index 0000000000..dfd05750d3
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acronym_declaration.snap
@@ -0,0 +1,49 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\DeclareAcronym{eg}{short = e.g,long = for example,tag = abbrev}\"#)"
+
+---
+ROOT@0..64
+ PREAMBLE@0..64
+ ACRONYM_DECLARATION@0..64
+ ACRONYM_DECLARATION_NAME@0..15 "\\DeclareAcronym"
+ CURLY_GROUP_WORD@15..19
+ L_CURLY@15..16 "{"
+ WORD@16..18 "eg"
+ R_CURLY@18..19 "}"
+ CURLY_GROUP_KEY_VALUE@19..64
+ L_CURLY@19..20 "{"
+ KEY_VALUE_BODY@20..63
+ KEY_VALUE_PAIR@20..31
+ KEY@20..26
+ WORD@20..25 "short"
+ WHITESPACE@25..26 " "
+ EQUALITY_SIGN@26..27 "="
+ WHITESPACE@27..28 " "
+ VALUE@28..31
+ TEXT@28..31
+ WORD@28..31 "e.g"
+ COMMA@31..32 ","
+ KEY_VALUE_PAIR@32..50
+ KEY@32..37
+ WORD@32..36 "long"
+ WHITESPACE@36..37 " "
+ EQUALITY_SIGN@37..38 "="
+ WHITESPACE@38..39 " "
+ VALUE@39..50
+ TEXT@39..50
+ WORD@39..42 "for"
+ WHITESPACE@42..43 " "
+ WORD@43..50 "example"
+ COMMA@50..51 ","
+ KEY_VALUE_PAIR@51..63
+ KEY@51..55
+ WORD@51..54 "tag"
+ WHITESPACE@54..55 " "
+ EQUALITY_SIGN@55..56 "="
+ WHITESPACE@56..57 " "
+ VALUE@57..63
+ TEXT@57..63
+ WORD@57..63 "abbrev"
+ R_CURLY@63..64 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_definition_options.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_definition_options.snap
new file mode 100644
index 0000000000..1838e80f9a
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_definition_options.snap
@@ -0,0 +1,46 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\newacronym[longplural={Frames per Second}]{fpsLabel}{FPS}{Frame per Second}\"#)"
+
+---
+ROOT@0..76
+ PREAMBLE@0..76
+ ACRONYM_DEFINITION@0..76
+ ACRONYM_DEFINITION_NAME@0..11 "\\newacronym"
+ BRACK_GROUP_KEY_VALUE@11..43
+ L_BRACK@11..12 "["
+ KEY_VALUE_BODY@12..42
+ KEY_VALUE_PAIR@12..42
+ KEY@12..22
+ WORD@12..22 "longplural"
+ EQUALITY_SIGN@22..23 "="
+ VALUE@23..42
+ CURLY_GROUP@23..42
+ L_CURLY@23..24 "{"
+ TEXT@24..41
+ WORD@24..30 "Frames"
+ WHITESPACE@30..31 " "
+ WORD@31..34 "per"
+ WHITESPACE@34..35 " "
+ WORD@35..41 "Second"
+ R_CURLY@41..42 "}"
+ R_BRACK@42..43 "]"
+ CURLY_GROUP_WORD@43..53
+ L_CURLY@43..44 "{"
+ WORD@44..52 "fpsLabel"
+ R_CURLY@52..53 "}"
+ CURLY_GROUP@53..58
+ L_CURLY@53..54 "{"
+ TEXT@54..57
+ WORD@54..57 "FPS"
+ R_CURLY@57..58 "}"
+ CURLY_GROUP@58..76
+ L_CURLY@58..59 "{"
+ TEXT@59..75
+ WORD@59..64 "Frame"
+ WHITESPACE@64..65 " "
+ WORD@65..68 "per"
+ WHITESPACE@68..69 " "
+ WORD@69..75 "Second"
+ R_CURLY@75..76 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_definition_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_definition_simple.snap
new file mode 100644
index 0000000000..1d32bed249
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_definition_simple.snap
@@ -0,0 +1,28 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\newacronym{fpsLabel}{FPS}{Frame per Second}\"#)"
+
+---
+ROOT@0..44
+ PREAMBLE@0..44
+ ACRONYM_DEFINITION@0..44
+ ACRONYM_DEFINITION_NAME@0..11 "\\newacronym"
+ CURLY_GROUP_WORD@11..21
+ L_CURLY@11..12 "{"
+ WORD@12..20 "fpsLabel"
+ R_CURLY@20..21 "}"
+ CURLY_GROUP@21..26
+ L_CURLY@21..22 "{"
+ TEXT@22..25
+ WORD@22..25 "FPS"
+ R_CURLY@25..26 "}"
+ CURLY_GROUP@26..44
+ L_CURLY@26..27 "{"
+ TEXT@27..43
+ WORD@27..32 "Frame"
+ WHITESPACE@32..33 " "
+ WORD@33..36 "per"
+ WHITESPACE@36..37 " "
+ WORD@37..43 "Second"
+ R_CURLY@43..44 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_reference_options.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_reference_options.snap
new file mode 100644
index 0000000000..ce5b85c1cf
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_reference_options.snap
@@ -0,0 +1,29 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\acrshort[foo=bar,baz]{fpsLabel}\"#)"
+
+---
+ROOT@0..32
+ PREAMBLE@0..32
+ ACRONYM_REFERENCE@0..32
+ ACRONYM_REFERENCE_NAME@0..9 "\\acrshort"
+ BRACK_GROUP_KEY_VALUE@9..22
+ L_BRACK@9..10 "["
+ KEY_VALUE_BODY@10..21
+ KEY_VALUE_PAIR@10..17
+ KEY@10..13
+ WORD@10..13 "foo"
+ EQUALITY_SIGN@13..14 "="
+ VALUE@14..17
+ TEXT@14..17
+ WORD@14..17 "bar"
+ COMMA@17..18 ","
+ KEY_VALUE_PAIR@18..21
+ KEY@18..21
+ WORD@18..21 "baz"
+ R_BRACK@21..22 "]"
+ CURLY_GROUP_WORD@22..32
+ L_CURLY@22..23 "{"
+ WORD@23..31 "fpsLabel"
+ R_CURLY@31..32 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_reference_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_reference_simple.snap
new file mode 100644
index 0000000000..5df8059f1b
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_reference_simple.snap
@@ -0,0 +1,14 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\acrshort{fpsLabel}\"#)"
+
+---
+ROOT@0..19
+ PREAMBLE@0..19
+ ACRONYM_REFERENCE@0..19
+ ACRONYM_REFERENCE_NAME@0..9 "\\acrshort"
+ CURLY_GROUP_WORD@9..19
+ L_CURLY@9..10 "{"
+ WORD@10..18 "fpsLabel"
+ R_CURLY@18..19 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__biblatex_include_options.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__biblatex_include_options.snap
new file mode 100644
index 0000000000..c910699363
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__biblatex_include_options.snap
@@ -0,0 +1,30 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\addbibresource[foo=bar, baz]{foo/bar.bib}\"#)"
+
+---
+ROOT@0..42
+ PREAMBLE@0..42
+ BIBLATEX_INCLUDE@0..42
+ BIBLATEX_INCLUDE_NAME@0..15 "\\addbibresource"
+ BRACK_GROUP_KEY_VALUE@15..29
+ L_BRACK@15..16 "["
+ KEY_VALUE_BODY@16..28
+ KEY_VALUE_PAIR@16..23
+ KEY@16..19
+ WORD@16..19 "foo"
+ EQUALITY_SIGN@19..20 "="
+ VALUE@20..23
+ TEXT@20..23
+ WORD@20..23 "bar"
+ COMMA@23..24 ","
+ WHITESPACE@24..25 " "
+ KEY_VALUE_PAIR@25..28
+ KEY@25..28
+ WORD@25..28 "baz"
+ R_BRACK@28..29 "]"
+ CURLY_GROUP_WORD_LIST@29..42
+ L_CURLY@29..30 "{"
+ WORD@30..41 "foo/bar.bib"
+ R_CURLY@41..42 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__biblatex_include_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__biblatex_include_simple.snap
new file mode 100644
index 0000000000..3549b985c4
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__biblatex_include_simple.snap
@@ -0,0 +1,14 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\addbibresource{foo/bar.bib}\"#)"
+
+---
+ROOT@0..28
+ PREAMBLE@0..28
+ BIBLATEX_INCLUDE@0..28
+ BIBLATEX_INCLUDE_NAME@0..15 "\\addbibresource"
+ CURLY_GROUP_WORD_LIST@15..28
+ L_CURLY@15..16 "{"
+ WORD@16..27 "foo/bar.bib"
+ R_CURLY@27..28 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__bibtex_include_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__bibtex_include_simple.snap
new file mode 100644
index 0000000000..f0d30ebfa3
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__bibtex_include_simple.snap
@@ -0,0 +1,14 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\bibliography{foo/bar}\"#)"
+
+---
+ROOT@0..22
+ PREAMBLE@0..22
+ BIBTEX_INCLUDE@0..22
+ BIBTEX_INCLUDE_NAME@0..13 "\\bibliography"
+ CURLY_GROUP_WORD_LIST@13..22
+ L_CURLY@13..14 "{"
+ WORD@14..21 "foo/bar"
+ R_CURLY@21..22 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__brace_group_missing_end.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__brace_group_missing_end.snap
new file mode 100644
index 0000000000..c5fc46ec4d
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__brace_group_missing_end.snap
@@ -0,0 +1,15 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"{hello world\"#)"
+
+---
+ROOT@0..12
+ PREAMBLE@0..12
+ CURLY_GROUP@0..12
+ L_CURLY@0..1 "{"
+ TEXT@1..12
+ WORD@1..6 "hello"
+ WHITESPACE@6..7 " "
+ WORD@7..12 "world"
+ MISSING@12..12 ""
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__brace_group_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__brace_group_simple.snap
new file mode 100644
index 0000000000..10ebc4418e
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__brace_group_simple.snap
@@ -0,0 +1,15 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"{hello world}\"#)"
+
+---
+ROOT@0..13
+ PREAMBLE@0..13
+ CURLY_GROUP@0..13
+ L_CURLY@0..1 "{"
+ TEXT@1..12
+ WORD@1..6 "hello"
+ WHITESPACE@6..7 " "
+ WORD@7..12 "world"
+ R_CURLY@12..13 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption.snap
new file mode 100644
index 0000000000..f598a8f7e9
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption.snap
@@ -0,0 +1,26 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\caption[qux]{Foo \\Bar Baz}\"#)"
+
+---
+ROOT@0..27
+ PREAMBLE@0..27
+ CAPTION@0..27
+ CAPTION_NAME@0..8 "\\caption"
+ BRACK_GROUP@8..13
+ L_BRACK@8..9 "["
+ TEXT@9..12
+ WORD@9..12 "qux"
+ R_BRACK@12..13 "]"
+ CURLY_GROUP@13..27
+ L_CURLY@13..14 "{"
+ TEXT@14..18
+ WORD@14..17 "Foo"
+ WHITESPACE@17..18 " "
+ GENERIC_COMMAND@18..23
+ GENERIC_COMMAND_NAME@18..22 "\\Bar"
+ WHITESPACE@22..23 " "
+ TEXT@23..26
+ WORD@23..26 "Baz"
+ R_CURLY@26..27 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_error.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_error.snap
new file mode 100644
index 0000000000..13697e882b
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_error.snap
@@ -0,0 +1,26 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\caption[qux]{Foo \\Bar Baz\"#)"
+
+---
+ROOT@0..26
+ PREAMBLE@0..26
+ CAPTION@0..26
+ CAPTION_NAME@0..8 "\\caption"
+ BRACK_GROUP@8..13
+ L_BRACK@8..9 "["
+ TEXT@9..12
+ WORD@9..12 "qux"
+ R_BRACK@12..13 "]"
+ CURLY_GROUP@13..26
+ L_CURLY@13..14 "{"
+ TEXT@14..18
+ WORD@14..17 "Foo"
+ WHITESPACE@17..18 " "
+ GENERIC_COMMAND@18..23
+ GENERIC_COMMAND_NAME@18..22 "\\Bar"
+ WHITESPACE@22..23 " "
+ TEXT@23..26
+ WORD@23..26 "Baz"
+ MISSING@26..26 ""
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_figure.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_figure.snap
new file mode 100644
index 0000000000..4b963caabd
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_figure.snap
@@ -0,0 +1,28 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\begin{figure}\\caption{Foo}\\end{figure}\"#)"
+
+---
+ROOT@0..39
+ PREAMBLE@0..39
+ ENVIRONMENT@0..39
+ BEGIN@0..14
+ BEGIN_ENVIRONMENT_NAME@0..6 "\\begin"
+ CURLY_GROUP_WORD@6..14
+ L_CURLY@6..7 "{"
+ WORD@7..13 "figure"
+ R_CURLY@13..14 "}"
+ CAPTION@14..27
+ CAPTION_NAME@14..22 "\\caption"
+ CURLY_GROUP@22..27
+ L_CURLY@22..23 "{"
+ TEXT@23..26
+ WORD@23..26 "Foo"
+ R_CURLY@26..27 "}"
+ END@27..39
+ END_ENVIRONMENT_NAME@27..31 "\\end"
+ CURLY_GROUP_WORD@31..39
+ L_CURLY@31..32 "{"
+ WORD@32..38 "figure"
+ R_CURLY@38..39 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_minimal.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_minimal.snap
new file mode 100644
index 0000000000..bf9e795e18
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_minimal.snap
@@ -0,0 +1,21 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\caption{Foo \\Bar Baz}\"#)"
+
+---
+ROOT@0..22
+ PREAMBLE@0..22
+ CAPTION@0..22
+ CAPTION_NAME@0..8 "\\caption"
+ CURLY_GROUP@8..22
+ L_CURLY@8..9 "{"
+ TEXT@9..13
+ WORD@9..12 "Foo"
+ WHITESPACE@12..13 " "
+ GENERIC_COMMAND@13..18
+ GENERIC_COMMAND_NAME@13..17 "\\Bar"
+ WHITESPACE@17..18 " "
+ TEXT@18..21
+ WORD@18..21 "Baz"
+ R_CURLY@21..22 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_minimal_error.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_minimal_error.snap
new file mode 100644
index 0000000000..6c932ce883
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_minimal_error.snap
@@ -0,0 +1,21 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\caption{Foo \\Bar Baz\"#)"
+
+---
+ROOT@0..21
+ PREAMBLE@0..21
+ CAPTION@0..21
+ CAPTION_NAME@0..8 "\\caption"
+ CURLY_GROUP@8..21
+ L_CURLY@8..9 "{"
+ TEXT@9..13
+ WORD@9..12 "Foo"
+ WHITESPACE@12..13 " "
+ GENERIC_COMMAND@13..18
+ GENERIC_COMMAND_NAME@13..17 "\\Bar"
+ WHITESPACE@17..18 " "
+ TEXT@18..21
+ WORD@18..21 "Baz"
+ MISSING@21..21 ""
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_empty.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_empty.snap
new file mode 100644
index 0000000000..faad4c0039
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_empty.snap
@@ -0,0 +1,13 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\cite{}\"#)"
+
+---
+ROOT@0..7
+ PREAMBLE@0..7
+ CITATION@0..7
+ CITATION_NAME@0..5 "\\cite"
+ CURLY_GROUP_WORD_LIST@5..7
+ L_CURLY@5..6 "{"
+ R_CURLY@6..7 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_missing_brace.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_missing_brace.snap
new file mode 100644
index 0000000000..363f8b0142
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_missing_brace.snap
@@ -0,0 +1,14 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\cite{foo\"#)"
+
+---
+ROOT@0..9
+ PREAMBLE@0..9
+ CITATION@0..9
+ CITATION_NAME@0..5 "\\cite"
+ CURLY_GROUP_WORD_LIST@5..9
+ L_CURLY@5..6 "{"
+ WORD@6..9 "foo"
+ MISSING@9..9 ""
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_multiple_keys.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_multiple_keys.snap
new file mode 100644
index 0000000000..dafc826a97
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_multiple_keys.snap
@@ -0,0 +1,17 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\cite{foo, bar}\"#)"
+
+---
+ROOT@0..15
+ PREAMBLE@0..15
+ CITATION@0..15
+ CITATION_NAME@0..5 "\\cite"
+ CURLY_GROUP_WORD_LIST@5..15
+ L_CURLY@5..6 "{"
+ WORD@6..9 "foo"
+ COMMA@9..10 ","
+ WHITESPACE@10..11 " "
+ WORD@11..14 "bar"
+ R_CURLY@14..15 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_prenote.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_prenote.snap
new file mode 100644
index 0000000000..93d83bb448
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_prenote.snap
@@ -0,0 +1,19 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\cite[foo]{bar}\"#)"
+
+---
+ROOT@0..15
+ PREAMBLE@0..15
+ CITATION@0..15
+ CITATION_NAME@0..5 "\\cite"
+ BRACK_GROUP@5..10
+ L_BRACK@5..6 "["
+ TEXT@6..9
+ WORD@6..9 "foo"
+ R_BRACK@9..10 "]"
+ CURLY_GROUP_WORD_LIST@10..15
+ L_CURLY@10..11 "{"
+ WORD@11..14 "bar"
+ R_CURLY@14..15 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_prenote_postnote.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_prenote_postnote.snap
new file mode 100644
index 0000000000..3acaafea3b
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_prenote_postnote.snap
@@ -0,0 +1,24 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\cite[foo][bar]{baz}\"#)"
+
+---
+ROOT@0..20
+ PREAMBLE@0..20
+ CITATION@0..20
+ CITATION_NAME@0..5 "\\cite"
+ BRACK_GROUP@5..10
+ L_BRACK@5..6 "["
+ TEXT@6..9
+ WORD@6..9 "foo"
+ R_BRACK@9..10 "]"
+ BRACK_GROUP@10..15
+ L_BRACK@10..11 "["
+ TEXT@11..14
+ WORD@11..14 "bar"
+ R_BRACK@14..15 "]"
+ CURLY_GROUP_WORD_LIST@15..20
+ L_CURLY@15..16 "{"
+ WORD@16..19 "baz"
+ R_CURLY@19..20 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_redundant_comma.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_redundant_comma.snap
new file mode 100644
index 0000000000..12f34f16ae
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_redundant_comma.snap
@@ -0,0 +1,16 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\cite{,foo,}\"#)"
+
+---
+ROOT@0..12
+ PREAMBLE@0..12
+ CITATION@0..12
+ CITATION_NAME@0..5 "\\cite"
+ CURLY_GROUP_WORD_LIST@5..12
+ L_CURLY@5..6 "{"
+ COMMA@6..7 ","
+ WORD@7..10 "foo"
+ COMMA@10..11 ","
+ R_CURLY@11..12 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_simple.snap
new file mode 100644
index 0000000000..a22129f85f
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_simple.snap
@@ -0,0 +1,14 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\cite{foo}\"#)"
+
+---
+ROOT@0..10
+ PREAMBLE@0..10
+ CITATION@0..10
+ CITATION_NAME@0..5 "\\cite"
+ CURLY_GROUP_WORD_LIST@5..10
+ L_CURLY@5..6 "{"
+ WORD@6..9 "foo"
+ R_CURLY@9..10 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_star.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_star.snap
new file mode 100644
index 0000000000..4388596dfd
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_star.snap
@@ -0,0 +1,14 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\nocite{*}\"#)"
+
+---
+ROOT@0..10
+ PREAMBLE@0..10
+ CITATION@0..10
+ CITATION_NAME@0..7 "\\nocite"
+ CURLY_GROUP_WORD_LIST@7..10
+ L_CURLY@7..8 "{"
+ WORD@8..9 "*"
+ R_CURLY@9..10 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__class_include_empty.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__class_include_empty.snap
new file mode 100644
index 0000000000..7d84653225
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__class_include_empty.snap
@@ -0,0 +1,13 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\documentclass{}\"#)"
+
+---
+ROOT@0..16
+ PREAMBLE@0..16
+ CLASS_INCLUDE@0..16
+ CLASS_INCLUDE_NAME@0..14 "\\documentclass"
+ CURLY_GROUP_WORD_LIST@14..16
+ L_CURLY@14..15 "{"
+ R_CURLY@15..16 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__class_include_options.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__class_include_options.snap
new file mode 100644
index 0000000000..0d0eb1b250
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__class_include_options.snap
@@ -0,0 +1,37 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\documentclass[foo = bar, baz, qux]{article}\"#)"
+
+---
+ROOT@0..44
+ PREAMBLE@0..44
+ CLASS_INCLUDE@0..44
+ CLASS_INCLUDE_NAME@0..14 "\\documentclass"
+ BRACK_GROUP_KEY_VALUE@14..35
+ L_BRACK@14..15 "["
+ KEY_VALUE_BODY@15..34
+ KEY_VALUE_PAIR@15..24
+ KEY@15..19
+ WORD@15..18 "foo"
+ WHITESPACE@18..19 " "
+ EQUALITY_SIGN@19..20 "="
+ WHITESPACE@20..21 " "
+ VALUE@21..24
+ TEXT@21..24
+ WORD@21..24 "bar"
+ COMMA@24..25 ","
+ WHITESPACE@25..26 " "
+ KEY_VALUE_PAIR@26..29
+ KEY@26..29
+ WORD@26..29 "baz"
+ COMMA@29..30 ","
+ WHITESPACE@30..31 " "
+ KEY_VALUE_PAIR@31..34
+ KEY@31..34
+ WORD@31..34 "qux"
+ R_BRACK@34..35 "]"
+ CURLY_GROUP_WORD_LIST@35..44
+ L_CURLY@35..36 "{"
+ WORD@36..43 "article"
+ R_CURLY@43..44 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__class_include_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__class_include_simple.snap
new file mode 100644
index 0000000000..85e21b47a8
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__class_include_simple.snap
@@ -0,0 +1,14 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\documentclass{article}\"#)"
+
+---
+ROOT@0..23
+ PREAMBLE@0..23
+ CLASS_INCLUDE@0..23
+ CLASS_INCLUDE_NAME@0..14 "\\documentclass"
+ CURLY_GROUP_WORD_LIST@14..23
+ L_CURLY@14..15 "{"
+ WORD@15..22 "article"
+ R_CURLY@22..23 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_definition_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_definition_simple.snap
new file mode 100644
index 0000000000..e4adfd534c
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_definition_simple.snap
@@ -0,0 +1,27 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\definecolor{foo}{rgb}{255,168,0}\"#)"
+
+---
+ROOT@0..33
+ PREAMBLE@0..33
+ COLOR_DEFINITION@0..33
+ COLOR_DEFINITION_NAME@0..12 "\\definecolor"
+ CURLY_GROUP_WORD@12..17
+ L_CURLY@12..13 "{"
+ WORD@13..16 "foo"
+ R_CURLY@16..17 "}"
+ CURLY_GROUP_WORD@17..22
+ L_CURLY@17..18 "{"
+ WORD@18..21 "rgb"
+ R_CURLY@21..22 "}"
+ CURLY_GROUP@22..33
+ L_CURLY@22..23 "{"
+ TEXT@23..32
+ WORD@23..26 "255"
+ COMMA@26..27 ","
+ WORD@27..30 "168"
+ COMMA@30..31 ","
+ WORD@31..32 "0"
+ R_CURLY@32..33 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_reference_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_reference_simple.snap
new file mode 100644
index 0000000000..41a3700122
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_reference_simple.snap
@@ -0,0 +1,14 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\color{black}\"#)"
+
+---
+ROOT@0..13
+ PREAMBLE@0..13
+ COLOR_REFERENCE@0..13
+ COLOR_REFERENCE_NAME@0..6 "\\color"
+ CURLY_GROUP_WORD@6..13
+ L_CURLY@6..7 "{"
+ WORD@7..12 "black"
+ R_CURLY@12..13 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error1.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error1.snap
new file mode 100644
index 0000000000..42095ff974
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error1.snap
@@ -0,0 +1,29 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\definecolorset[ty]{rgb,HTML}{foo}{bar}\"#)"
+
+---
+ROOT@0..39
+ PREAMBLE@0..39
+ COLOR_SET_DEFINITION@0..39
+ COLOR_SET_DEFINITION_NAME@0..15 "\\definecolorset"
+ BRACK_GROUP_WORD@15..19
+ L_BRACK@15..16 "["
+ WORD@16..18 "ty"
+ R_BRACK@18..19 "]"
+ CURLY_GROUP_WORD_LIST@19..29
+ L_CURLY@19..20 "{"
+ WORD@20..23 "rgb"
+ COMMA@23..24 ","
+ WORD@24..28 "HTML"
+ R_CURLY@28..29 "}"
+ CURLY_GROUP_WORD@29..34
+ L_CURLY@29..30 "{"
+ WORD@30..33 "foo"
+ R_CURLY@33..34 "}"
+ CURLY_GROUP_WORD@34..39
+ L_CURLY@34..35 "{"
+ WORD@35..38 "bar"
+ R_CURLY@38..39 "}"
+ MISSING@39..39 ""
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error2.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error2.snap
new file mode 100644
index 0000000000..718d49a3e3
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error2.snap
@@ -0,0 +1,22 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\definecolorset{rgb,HTML}{foo}\"#)"
+
+---
+ROOT@0..30
+ PREAMBLE@0..30
+ COLOR_SET_DEFINITION@0..30
+ COLOR_SET_DEFINITION_NAME@0..15 "\\definecolorset"
+ CURLY_GROUP_WORD_LIST@15..25
+ L_CURLY@15..16 "{"
+ WORD@16..19 "rgb"
+ COMMA@19..20 ","
+ WORD@20..24 "HTML"
+ R_CURLY@24..25 "}"
+ CURLY_GROUP_WORD@25..30
+ L_CURLY@25..26 "{"
+ WORD@26..29 "foo"
+ R_CURLY@29..30 "}"
+ MISSING@30..30 ""
+ MISSING@30..30 ""
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error3.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error3.snap
new file mode 100644
index 0000000000..f92d813123
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error3.snap
@@ -0,0 +1,19 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\definecolorset{rgb,HTML}\"#)"
+
+---
+ROOT@0..25
+ PREAMBLE@0..25
+ COLOR_SET_DEFINITION@0..25
+ COLOR_SET_DEFINITION_NAME@0..15 "\\definecolorset"
+ CURLY_GROUP_WORD_LIST@15..25
+ L_CURLY@15..16 "{"
+ WORD@16..19 "rgb"
+ COMMA@19..20 ","
+ WORD@20..24 "HTML"
+ R_CURLY@24..25 "}"
+ MISSING@25..25 ""
+ MISSING@25..25 ""
+ MISSING@25..25 ""
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error4.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error4.snap
new file mode 100644
index 0000000000..3cafa81bae
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error4.snap
@@ -0,0 +1,14 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\definecolorset\"#)"
+
+---
+ROOT@0..15
+ PREAMBLE@0..15
+ COLOR_SET_DEFINITION@0..15
+ COLOR_SET_DEFINITION_NAME@0..15 "\\definecolorset"
+ MISSING@15..15 ""
+ MISSING@15..15 ""
+ MISSING@15..15 ""
+ MISSING@15..15 ""
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_simple.snap
new file mode 100644
index 0000000000..f9a736c65f
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_simple.snap
@@ -0,0 +1,32 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\definecolorset[ty]{rgb,HTML}{foo}{bar}{baz}\"#)"
+
+---
+ROOT@0..44
+ PREAMBLE@0..44
+ COLOR_SET_DEFINITION@0..44
+ COLOR_SET_DEFINITION_NAME@0..15 "\\definecolorset"
+ BRACK_GROUP_WORD@15..19
+ L_BRACK@15..16 "["
+ WORD@16..18 "ty"
+ R_BRACK@18..19 "]"
+ CURLY_GROUP_WORD_LIST@19..29
+ L_CURLY@19..20 "{"
+ WORD@20..23 "rgb"
+ COMMA@23..24 ","
+ WORD@24..28 "HTML"
+ R_CURLY@28..29 "}"
+ CURLY_GROUP_WORD@29..34
+ L_CURLY@29..30 "{"
+ WORD@30..33 "foo"
+ R_CURLY@33..34 "}"
+ CURLY_GROUP_WORD@34..39
+ L_CURLY@34..35 "{"
+ WORD@35..38 "bar"
+ R_CURLY@38..39 "}"
+ CURLY_GROUP_WORD@39..44
+ L_CURLY@39..40 "{"
+ WORD@40..43 "baz"
+ R_CURLY@43..44 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_no_argc.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_no_argc.snap
new file mode 100644
index 0000000000..2ff3896080
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_no_argc.snap
@@ -0,0 +1,19 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\newcommand{\\foo}{foo}\"#)"
+
+---
+ROOT@0..22
+ PREAMBLE@0..22
+ COMMAND_DEFINITION@0..22
+ COMMAND_DEFINITION_NAME@0..11 "\\newcommand"
+ CURLY_GROUP_COMMAND@11..17
+ L_CURLY@11..12 "{"
+ GENERIC_COMMAND_NAME@12..16 "\\foo"
+ R_CURLY@16..17 "}"
+ CURLY_GROUP@17..22
+ L_CURLY@17..18 "{"
+ TEXT@18..21
+ WORD@18..21 "foo"
+ R_CURLY@21..22 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_no_impl.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_no_impl.snap
new file mode 100644
index 0000000000..4602a7a688
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_no_impl.snap
@@ -0,0 +1,15 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\newcommand{\\foo}\"#)"
+
+---
+ROOT@0..17
+ PREAMBLE@0..17
+ COMMAND_DEFINITION@0..17
+ COMMAND_DEFINITION_NAME@0..11 "\\newcommand"
+ CURLY_GROUP_COMMAND@11..17
+ L_CURLY@11..12 "{"
+ GENERIC_COMMAND_NAME@12..16 "\\foo"
+ R_CURLY@16..17 "}"
+ MISSING@17..17 ""
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_no_impl_error.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_no_impl_error.snap
new file mode 100644
index 0000000000..4a20be7d7c
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_no_impl_error.snap
@@ -0,0 +1,15 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\newcommand{\\foo\"#)"
+
+---
+ROOT@0..16
+ PREAMBLE@0..16
+ COMMAND_DEFINITION@0..16
+ COMMAND_DEFINITION_NAME@0..11 "\\newcommand"
+ CURLY_GROUP_COMMAND@11..16
+ L_CURLY@11..12 "{"
+ GENERIC_COMMAND_NAME@12..16 "\\foo"
+ MISSING@16..16 ""
+ MISSING@16..16 ""
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_simple.snap
new file mode 100644
index 0000000000..e5f2377fcf
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_simple.snap
@@ -0,0 +1,24 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\newcommand[1]{\\id}{#1}\"#)"
+
+---
+ROOT@0..23
+ PREAMBLE@0..23
+ COMMAND_DEFINITION@0..19
+ COMMAND_DEFINITION_NAME@0..11 "\\newcommand"
+ MISSING@11..11 ""
+ BRACK_GROUP_WORD@11..14
+ L_BRACK@11..12 "["
+ WORD@12..13 "1"
+ R_BRACK@13..14 "]"
+ CURLY_GROUP@14..19
+ L_CURLY@14..15 "{"
+ GENERIC_COMMAND@15..18
+ GENERIC_COMMAND_NAME@15..18 "\\id"
+ R_CURLY@18..19 "}"
+ CURLY_GROUP@19..23
+ L_CURLY@19..20 "{"
+ PARAMETER@20..22 "#1"
+ R_CURLY@22..23 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__empty.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__empty.snap
new file mode 100644
index 0000000000..f01f7f9092
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__empty.snap
@@ -0,0 +1,8 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\"#)"
+
+---
+ROOT@0..0
+ PREAMBLE@0..0
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_definition.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_definition.snap
new file mode 100644
index 0000000000..df3c0d56fb
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_definition.snap
@@ -0,0 +1,38 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\newenvironment{bar}[1]{\\begin{foo}}{\\end{foo}}\"#)"
+
+---
+ROOT@0..47
+ PREAMBLE@0..47
+ ENVIRONMENT_DEFINITION@0..47
+ ENVIRONMENT_DEFINIITION_NAME@0..15 "\\newenvironment"
+ CURLY_GROUP_WORD@15..20
+ L_CURLY@15..16 "{"
+ WORD@16..19 "bar"
+ R_CURLY@19..20 "}"
+ BRACK_GROUP_WORD@20..23
+ L_BRACK@20..21 "["
+ WORD@21..22 "1"
+ R_BRACK@22..23 "]"
+ CURLY_GROUP@23..36
+ L_CURLY@23..24 "{"
+ GENERIC_COMMAND@24..35
+ BEGIN_ENVIRONMENT_NAME@24..30 "\\begin"
+ CURLY_GROUP@30..35
+ L_CURLY@30..31 "{"
+ TEXT@31..34
+ WORD@31..34 "foo"
+ R_CURLY@34..35 "}"
+ R_CURLY@35..36 "}"
+ CURLY_GROUP@36..47
+ L_CURLY@36..37 "{"
+ GENERIC_COMMAND@37..46
+ END_ENVIRONMENT_NAME@37..41 "\\end"
+ CURLY_GROUP@41..46
+ L_CURLY@41..42 "{"
+ TEXT@42..45
+ WORD@42..45 "foo"
+ R_CURLY@45..46 "}"
+ R_CURLY@46..47 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_nested.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_nested.snap
new file mode 100644
index 0000000000..a5c566f773
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_nested.snap
@@ -0,0 +1,37 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\begin{foo} \\begin{qux} \\end{baz} \\end{bar}\"#)"
+
+---
+ROOT@0..43
+ PREAMBLE@0..43
+ ENVIRONMENT@0..43
+ BEGIN@0..12
+ BEGIN_ENVIRONMENT_NAME@0..6 "\\begin"
+ CURLY_GROUP_WORD@6..12
+ L_CURLY@6..7 "{"
+ WORD@7..10 "foo"
+ R_CURLY@10..11 "}"
+ WHITESPACE@11..12 " "
+ ENVIRONMENT@12..34
+ BEGIN@12..24
+ BEGIN_ENVIRONMENT_NAME@12..18 "\\begin"
+ CURLY_GROUP_WORD@18..24
+ L_CURLY@18..19 "{"
+ WORD@19..22 "qux"
+ R_CURLY@22..23 "}"
+ WHITESPACE@23..24 " "
+ END@24..34
+ END_ENVIRONMENT_NAME@24..28 "\\end"
+ CURLY_GROUP_WORD@28..34
+ L_CURLY@28..29 "{"
+ WORD@29..32 "baz"
+ R_CURLY@32..33 "}"
+ WHITESPACE@33..34 " "
+ END@34..43
+ END_ENVIRONMENT_NAME@34..38 "\\end"
+ CURLY_GROUP_WORD@38..43
+ L_CURLY@38..39 "{"
+ WORD@39..42 "bar"
+ R_CURLY@42..43 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_nested_missing_braces.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_nested_missing_braces.snap
new file mode 100644
index 0000000000..9ff76605d6
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_nested_missing_braces.snap
@@ -0,0 +1,42 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\begin{foo \\begin{qux Hello World \\end{baz} \\end{bar\"#)"
+
+---
+ROOT@0..52
+ PREAMBLE@0..52
+ ENVIRONMENT@0..52
+ BEGIN@0..11
+ BEGIN_ENVIRONMENT_NAME@0..6 "\\begin"
+ CURLY_GROUP_WORD@6..11
+ L_CURLY@6..7 "{"
+ WORD@7..10 "foo"
+ WHITESPACE@10..11 " "
+ MISSING@11..11 ""
+ ENVIRONMENT@11..44
+ BEGIN@11..22
+ BEGIN_ENVIRONMENT_NAME@11..17 "\\begin"
+ CURLY_GROUP_WORD@17..22
+ L_CURLY@17..18 "{"
+ WORD@18..21 "qux"
+ WHITESPACE@21..22 " "
+ MISSING@22..22 ""
+ TEXT@22..34
+ WORD@22..27 "Hello"
+ WHITESPACE@27..28 " "
+ WORD@28..33 "World"
+ WHITESPACE@33..34 " "
+ END@34..44
+ END_ENVIRONMENT_NAME@34..38 "\\end"
+ CURLY_GROUP_WORD@38..44
+ L_CURLY@38..39 "{"
+ WORD@39..42 "baz"
+ R_CURLY@42..43 "}"
+ WHITESPACE@43..44 " "
+ END@44..52
+ END_ENVIRONMENT_NAME@44..48 "\\end"
+ CURLY_GROUP_WORD@48..52
+ L_CURLY@48..49 "{"
+ WORD@49..52 "bar"
+ MISSING@52..52 ""
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_simple.snap
new file mode 100644
index 0000000000..d58f5b26c4
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_simple.snap
@@ -0,0 +1,27 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\begin{foo} Hello World \\end{bar}\"#)"
+
+---
+ROOT@0..33
+ PREAMBLE@0..33
+ ENVIRONMENT@0..33
+ BEGIN@0..12
+ BEGIN_ENVIRONMENT_NAME@0..6 "\\begin"
+ CURLY_GROUP_WORD@6..12
+ L_CURLY@6..7 "{"
+ WORD@7..10 "foo"
+ R_CURLY@10..11 "}"
+ WHITESPACE@11..12 " "
+ TEXT@12..24
+ WORD@12..17 "Hello"
+ WHITESPACE@17..18 " "
+ WORD@18..23 "World"
+ WHITESPACE@23..24 " "
+ END@24..33
+ END_ENVIRONMENT_NAME@24..28 "\\end"
+ CURLY_GROUP_WORD@28..33
+ L_CURLY@28..29 "{"
+ WORD@29..32 "bar"
+ R_CURLY@32..33 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation.snap
new file mode 100644
index 0000000000..e146fcaee6
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation.snap
@@ -0,0 +1,17 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\[ foo bar \\]\"#)"
+
+---
+ROOT@0..13
+ PREAMBLE@0..13
+ EQUATION@0..13
+ BEGIN_EQUATION_NAME@0..2 "\\["
+ WHITESPACE@2..3 " "
+ TEXT@3..11
+ WORD@3..6 "foo"
+ WHITESPACE@6..7 " "
+ WORD@7..10 "bar"
+ WHITESPACE@10..11 " "
+ END_EQUATION_NAME@11..13 "\\]"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation_label_reference_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation_label_reference_simple.snap
new file mode 100644
index 0000000000..7a851233ef
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation_label_reference_simple.snap
@@ -0,0 +1,14 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\eqref{foo}\"#)"
+
+---
+ROOT@0..11
+ PREAMBLE@0..11
+ LABEL_REFERENCE@0..11
+ LABEL_REFERENCE_NAME@0..6 "\\eqref"
+ CURLY_GROUP_WORD_LIST@6..11
+ L_CURLY@6..7 "{"
+ WORD@7..10 "foo"
+ R_CURLY@10..11 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation_missing_begin.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation_missing_begin.snap
new file mode 100644
index 0000000000..fe065667c3
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation_missing_begin.snap
@@ -0,0 +1,30 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\begin{a} foo bar \\] \\end{b}\"#)"
+
+---
+ROOT@0..28
+ PREAMBLE@0..28
+ ENVIRONMENT@0..28
+ BEGIN@0..10
+ BEGIN_ENVIRONMENT_NAME@0..6 "\\begin"
+ CURLY_GROUP_WORD@6..10
+ L_CURLY@6..7 "{"
+ WORD@7..8 "a"
+ R_CURLY@8..9 "}"
+ WHITESPACE@9..10 " "
+ TEXT@10..18
+ WORD@10..13 "foo"
+ WHITESPACE@13..14 " "
+ WORD@14..17 "bar"
+ WHITESPACE@17..18 " "
+ GENERIC_COMMAND@18..21
+ END_EQUATION_NAME@18..20 "\\]"
+ WHITESPACE@20..21 " "
+ END@21..28
+ END_ENVIRONMENT_NAME@21..25 "\\end"
+ CURLY_GROUP_WORD@25..28
+ L_CURLY@25..26 "{"
+ WORD@26..27 "b"
+ R_CURLY@27..28 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation_missing_end.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation_missing_end.snap
new file mode 100644
index 0000000000..f7b6205ffb
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation_missing_end.snap
@@ -0,0 +1,31 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\begin{a} \\[ foo bar \\end{b}\"#)"
+
+---
+ROOT@0..28
+ PREAMBLE@0..28
+ ENVIRONMENT@0..28
+ BEGIN@0..10
+ BEGIN_ENVIRONMENT_NAME@0..6 "\\begin"
+ CURLY_GROUP_WORD@6..10
+ L_CURLY@6..7 "{"
+ WORD@7..8 "a"
+ R_CURLY@8..9 "}"
+ WHITESPACE@9..10 " "
+ EQUATION@10..21
+ BEGIN_EQUATION_NAME@10..12 "\\["
+ WHITESPACE@12..13 " "
+ TEXT@13..21
+ WORD@13..16 "foo"
+ WHITESPACE@16..17 " "
+ WORD@17..20 "bar"
+ WHITESPACE@20..21 " "
+ MISSING@21..21 ""
+ END@21..28
+ END_ENVIRONMENT_NAME@21..25 "\\end"
+ CURLY_GROUP_WORD@25..28
+ L_CURLY@25..26 "{"
+ WORD@26..27 "b"
+ R_CURLY@27..28 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__escaped_brackets.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__escaped_brackets.snap
new file mode 100644
index 0000000000..c595828583
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__escaped_brackets.snap
@@ -0,0 +1,19 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"{[}{]}\"#)"
+
+---
+ROOT@0..6
+ PREAMBLE@0..6
+ CURLY_GROUP@0..3
+ L_CURLY@0..1 "{"
+ MIXED_GROUP@1..2
+ L_BRACK@1..2 "["
+ MISSING@2..2 ""
+ R_CURLY@2..3 "}"
+ CURLY_GROUP@3..6
+ L_CURLY@3..4 "{"
+ ERROR@4..5
+ R_BRACK@4..5 "]"
+ R_CURLY@5..6 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__generic_command_args.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__generic_command_args.snap
new file mode 100644
index 0000000000..b62c8c2559
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__generic_command_args.snap
@@ -0,0 +1,20 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\foo{bar}[qux]\"#)"
+
+---
+ROOT@0..14
+ PREAMBLE@0..14
+ GENERIC_COMMAND@0..14
+ GENERIC_COMMAND_NAME@0..4 "\\foo"
+ CURLY_GROUP@4..9
+ L_CURLY@4..5 "{"
+ TEXT@5..8
+ WORD@5..8 "bar"
+ R_CURLY@8..9 "}"
+ MIXED_GROUP@9..14
+ L_BRACK@9..10 "["
+ TEXT@10..13
+ WORD@10..13 "qux"
+ R_BRACK@13..14 "]"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__generic_command_empty.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__generic_command_empty.snap
new file mode 100644
index 0000000000..84e62ea81d
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__generic_command_empty.snap
@@ -0,0 +1,10 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\foo\"#)"
+
+---
+ROOT@0..4
+ PREAMBLE@0..4
+ GENERIC_COMMAND@0..4
+ GENERIC_COMMAND_NAME@0..4 "\\foo"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__generic_command_escape.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__generic_command_escape.snap
new file mode 100644
index 0000000000..a0d5d2e29b
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__generic_command_escape.snap
@@ -0,0 +1,10 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\#\"#)"
+
+---
+ROOT@0..2
+ PREAMBLE@0..2
+ GENERIC_COMMAND@0..2
+ GENERIC_COMMAND_NAME@0..2 "\\#"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__glossary_entry_definition_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__glossary_entry_definition_simple.snap
new file mode 100644
index 0000000000..d1b31e275a
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__glossary_entry_definition_simple.snap
@@ -0,0 +1,33 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\newglossaryentry{foo}{bar = baz, qux,}\"#)"
+
+---
+ROOT@0..39
+ PREAMBLE@0..39
+ GLOSSARY_ENTRY_DEFINITION@0..39
+ GLOSSARY_ENTRY_DEFINITION_NAME@0..17 "\\newglossaryentry"
+ CURLY_GROUP_WORD@17..22
+ L_CURLY@17..18 "{"
+ WORD@18..21 "foo"
+ R_CURLY@21..22 "}"
+ CURLY_GROUP_KEY_VALUE@22..39
+ L_CURLY@22..23 "{"
+ KEY_VALUE_BODY@23..38
+ KEY_VALUE_PAIR@23..32
+ KEY@23..27
+ WORD@23..26 "bar"
+ WHITESPACE@26..27 " "
+ EQUALITY_SIGN@27..28 "="
+ WHITESPACE@28..29 " "
+ VALUE@29..32
+ TEXT@29..32
+ WORD@29..32 "baz"
+ COMMA@32..33 ","
+ WHITESPACE@33..34 " "
+ KEY_VALUE_PAIR@34..37
+ KEY@34..37
+ WORD@34..37 "qux"
+ COMMA@37..38 ","
+ R_CURLY@38..39 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__glossary_entry_reference_options.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__glossary_entry_reference_options.snap
new file mode 100644
index 0000000000..ad5c44885a
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__glossary_entry_reference_options.snap
@@ -0,0 +1,32 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\gls[foo = bar, qux]{baz}\"#)"
+
+---
+ROOT@0..25
+ PREAMBLE@0..25
+ GLOSSARY_ENTRY_REFERENCE@0..25
+ GLOSSARY_ENTRY_REFERENCE_NAME@0..4 "\\gls"
+ BRACK_GROUP_KEY_VALUE@4..20
+ L_BRACK@4..5 "["
+ KEY_VALUE_BODY@5..19
+ KEY_VALUE_PAIR@5..14
+ KEY@5..9
+ WORD@5..8 "foo"
+ WHITESPACE@8..9 " "
+ EQUALITY_SIGN@9..10 "="
+ WHITESPACE@10..11 " "
+ VALUE@11..14
+ TEXT@11..14
+ WORD@11..14 "bar"
+ COMMA@14..15 ","
+ WHITESPACE@15..16 " "
+ KEY_VALUE_PAIR@16..19
+ KEY@16..19
+ WORD@16..19 "qux"
+ R_BRACK@19..20 "]"
+ CURLY_GROUP_WORD@20..25
+ L_CURLY@20..21 "{"
+ WORD@21..24 "baz"
+ R_CURLY@24..25 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__glossary_entry_reference_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__glossary_entry_reference_simple.snap
new file mode 100644
index 0000000000..2a6b845733
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__glossary_entry_reference_simple.snap
@@ -0,0 +1,14 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\gls{foo}\"#)"
+
+---
+ROOT@0..9
+ PREAMBLE@0..9
+ GLOSSARY_ENTRY_REFERENCE@0..9
+ GLOSSARY_ENTRY_REFERENCE_NAME@0..4 "\\gls"
+ CURLY_GROUP_WORD@4..9
+ L_CURLY@4..5 "{"
+ WORD@5..8 "foo"
+ R_CURLY@8..9 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__graphics_include_options.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__graphics_include_options.snap
new file mode 100644
index 0000000000..910a4848ef
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__graphics_include_options.snap
@@ -0,0 +1,25 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\includegraphics[scale=.5]{foo/bar.pdf}\"#)"
+
+---
+ROOT@0..39
+ PREAMBLE@0..39
+ GRAPHICS_INCLUDE@0..39
+ GRAPHICS_INCLUDE_NAME@0..16 "\\includegraphics"
+ BRACK_GROUP_KEY_VALUE@16..26
+ L_BRACK@16..17 "["
+ KEY_VALUE_BODY@17..25
+ KEY_VALUE_PAIR@17..25
+ KEY@17..22
+ WORD@17..22 "scale"
+ EQUALITY_SIGN@22..23 "="
+ VALUE@23..25
+ TEXT@23..25
+ WORD@23..25 ".5"
+ R_BRACK@25..26 "]"
+ CURLY_GROUP_WORD_LIST@26..39
+ L_CURLY@26..27 "{"
+ WORD@27..38 "foo/bar.pdf"
+ R_CURLY@38..39 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__graphics_include_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__graphics_include_simple.snap
new file mode 100644
index 0000000000..fa6fbf1a14
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__graphics_include_simple.snap
@@ -0,0 +1,14 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\includegraphics{foo/bar.pdf}\"#)"
+
+---
+ROOT@0..29
+ PREAMBLE@0..29
+ GRAPHICS_INCLUDE@0..29
+ GRAPHICS_INCLUDE_NAME@0..16 "\\includegraphics"
+ CURLY_GROUP_WORD_LIST@16..29
+ L_CURLY@16..17 "{"
+ WORD@17..28 "foo/bar.pdf"
+ R_CURLY@28..29 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__hello_world.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__hello_world.snap
new file mode 100644
index 0000000000..ee25cb5453
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__hello_world.snap
@@ -0,0 +1,12 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"Hello World!\"#)"
+
+---
+ROOT@0..12
+ PREAMBLE@0..12
+ TEXT@0..12
+ WORD@0..5 "Hello"
+ WHITESPACE@5..6 " "
+ WORD@6..12 "World!"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__import_incomplete.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__import_incomplete.snap
new file mode 100644
index 0000000000..3dae1f433a
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__import_incomplete.snap
@@ -0,0 +1,15 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\import{foo\"#)"
+
+---
+ROOT@0..11
+ PREAMBLE@0..11
+ IMPORT@0..11
+ IMPORT_NAME@0..7 "\\import"
+ CURLY_GROUP_WORD@7..11
+ L_CURLY@7..8 "{"
+ WORD@8..11 "foo"
+ MISSING@11..11 ""
+ MISSING@11..11 ""
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__import_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__import_simple.snap
new file mode 100644
index 0000000000..6e21f65cbf
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__import_simple.snap
@@ -0,0 +1,18 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\import{foo}{bar}\"#)"
+
+---
+ROOT@0..17
+ PREAMBLE@0..17
+ IMPORT@0..17
+ IMPORT_NAME@0..7 "\\import"
+ CURLY_GROUP_WORD@7..12
+ L_CURLY@7..8 "{"
+ WORD@8..11 "foo"
+ R_CURLY@11..12 "}"
+ CURLY_GROUP_WORD@12..17
+ L_CURLY@12..13 "{"
+ WORD@13..16 "bar"
+ R_CURLY@16..17 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inkscape_include_options.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inkscape_include_options.snap
new file mode 100644
index 0000000000..d1fd2df652
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inkscape_include_options.snap
@@ -0,0 +1,25 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\includesvg[scale=.5]{foo/bar}\"#)"
+
+---
+ROOT@0..30
+ PREAMBLE@0..30
+ SVG_INCLUDE@0..30
+ SVG_INCLUDE_NAME@0..11 "\\includesvg"
+ BRACK_GROUP_KEY_VALUE@11..21
+ L_BRACK@11..12 "["
+ KEY_VALUE_BODY@12..20
+ KEY_VALUE_PAIR@12..20
+ KEY@12..17
+ WORD@12..17 "scale"
+ EQUALITY_SIGN@17..18 "="
+ VALUE@18..20
+ TEXT@18..20
+ WORD@18..20 ".5"
+ R_BRACK@20..21 "]"
+ CURLY_GROUP_WORD_LIST@21..30
+ L_CURLY@21..22 "{"
+ WORD@22..29 "foo/bar"
+ R_CURLY@29..30 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inkscape_include_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inkscape_include_simple.snap
new file mode 100644
index 0000000000..59f50c36d3
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inkscape_include_simple.snap
@@ -0,0 +1,14 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\includesvg{foo/bar}\"#)"
+
+---
+ROOT@0..20
+ PREAMBLE@0..20
+ SVG_INCLUDE@0..20
+ SVG_INCLUDE_NAME@0..11 "\\includesvg"
+ CURLY_GROUP_WORD_LIST@11..20
+ L_CURLY@11..12 "{"
+ WORD@12..19 "foo/bar"
+ R_CURLY@19..20 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inline.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inline.snap
new file mode 100644
index 0000000000..b430de766e
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inline.snap
@@ -0,0 +1,26 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"$x \\in [0, \\infty)$\"#)"
+
+---
+ROOT@0..19
+ PREAMBLE@0..19
+ FORMULA@0..19
+ DOLLAR@0..1 "$"
+ TEXT@1..3
+ WORD@1..2 "x"
+ WHITESPACE@2..3 " "
+ GENERIC_COMMAND@3..18
+ GENERIC_COMMAND_NAME@3..6 "\\in"
+ WHITESPACE@6..7 " "
+ MIXED_GROUP@7..18
+ L_BRACK@7..8 "["
+ TEXT@8..11
+ WORD@8..9 "0"
+ COMMA@9..10 ","
+ WHITESPACE@10..11 " "
+ GENERIC_COMMAND@11..17
+ GENERIC_COMMAND_NAME@11..17 "\\infty"
+ R_PAREN@17..18 ")"
+ DOLLAR@18..19 "$"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inline_double_dollar.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inline_double_dollar.snap
new file mode 100644
index 0000000000..8da22f3ecb
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inline_double_dollar.snap
@@ -0,0 +1,26 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"$$x \\in [0, \\infty)$$\"#)"
+
+---
+ROOT@0..21
+ PREAMBLE@0..21
+ FORMULA@0..21
+ DOLLAR@0..2 "$$"
+ TEXT@2..4
+ WORD@2..3 "x"
+ WHITESPACE@3..4 " "
+ GENERIC_COMMAND@4..19
+ GENERIC_COMMAND_NAME@4..7 "\\in"
+ WHITESPACE@7..8 " "
+ MIXED_GROUP@8..19
+ L_BRACK@8..9 "["
+ TEXT@9..12
+ WORD@9..10 "0"
+ COMMA@10..11 ","
+ WHITESPACE@11..12 " "
+ GENERIC_COMMAND@12..18
+ GENERIC_COMMAND_NAME@12..18 "\\infty"
+ R_PAREN@18..19 ")"
+ DOLLAR@19..21 "$$"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_definition_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_definition_simple.snap
new file mode 100644
index 0000000000..7eacd65c59
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_definition_simple.snap
@@ -0,0 +1,14 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\label{foo}\"#)"
+
+---
+ROOT@0..11
+ PREAMBLE@0..11
+ LABEL_DEFINITION@0..11
+ LABEL_DEFINITION_NAME@0..6 "\\label"
+ CURLY_GROUP_WORD@6..11
+ L_CURLY@6..7 "{"
+ WORD@7..10 "foo"
+ R_CURLY@10..11 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_number.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_number.snap
new file mode 100644
index 0000000000..46392d7574
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_number.snap
@@ -0,0 +1,23 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\newlabel{foo}{{1.1}}\"#)"
+
+---
+ROOT@0..21
+ PREAMBLE@0..21
+ LABEL_NUMBER@0..21
+ LABEL_NUMBER_NAME@0..9 "\\newlabel"
+ CURLY_GROUP_WORD@9..14
+ L_CURLY@9..10 "{"
+ WORD@10..13 "foo"
+ R_CURLY@13..14 "}"
+ CURLY_GROUP@14..21
+ L_CURLY@14..15 "{"
+ CURLY_GROUP@15..20
+ L_CURLY@15..16 "{"
+ TEXT@16..19
+ WORD@16..19 "1.1"
+ R_CURLY@19..20 "}"
+ R_CURLY@20..21 "}"
+ MISSING@21..21 ""
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_multiple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_multiple.snap
new file mode 100644
index 0000000000..e399397e09
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_multiple.snap
@@ -0,0 +1,17 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\ref{foo, bar}\"#)"
+
+---
+ROOT@0..14
+ PREAMBLE@0..14
+ LABEL_REFERENCE@0..14
+ LABEL_REFERENCE_NAME@0..4 "\\ref"
+ CURLY_GROUP_WORD_LIST@4..14
+ L_CURLY@4..5 "{"
+ WORD@5..8 "foo"
+ COMMA@8..9 ","
+ WHITESPACE@9..10 " "
+ WORD@10..13 "bar"
+ R_CURLY@13..14 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_range_error.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_range_error.snap
new file mode 100644
index 0000000000..2894a9c2ec
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_range_error.snap
@@ -0,0 +1,18 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\crefrange{foo{bar}\"#)"
+
+---
+ROOT@0..19
+ PREAMBLE@0..19
+ LABEL_REFERENCE_RANGE@0..19
+ LABEL_REFERENCE_RANGE_NAME@0..10 "\\crefrange"
+ CURLY_GROUP_WORD@10..14
+ L_CURLY@10..11 "{"
+ WORD@11..14 "foo"
+ MISSING@14..14 ""
+ CURLY_GROUP_WORD@14..19
+ L_CURLY@14..15 "{"
+ WORD@15..18 "bar"
+ R_CURLY@18..19 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_range_incomplete.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_range_incomplete.snap
new file mode 100644
index 0000000000..0a1753867e
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_range_incomplete.snap
@@ -0,0 +1,15 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\crefrange{foo}\"#)"
+
+---
+ROOT@0..15
+ PREAMBLE@0..15
+ LABEL_REFERENCE_RANGE@0..15
+ LABEL_REFERENCE_RANGE_NAME@0..10 "\\crefrange"
+ CURLY_GROUP_WORD@10..15
+ L_CURLY@10..11 "{"
+ WORD@11..14 "foo"
+ R_CURLY@14..15 "}"
+ MISSING@15..15 ""
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_range_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_range_simple.snap
new file mode 100644
index 0000000000..c06c31c1b7
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_range_simple.snap
@@ -0,0 +1,18 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\crefrange{foo}{bar}\"#)"
+
+---
+ROOT@0..20
+ PREAMBLE@0..20
+ LABEL_REFERENCE_RANGE@0..20
+ LABEL_REFERENCE_RANGE_NAME@0..10 "\\crefrange"
+ CURLY_GROUP_WORD@10..15
+ L_CURLY@10..11 "{"
+ WORD@11..14 "foo"
+ R_CURLY@14..15 "}"
+ CURLY_GROUP_WORD@15..20
+ L_CURLY@15..16 "{"
+ WORD@16..19 "bar"
+ R_CURLY@19..20 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_simple.snap
new file mode 100644
index 0000000000..ed06f5dfc1
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_simple.snap
@@ -0,0 +1,14 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\ref{foo}\"#)"
+
+---
+ROOT@0..9
+ PREAMBLE@0..9
+ LABEL_REFERENCE@0..9
+ LABEL_REFERENCE_NAME@0..4 "\\ref"
+ CURLY_GROUP_WORD_LIST@4..9
+ L_CURLY@4..5 "{"
+ WORD@5..8 "foo"
+ R_CURLY@8..9 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__latex_include_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__latex_include_simple.snap
new file mode 100644
index 0000000000..34565aa072
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__latex_include_simple.snap
@@ -0,0 +1,14 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\include{foo/bar}\"#)"
+
+---
+ROOT@0..17
+ PREAMBLE@0..17
+ LATEX_INCLUDE@0..17
+ LATEX_INCLUDE_NAME@0..8 "\\include"
+ CURLY_GROUP_WORD_LIST@8..17
+ L_CURLY@8..9 "{"
+ WORD@9..16 "foo/bar"
+ R_CURLY@16..17 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__latex_input_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__latex_input_simple.snap
new file mode 100644
index 0000000000..208e898407
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__latex_input_simple.snap
@@ -0,0 +1,14 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\input{foo/bar.tex}\"#)"
+
+---
+ROOT@0..19
+ PREAMBLE@0..19
+ LATEX_INCLUDE@0..19
+ LATEX_INCLUDE_NAME@0..6 "\\input"
+ CURLY_GROUP_WORD_LIST@6..19
+ L_CURLY@6..7 "{"
+ WORD@7..18 "foo/bar.tex"
+ R_CURLY@18..19 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__math_operator_no_impl.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__math_operator_no_impl.snap
new file mode 100644
index 0000000000..2e39ee899c
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__math_operator_no_impl.snap
@@ -0,0 +1,15 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\DeclareMathOperator{\\foo}\"#)"
+
+---
+ROOT@0..26
+ PREAMBLE@0..26
+ MATH_OPERATOR@0..26
+ MATH_OPERATOR_NAME@0..20 "\\DeclareMathOperator"
+ CURLY_GROUP_COMMAND@20..26
+ L_CURLY@20..21 "{"
+ GENERIC_COMMAND_NAME@21..25 "\\foo"
+ R_CURLY@25..26 "}"
+ MISSING@26..26 ""
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__math_operator_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__math_operator_simple.snap
new file mode 100644
index 0000000000..bb44005153
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__math_operator_simple.snap
@@ -0,0 +1,19 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\DeclareMathOperator{\\foo}{foo}\"#)"
+
+---
+ROOT@0..31
+ PREAMBLE@0..31
+ MATH_OPERATOR@0..31
+ MATH_OPERATOR_NAME@0..20 "\\DeclareMathOperator"
+ CURLY_GROUP_COMMAND@20..26
+ L_CURLY@20..21 "{"
+ GENERIC_COMMAND_NAME@21..25 "\\foo"
+ R_CURLY@25..26 "}"
+ CURLY_GROUP@26..31
+ L_CURLY@26..27 "{"
+ TEXT@27..30
+ WORD@27..30 "foo"
+ R_CURLY@30..31 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_empty.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_empty.snap
new file mode 100644
index 0000000000..b8851b6e1d
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_empty.snap
@@ -0,0 +1,13 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\usepackage{}\"#)"
+
+---
+ROOT@0..13
+ PREAMBLE@0..13
+ PACKAGE_INCLUDE@0..13
+ PACKAGE_INCLUDE_NAME@0..11 "\\usepackage"
+ CURLY_GROUP_WORD_LIST@11..13
+ L_CURLY@11..12 "{"
+ R_CURLY@12..13 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_multiple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_multiple.snap
new file mode 100644
index 0000000000..e68b98d1b5
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_multiple.snap
@@ -0,0 +1,17 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\usepackage{amsmath, lipsum}\"#)"
+
+---
+ROOT@0..28
+ PREAMBLE@0..28
+ PACKAGE_INCLUDE@0..28
+ PACKAGE_INCLUDE_NAME@0..11 "\\usepackage"
+ CURLY_GROUP_WORD_LIST@11..28
+ L_CURLY@11..12 "{"
+ WORD@12..19 "amsmath"
+ COMMA@19..20 ","
+ WHITESPACE@20..21 " "
+ WORD@21..27 "lipsum"
+ R_CURLY@27..28 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_options.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_options.snap
new file mode 100644
index 0000000000..a33c4e0bd7
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_options.snap
@@ -0,0 +1,37 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\usepackage[foo = bar, baz, qux]{amsmath}\"#)"
+
+---
+ROOT@0..41
+ PREAMBLE@0..41
+ PACKAGE_INCLUDE@0..41
+ PACKAGE_INCLUDE_NAME@0..11 "\\usepackage"
+ BRACK_GROUP_KEY_VALUE@11..32
+ L_BRACK@11..12 "["
+ KEY_VALUE_BODY@12..31
+ KEY_VALUE_PAIR@12..21
+ KEY@12..16
+ WORD@12..15 "foo"
+ WHITESPACE@15..16 " "
+ EQUALITY_SIGN@16..17 "="
+ WHITESPACE@17..18 " "
+ VALUE@18..21
+ TEXT@18..21
+ WORD@18..21 "bar"
+ COMMA@21..22 ","
+ WHITESPACE@22..23 " "
+ KEY_VALUE_PAIR@23..26
+ KEY@23..26
+ WORD@23..26 "baz"
+ COMMA@26..27 ","
+ WHITESPACE@27..28 " "
+ KEY_VALUE_PAIR@28..31
+ KEY@28..31
+ WORD@28..31 "qux"
+ R_BRACK@31..32 "]"
+ CURLY_GROUP_WORD_LIST@32..41
+ L_CURLY@32..33 "{"
+ WORD@33..40 "amsmath"
+ R_CURLY@40..41 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_simple.snap
new file mode 100644
index 0000000000..00c075fdfe
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_simple.snap
@@ -0,0 +1,14 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\usepackage{amsmath}\"#)"
+
+---
+ROOT@0..20
+ PREAMBLE@0..20
+ PACKAGE_INCLUDE@0..20
+ PACKAGE_INCLUDE_NAME@0..11 "\\usepackage"
+ CURLY_GROUP_WORD_LIST@11..20
+ L_CURLY@11..12 "{"
+ WORD@12..19 "amsmath"
+ R_CURLY@19..20 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__parameter.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__parameter.snap
new file mode 100644
index 0000000000..8c6bd90cb8
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__parameter.snap
@@ -0,0 +1,9 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"#1\"#)"
+
+---
+ROOT@0..2
+ PREAMBLE@0..2
+ PARAMETER@0..2 "#1"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__parameter_error.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__parameter_error.snap
new file mode 100644
index 0000000000..1a9b68fa2d
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__parameter_error.snap
@@ -0,0 +1,9 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"#\"#)"
+
+---
+ROOT@0..1
+ PREAMBLE@0..1
+ PARAMETER@0..1 "#"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__pgf_library_import_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__pgf_library_import_simple.snap
new file mode 100644
index 0000000000..8f289f97a4
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__pgf_library_import_simple.snap
@@ -0,0 +1,14 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\usepgflibrary{foo}\"#)"
+
+---
+ROOT@0..19
+ PREAMBLE@0..19
+ TIKZ_LIBRARY_IMPORT@0..19
+ TIKZ_LIBRARY_IMPORT_NAME@0..14 "\\usepgflibrary"
+ CURLY_GROUP_WORD_LIST@14..19
+ L_CURLY@14..15 "{"
+ WORD@15..18 "foo"
+ R_CURLY@18..19 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_enum_item.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_enum_item.snap
new file mode 100644
index 0000000000..43dcae1987
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_enum_item.snap
@@ -0,0 +1,45 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\begin{enumerate} \\item 1 \\item[2] 2 \\item 3 \\end{enumerate}\"#)"
+
+---
+ROOT@0..60
+ PREAMBLE@0..60
+ ENVIRONMENT@0..60
+ BEGIN@0..18
+ BEGIN_ENVIRONMENT_NAME@0..6 "\\begin"
+ CURLY_GROUP_WORD@6..18
+ L_CURLY@6..7 "{"
+ WORD@7..16 "enumerate"
+ R_CURLY@16..17 "}"
+ WHITESPACE@17..18 " "
+ ENUM_ITEM@18..26
+ ENUM_ITEM_NAME@18..23 "\\item"
+ WHITESPACE@23..24 " "
+ TEXT@24..26
+ WORD@24..25 "1"
+ WHITESPACE@25..26 " "
+ ENUM_ITEM@26..37
+ ENUM_ITEM_NAME@26..31 "\\item"
+ BRACK_GROUP@31..35
+ L_BRACK@31..32 "["
+ TEXT@32..33
+ WORD@32..33 "2"
+ R_BRACK@33..34 "]"
+ WHITESPACE@34..35 " "
+ TEXT@35..37
+ WORD@35..36 "2"
+ WHITESPACE@36..37 " "
+ ENUM_ITEM@37..45
+ ENUM_ITEM_NAME@37..42 "\\item"
+ WHITESPACE@42..43 " "
+ TEXT@43..45
+ WORD@43..44 "3"
+ WHITESPACE@44..45 " "
+ END@45..60
+ END_ENVIRONMENT_NAME@45..49 "\\end"
+ CURLY_GROUP_WORD@49..60
+ L_CURLY@49..50 "{"
+ WORD@50..59 "enumerate"
+ R_CURLY@59..60 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_invalid_nesting.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_invalid_nesting.snap
new file mode 100644
index 0000000000..f999a50e9a
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_invalid_nesting.snap
@@ -0,0 +1,23 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\section{Foo} \\chapter{Bar}\"#)"
+
+---
+ROOT@0..27
+ PREAMBLE@0..27
+ SECTION@0..14
+ SECTION_NAME@0..8 "\\section"
+ CURLY_GROUP@8..14
+ L_CURLY@8..9 "{"
+ TEXT@9..12
+ WORD@9..12 "Foo"
+ R_CURLY@12..13 "}"
+ WHITESPACE@13..14 " "
+ CHAPTER@14..27
+ CHAPTER_NAME@14..22 "\\chapter"
+ CURLY_GROUP@22..27
+ L_CURLY@22..23 "{"
+ TEXT@23..26
+ WORD@23..26 "Bar"
+ R_CURLY@26..27 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_nested.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_nested.snap
new file mode 100644
index 0000000000..db5602e591
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_nested.snap
@@ -0,0 +1,57 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\part{1}\\chapter{2}\\section{3}\\subsection{4}\\subsubsection{5}\\paragraph{6}\\subparagraph{7}\"#)"
+
+---
+ROOT@0..90
+ PREAMBLE@0..90
+ PART@0..90
+ PART_NAME@0..5 "\\part"
+ CURLY_GROUP@5..8
+ L_CURLY@5..6 "{"
+ TEXT@6..7
+ WORD@6..7 "1"
+ R_CURLY@7..8 "}"
+ CHAPTER@8..90
+ CHAPTER_NAME@8..16 "\\chapter"
+ CURLY_GROUP@16..19
+ L_CURLY@16..17 "{"
+ TEXT@17..18
+ WORD@17..18 "2"
+ R_CURLY@18..19 "}"
+ SECTION@19..90
+ SECTION_NAME@19..27 "\\section"
+ CURLY_GROUP@27..30
+ L_CURLY@27..28 "{"
+ TEXT@28..29
+ WORD@28..29 "3"
+ R_CURLY@29..30 "}"
+ SUBSECTION@30..90
+ SUBSECTION_NAME@30..41 "\\subsection"
+ CURLY_GROUP@41..44
+ L_CURLY@41..42 "{"
+ TEXT@42..43
+ WORD@42..43 "4"
+ R_CURLY@43..44 "}"
+ SUBSUBSECTION@44..90
+ SUBSUBSECTION_NAME@44..58 "\\subsubsection"
+ CURLY_GROUP@58..61
+ L_CURLY@58..59 "{"
+ TEXT@59..60
+ WORD@59..60 "5"
+ R_CURLY@60..61 "}"
+ PARAGRAPH@61..90
+ PARAGRAPH_NAME@61..71 "\\paragraph"
+ CURLY_GROUP@71..74
+ L_CURLY@71..72 "{"
+ TEXT@72..73
+ WORD@72..73 "6"
+ R_CURLY@73..74 "}"
+ SUBPARAGRAPH@74..90
+ SUBPARAGRAPH_NAME@74..87 "\\subparagraph"
+ CURLY_GROUP@87..90
+ L_CURLY@87..88 "{"
+ TEXT@88..89
+ WORD@88..89 "7"
+ R_CURLY@89..90 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_siblings.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_siblings.snap
new file mode 100644
index 0000000000..94d2ef40bd
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_siblings.snap
@@ -0,0 +1,29 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\section{Foo} Foo \\section{Bar} Bar\"#)"
+
+---
+ROOT@0..35
+ PREAMBLE@0..35
+ SECTION@0..18
+ SECTION_NAME@0..8 "\\section"
+ CURLY_GROUP@8..14
+ L_CURLY@8..9 "{"
+ TEXT@9..12
+ WORD@9..12 "Foo"
+ R_CURLY@12..13 "}"
+ WHITESPACE@13..14 " "
+ TEXT@14..18
+ WORD@14..17 "Foo"
+ WHITESPACE@17..18 " "
+ SECTION@18..35
+ SECTION_NAME@18..26 "\\section"
+ CURLY_GROUP@26..32
+ L_CURLY@26..27 "{"
+ TEXT@27..30
+ WORD@27..30 "Bar"
+ R_CURLY@30..31 "}"
+ WHITESPACE@31..32 " "
+ TEXT@32..35
+ WORD@32..35 "Bar"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__svg_include_options.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__svg_include_options.snap
new file mode 100644
index 0000000000..863f27cf56
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__svg_include_options.snap
@@ -0,0 +1,25 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\includesvg[scale=.5]{foo/bar.svg}\"#)"
+
+---
+ROOT@0..34
+ PREAMBLE@0..34
+ SVG_INCLUDE@0..34
+ SVG_INCLUDE_NAME@0..11 "\\includesvg"
+ BRACK_GROUP_KEY_VALUE@11..21
+ L_BRACK@11..12 "["
+ KEY_VALUE_BODY@12..20
+ KEY_VALUE_PAIR@12..20
+ KEY@12..17
+ WORD@12..17 "scale"
+ EQUALITY_SIGN@17..18 "="
+ VALUE@18..20
+ TEXT@18..20
+ WORD@18..20 ".5"
+ R_BRACK@20..21 "]"
+ CURLY_GROUP_WORD_LIST@21..34
+ L_CURLY@21..22 "{"
+ WORD@22..33 "foo/bar.svg"
+ R_CURLY@33..34 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__svg_include_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__svg_include_simple.snap
new file mode 100644
index 0000000000..02db942d99
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__svg_include_simple.snap
@@ -0,0 +1,14 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\includesvg{foo/bar.svg}\"#)"
+
+---
+ROOT@0..24
+ PREAMBLE@0..24
+ SVG_INCLUDE@0..24
+ SVG_INCLUDE_NAME@0..11 "\\includesvg"
+ CURLY_GROUP_WORD_LIST@11..24
+ L_CURLY@11..12 "{"
+ WORD@12..23 "foo/bar.svg"
+ R_CURLY@23..24 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_full.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_full.snap
new file mode 100644
index 0000000000..d8fe3816ad
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_full.snap
@@ -0,0 +1,27 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\newtheorem{foo}[bar]{Foo}[baz]\"#)"
+
+---
+ROOT@0..31
+ PREAMBLE@0..31
+ THEOREM_DEFINITION@0..31
+ THEOREM_DEFINITION_NAME@0..11 "\\newtheorem"
+ CURLY_GROUP_WORD@11..16
+ L_CURLY@11..12 "{"
+ WORD@12..15 "foo"
+ R_CURLY@15..16 "}"
+ BRACK_GROUP_WORD@16..21
+ L_BRACK@16..17 "["
+ WORD@17..20 "bar"
+ R_BRACK@20..21 "]"
+ CURLY_GROUP@21..26
+ L_CURLY@21..22 "{"
+ TEXT@22..25
+ WORD@22..25 "Foo"
+ R_CURLY@25..26 "}"
+ BRACK_GROUP_WORD@26..31
+ L_BRACK@26..27 "["
+ WORD@27..30 "baz"
+ R_BRACK@30..31 "]"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_name_with_counter.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_name_with_counter.snap
new file mode 100644
index 0000000000..9e550b19a4
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_name_with_counter.snap
@@ -0,0 +1,19 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\newtheorem{foo}[bar]\"#)"
+
+---
+ROOT@0..21
+ PREAMBLE@0..21
+ THEOREM_DEFINITION@0..21
+ THEOREM_DEFINITION_NAME@0..11 "\\newtheorem"
+ CURLY_GROUP_WORD@11..16
+ L_CURLY@11..12 "{"
+ WORD@12..15 "foo"
+ R_CURLY@15..16 "}"
+ BRACK_GROUP_WORD@16..21
+ L_BRACK@16..17 "["
+ WORD@17..20 "bar"
+ R_BRACK@20..21 "]"
+ MISSING@21..21 ""
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_name_with_description.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_name_with_description.snap
new file mode 100644
index 0000000000..356dc079f1
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_name_with_description.snap
@@ -0,0 +1,19 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\newtheorem{foo}{Foo}\"#)"
+
+---
+ROOT@0..21
+ PREAMBLE@0..21
+ THEOREM_DEFINITION@0..21
+ THEOREM_DEFINITION_NAME@0..11 "\\newtheorem"
+ CURLY_GROUP_WORD@11..16
+ L_CURLY@11..12 "{"
+ WORD@12..15 "foo"
+ R_CURLY@15..16 "}"
+ CURLY_GROUP@16..21
+ L_CURLY@16..17 "{"
+ TEXT@17..20
+ WORD@17..20 "Foo"
+ R_CURLY@20..21 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_name_with_description_and_counter.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_name_with_description_and_counter.snap
new file mode 100644
index 0000000000..cc6e34d378
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_name_with_description_and_counter.snap
@@ -0,0 +1,23 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\newtheorem{foo}[bar]{Foo}\"#)"
+
+---
+ROOT@0..26
+ PREAMBLE@0..26
+ THEOREM_DEFINITION@0..26
+ THEOREM_DEFINITION_NAME@0..11 "\\newtheorem"
+ CURLY_GROUP_WORD@11..16
+ L_CURLY@11..12 "{"
+ WORD@12..15 "foo"
+ R_CURLY@15..16 "}"
+ BRACK_GROUP_WORD@16..21
+ L_BRACK@16..17 "["
+ WORD@17..20 "bar"
+ R_BRACK@20..21 "]"
+ CURLY_GROUP@21..26
+ L_CURLY@21..22 "{"
+ TEXT@22..25
+ WORD@22..25 "Foo"
+ R_CURLY@25..26 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_only_name.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_only_name.snap
new file mode 100644
index 0000000000..27a040b7ff
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_only_name.snap
@@ -0,0 +1,15 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\newtheorem{foo}\"#)"
+
+---
+ROOT@0..16
+ PREAMBLE@0..16
+ THEOREM_DEFINITION@0..16
+ THEOREM_DEFINITION_NAME@0..11 "\\newtheorem"
+ CURLY_GROUP_WORD@11..16
+ L_CURLY@11..12 "{"
+ WORD@12..15 "foo"
+ R_CURLY@15..16 "}"
+ MISSING@16..16 ""
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__tikz_library_import_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__tikz_library_import_simple.snap
new file mode 100644
index 0000000000..4915306c55
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__tikz_library_import_simple.snap
@@ -0,0 +1,14 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\usetikzlibrary{foo}\"#)"
+
+---
+ROOT@0..20
+ PREAMBLE@0..20
+ TIKZ_LIBRARY_IMPORT@0..20
+ TIKZ_LIBRARY_IMPORT_NAME@0..15 "\\usetikzlibrary"
+ CURLY_GROUP_WORD_LIST@15..20
+ L_CURLY@15..16 "{"
+ WORD@16..19 "foo"
+ R_CURLY@19..20 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__unmatched_braces.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__unmatched_braces.snap
new file mode 100644
index 0000000000..df463f501a
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__unmatched_braces.snap
@@ -0,0 +1,13 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"}{\"#)"
+
+---
+ROOT@0..2
+ PREAMBLE@0..2
+ ERROR@0..1
+ R_CURLY@0..1 "}"
+ CURLY_GROUP@1..2
+ L_CURLY@1..2 "{"
+ MISSING@2..2 ""
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__unmatched_brackets.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__unmatched_brackets.snap
new file mode 100644
index 0000000000..578cbf6099
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__unmatched_brackets.snap
@@ -0,0 +1,13 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"][\"#)"
+
+---
+ROOT@0..2
+ PREAMBLE@0..2
+ ERROR@0..1
+ R_BRACK@0..1 "]"
+ MIXED_GROUP@1..2
+ L_BRACK@1..2 "["
+ MISSING@2..2 ""
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__unmatched_brackets_with_group.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__unmatched_brackets_with_group.snap
new file mode 100644
index 0000000000..d929c98c46
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__unmatched_brackets_with_group.snap
@@ -0,0 +1,16 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"{][}\"#)"
+
+---
+ROOT@0..4
+ PREAMBLE@0..4
+ CURLY_GROUP@0..4
+ L_CURLY@0..1 "{"
+ ERROR@1..2
+ R_BRACK@1..2 "]"
+ MIXED_GROUP@2..3
+ L_BRACK@2..3 "["
+ MISSING@3..3 ""
+ R_CURLY@3..4 "}"
+
diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__verbatim_include_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__verbatim_include_simple.snap
new file mode 100644
index 0000000000..399e16d791
--- /dev/null
+++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__verbatim_include_simple.snap
@@ -0,0 +1,14 @@
+---
+source: src/syntax/latex/parser.rs
+expression: "setup(r#\"\\verbatiminput{foo/bar.txt}\"#)"
+
+---
+ROOT@0..27
+ PREAMBLE@0..27
+ VERBATIM_INCLUDE@0..27
+ VERBATIM_INCLUDE_NAME@0..14 "\\verbatiminput"
+ CURLY_GROUP_WORD_LIST@14..27
+ L_CURLY@14..15 "{"
+ WORD@15..26 "foo/bar.txt"
+ R_CURLY@26..27 "}"
+
diff --git a/support/texlab/src/syntax/latexindent.rs b/support/texlab/src/syntax/latexindent.rs
deleted file mode 100644
index c45c95d4e5..0000000000
--- a/support/texlab/src/syntax/latexindent.rs
+++ /dev/null
@@ -1,21 +0,0 @@
-use std::{io, process::Stdio};
-use tempfile::tempdir;
-use tokio::{fs, process::Command};
-
-pub async fn format(text: &str, extension: &str) -> io::Result<String> {
- let dir = tempdir()?;
- let input = format!("input.{}", extension);
- let output = format!("output.{}", extension);
- fs::write(dir.path().join(&input), text).await?;
-
- Command::new("latexindent")
- .args(&["-o", &output, &input])
- .current_dir(dir.path())
- .stdin(Stdio::null())
- .stdout(Stdio::null())
- .stderr(Stdio::null())
- .spawn()?
- .await?;
-
- fs::read_to_string(dir.path().join(&output)).await
-}
diff --git a/support/texlab/src/syntax/mod.rs b/support/texlab/src/syntax/mod.rs
deleted file mode 100644
index 87b33792df..0000000000
--- a/support/texlab/src/syntax/mod.rs
+++ /dev/null
@@ -1,14 +0,0 @@
-pub mod bibtex;
-mod generic_ast;
-mod lang_data;
-pub mod latex;
-pub mod latexindent;
-mod lsp_kind;
-mod text;
-
-pub use self::{
- generic_ast::{Ast, AstNodeIndex},
- lang_data::*,
- lsp_kind::Structure,
- text::{CharStream, Span, SyntaxNode},
-};
diff --git a/support/texlab/src/syntax/text.rs b/support/texlab/src/syntax/text.rs
deleted file mode 100644
index 17d6876126..0000000000
--- a/support/texlab/src/syntax/text.rs
+++ /dev/null
@@ -1,260 +0,0 @@
-use crate::protocol::{Position, Range};
-use serde::{Deserialize, Serialize};
-use std::{iter::Peekable, str::CharIndices};
-
-pub trait SyntaxNode {
- fn range(&self) -> Range;
-
- fn start(&self) -> Position {
- self.range().start
- }
-
- fn end(&self) -> Position {
- self.range().end
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct Span {
- pub range: Range,
- pub text: String,
-}
-
-impl Span {
- pub fn new(range: Range, text: String) -> Self {
- Self { range, text }
- }
-}
-
-impl SyntaxNode for Span {
- fn range(&self) -> Range {
- self.range
- }
-}
-
-#[derive(Debug)]
-pub struct CharStream<'a> {
- text: &'a str,
- chars: Peekable<CharIndices<'a>>,
- pub current_position: Position,
- pub current_index: usize,
- start_position: Position,
- start_index: usize,
-}
-
-impl<'a> CharStream<'a> {
- pub fn new(text: &'a str) -> Self {
- Self {
- text,
- chars: text.char_indices().peekable(),
- current_position: Position::new(0, 0),
- current_index: 0,
- start_position: Position::new(0, 0),
- start_index: 0,
- }
- }
-
- pub fn peek(&mut self) -> Option<char> {
- self.chars.peek().map(|(_, c)| *c)
- }
-
- pub fn satifies<P: FnOnce(&char) -> bool>(&mut self, predicate: P) -> bool {
- self.peek().filter(predicate).is_some()
- }
-
- pub fn skip_rest_of_line(&mut self) {
- loop {
- match self.peek() {
- Some('\n') => {
- self.next();
- break;
- }
- Some(_) => {
- self.next();
- }
- None => {
- break;
- }
- }
- }
- }
-
- pub fn start_span(&mut self) {
- self.start_index = self.current_index;
- self.start_position = self.current_position;
- }
-
- pub fn end_span(&mut self) -> Span {
- let range = Range::new(self.start_position, self.current_position);
- let text = &self.text[self.start_index..self.current_index];
- Span::new(range, text.to_owned())
- }
-
- pub fn seek(&mut self, position: Position) {
- while self.current_position < position {
- self.next();
- }
- }
-
- pub fn command(&mut self) -> Span {
- self.start_span();
- self.next();
- let mut escape = true;
- while self.satifies(|c| is_command_char(*c)) {
- self.next();
- escape = false;
- }
-
- if let Some(c) = self.peek() {
- if c != '\r' && c != '\n' && (escape || c == '*') {
- self.next();
- }
- }
-
- self.end_span()
- }
-
- fn update_position(&mut self, c: char) {
- if c == '\n' {
- self.current_position.line += 1;
- self.current_position.character = 0;
- } else {
- self.current_position.character += 1;
- }
- }
-
- pub fn extract(text: &'a str, range: Range) -> String {
- let mut stream = Self::new(text);
- stream.seek(range.start);
- stream.start_span();
- stream.seek(range.end);
- stream.end_span().text
- }
-}
-
-impl<'a> Iterator for CharStream<'a> {
- type Item = char;
-
- fn next(&mut self) -> Option<char> {
- if let Some((i, c)) = self.chars.next() {
- self.current_index = i + c.len_utf8();
- self.update_position(c);
- Some(c)
- } else {
- None
- }
- }
-}
-
-fn is_command_char(c: char) -> bool {
- c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '@'
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::protocol::RangeExt;
-
- #[test]
- fn peek() {
- let mut stream = CharStream::new("ab\nc");
- assert_eq!(Some('a'), stream.peek());
- assert_eq!(Some('a'), stream.next());
- assert_eq!(Some('b'), stream.peek());
- assert_eq!(Some('b'), stream.next());
- assert_eq!(Some('\n'), stream.peek());
- assert_eq!(Some('\n'), stream.next());
- assert_eq!(Some('c'), stream.peek());
- assert_eq!(Some('c'), stream.next());
- assert_eq!(None, stream.peek());
- assert_eq!(None, stream.next());
- }
-
- #[test]
- fn span() {
- let mut stream = CharStream::new("abc\ndef");
- stream.next();
- stream.start_span();
- stream.next();
- stream.next();
- let span = stream.end_span();
- assert_eq!(
- Span::new(Range::new_simple(0, 1, 0, 3), "bc".to_owned()),
- span
- );
- assert_eq!(Position::new(0, 1), span.start());
- assert_eq!(Position::new(0, 3), span.end());
- }
-
- #[test]
- fn span_unicode() {
- let mut stream = CharStream::new("😀😃😄😁");
- stream.next();
- stream.start_span();
- stream.next();
- stream.next();
- let span = stream.end_span();
- assert_eq!(
- Span::new(Range::new_simple(0, 1, 0, 3), "😃😄".to_owned()),
- span
- );
- }
-
- #[test]
- fn satifies() {
- let mut stream = CharStream::new("aBc");
- assert_eq!(true, stream.satifies(|c| c.is_lowercase()));
- stream.next();
- assert_eq!(false, stream.satifies(|c| c.is_lowercase()));
- }
-
- #[test]
- fn skip_rest_of_line() {
- let mut stream = CharStream::new("abc\ndef");
- stream.skip_rest_of_line();
- assert_eq!(Some('d'), stream.next());
- stream.skip_rest_of_line();
- assert_eq!(None, stream.next());
- stream.skip_rest_of_line();
- assert_eq!(None, stream.next());
- }
-
- #[test]
- fn seek() {
- let mut stream = CharStream::new("abc\ndefghi");
- let pos = Position::new(1, 2);
- stream.seek(pos);
- assert_eq!(Some('f'), stream.peek());
- }
-
- #[test]
- fn command_basic() {
- let mut stream = CharStream::new("\\foo@bar");
- let span = stream.command();
- assert_eq!(
- Span::new(Range::new_simple(0, 0, 0, 8), "\\foo@bar".to_owned()),
- span
- );
- }
-
- #[test]
- fn command_star() {
- let mut stream = CharStream::new("\\foo*");
- let span = stream.command();
- assert_eq!(
- Span::new(Range::new_simple(0, 0, 0, 5), "\\foo*".to_owned()),
- span
- );
- }
-
- #[test]
- fn command_escape() {
- let mut stream = CharStream::new("\\**");
- let span = stream.command();
- assert_eq!(
- Span::new(Range::new_simple(0, 0, 0, 2), "\\*".to_owned()),
- span
- );
- }
-}
diff --git a/support/texlab/src/test/client.rs b/support/texlab/src/test/client.rs
deleted file mode 100644
index b8806cc933..0000000000
--- a/support/texlab/src/test/client.rs
+++ /dev/null
@@ -1,73 +0,0 @@
-use crate::protocol::*;
-use jsonrpc::client::Result;
-use jsonrpc_derive::{jsonrpc_client, jsonrpc_method};
-
-#[jsonrpc_client(TestLatexLspClient)]
-pub trait TestLspClient {
- #[jsonrpc_method("initialize", kind = "request")]
- async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult>;
-
- #[jsonrpc_method("initialized", kind = "notification")]
- async fn initialized(&self, params: InitializedParams);
-
- #[jsonrpc_method("shutdown", kind = "request")]
- async fn shutdown(&self, params: ()) -> Result<()>;
-
- #[jsonrpc_method("exit", kind = "notification")]
- async fn exit(&self, params: ());
-
- #[jsonrpc_method("textDocument/didOpen", kind = "notification")]
- async fn did_open(&self, params: DidOpenTextDocumentParams);
-
- #[jsonrpc_method("textDocument/didChange", kind = "notification")]
- async fn did_change(&self, params: DidChangeTextDocumentParams);
-
- #[jsonrpc_method("workspace/didChangeConfiguration", kind = "notification")]
- async fn did_change_configuration(&self, params: DidChangeConfigurationParams);
-
- #[jsonrpc_method("textDocument/definition", kind = "request")]
- async fn definition(&self, params: TextDocumentPositionParams) -> Result<DefinitionResponse>;
-
- #[jsonrpc_method("textDocument/completion", kind = "request")]
- async fn completion(&self, params: CompletionParams) -> Result<CompletionList>;
-
- #[jsonrpc_method("completionItem/resolve", kind = "request")]
- async fn completion_resolve(&self, item: CompletionItem) -> Result<CompletionItem>;
-
- #[jsonrpc_method("textDocument/foldingRange", kind = "request")]
- async fn folding_range(&self, params: FoldingRangeParams) -> Result<Vec<FoldingRange>>;
-
- #[jsonrpc_method("textDocument/documentHighlight", kind = "request")]
- async fn document_highlight(
- &self,
- params: TextDocumentPositionParams,
- ) -> Result<Vec<DocumentHighlight>>;
-
- #[jsonrpc_method("textDocument/documentLink", kind = "request")]
- async fn document_link(&self, params: DocumentLinkParams) -> Result<Vec<DocumentLink>>;
-
- #[jsonrpc_method("textDocument/references", kind = "request")]
- async fn references(&self, params: ReferenceParams) -> Result<Vec<Location>>;
-
- #[jsonrpc_method("textDocument/prepareRename", kind = "request")]
- async fn prepare_rename(&self, params: TextDocumentPositionParams) -> Result<Option<Range>>;
-
- #[jsonrpc_method("textDocument/rename", kind = "request")]
- async fn rename(&self, params: RenameParams) -> Result<Option<WorkspaceEdit>>;
-
- #[jsonrpc_method("textDocument/hover", kind = "request")]
- async fn hover(&self, params: TextDocumentPositionParams) -> Result<Option<Hover>>;
-
- #[jsonrpc_method("workspace/symbol", kind = "request")]
- async fn workspace_symbol(
- &self,
- params: WorkspaceSymbolParams,
- ) -> Result<Vec<SymbolInformation>>;
-
- #[jsonrpc_method("textDocument/documentSymbol", kind = "request")]
- async fn document_symbol(&self, params: DocumentSymbolParams)
- -> Result<DocumentSymbolResponse>;
-
- #[jsonrpc_method("$/detectRoot", kind = "request")]
- async fn detect_root(&self, params: TextDocumentIdentifier) -> Result<()>;
-}
diff --git a/support/texlab/src/test/mod.rs b/support/texlab/src/test/mod.rs
deleted file mode 100644
index 5f26fb0dcc..0000000000
--- a/support/texlab/src/test/mod.rs
+++ /dev/null
@@ -1,632 +0,0 @@
-#![allow(deprecated)]
-
-mod client;
-mod server;
-
-pub use self::{
- client::{TestLatexLspClient, TestLspClient},
- server::TestLatexLspServer,
-};
-
-use crate::{
- protocol::*,
- server::LatexLspServer,
- tex::{Distribution, UnknownDistribution},
-};
-use futures::{
- channel::mpsc,
- future::{join, AbortHandle, Abortable},
- lock::Mutex,
- prelude::*,
-};
-use jsonrpc::MessageHandler;
-use once_cell::sync::Lazy;
-use std::{path::PathBuf, sync::Arc};
-use tempfile::{tempdir, TempDir};
-use tokio::fs;
-
-struct GlobalDistribution {
- distro: Mutex<Option<Arc<dyn Distribution>>>,
-}
-
-impl GlobalDistribution {
- fn new() -> Self {
- Self {
- distro: Mutex::new(None),
- }
- }
-
- async fn get(&self, use_distro: bool) -> Arc<dyn Distribution> {
- if use_distro {
- let mut distro_lock = self.distro.lock().await;
- match &*distro_lock {
- Some(distro) => distro.clone(),
- None => {
- let distro = Distribution::detect().await;
- *distro_lock = Some(distro.clone());
- distro
- }
- }
- } else {
- Arc::new(UnknownDistribution::default())
- }
- }
-}
-
-static DISTRO: Lazy<GlobalDistribution> = Lazy::new(GlobalDistribution::new);
-
-#[derive(Debug)]
-struct Endpoint<S, C> {
- receiver: mpsc::Receiver<String>,
- client: Arc<C>,
- server: Arc<S>,
- handler: MessageHandler<S, C>,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Default)]
-pub struct TestBedBuilder {
- files: Vec<(PathBuf, String)>,
- use_distro: bool,
- root_dir: Option<PathBuf>,
- latex_build: Option<LatexBuildOptions>,
- latex_lint: Option<LatexLintOptions>,
- latex_forward_search: Option<LatexForwardSearchOptions>,
- bibtex_formatting: Option<BibtexFormattingOptions>,
-}
-
-impl TestBedBuilder {
- pub fn new() -> Self {
- Self::default()
- }
-
- pub fn file<P: Into<PathBuf>>(&mut self, path: P, text: &str) -> &mut Self {
- self.files.push((path.into(), text.trim().into()));
- self
- }
-
- pub fn use_distro(&mut self) -> &mut Self {
- self.use_distro = true;
- self
- }
-
- pub fn root_dir<P: Into<PathBuf>>(&mut self, path: P) -> &mut Self {
- self.root_dir = Some(path.into());
- self
- }
-
- pub fn latex_build(&mut self, options: LatexBuildOptions) -> &mut Self {
- self.latex_build = Some(options);
- self
- }
-
- pub fn latex_lint(&mut self, options: LatexLintOptions) -> &mut Self {
- self.latex_lint = Some(options);
- self
- }
-
- pub fn latex_forward_search(&mut self, options: LatexForwardSearchOptions) -> &mut Self {
- self.latex_forward_search = Some(options);
- self
- }
-
- pub fn bibtex_formatting(&mut self, options: BibtexFormattingOptions) -> &mut Self {
- self.bibtex_formatting = Some(options);
- self
- }
-
- pub async fn build(&self) -> TestBed {
- let dir = tempdir().expect("failed to create temporary directory");
- for (path, text) in &self.files {
- let full_path = dir.path().join(path);
- fs::create_dir_all(full_path.parent().unwrap())
- .await
- .unwrap();
- fs::write(&full_path, text).await.unwrap();
- }
-
- let (tx1, rx1) = mpsc::channel(0);
- let (tx2, rx2) = mpsc::channel(0);
-
- let endpoint1 = self.build_endpoint1(&dir, tx2, rx1).await;
- let endpoint2 = self.build_endpoint2(&dir, tx1, rx2).await;
- let server = Arc::clone(&endpoint2.server);
- let client = Arc::clone(&endpoint2.client);
- TestBed {
- dir,
- server,
- client,
- endpoint1: Some(endpoint1),
- endpoint2: Some(endpoint2),
- handle: None,
- }
- }
-
- async fn build_endpoint1(
- &self,
- dir: &TempDir,
- tx2: mpsc::Sender<String>,
- rx1: mpsc::Receiver<String>,
- ) -> Endpoint<LatexLspServer<LatexLspClient>, LatexLspClient> {
- let client = Arc::new(LatexLspClient::new(tx2.clone()));
- let server = Arc::new(LatexLspServer::new(
- DISTRO.get(self.use_distro).await,
- Arc::clone(&client),
- Arc::new(dir.path().to_path_buf()),
- ));
-
- Endpoint {
- receiver: rx1,
- client: Arc::clone(&client),
- server: Arc::clone(&server),
- handler: MessageHandler {
- client,
- server,
- output: tx2.clone(),
- },
- }
- }
-
- async fn build_endpoint2(
- &self,
- dir: &TempDir,
- tx1: mpsc::Sender<String>,
- rx2: mpsc::Receiver<String>,
- ) -> Endpoint<TestLatexLspServer, TestLatexLspClient> {
- let options = Options {
- latex: Some(LatexOptions {
- root_directory: self.root_dir.as_ref().map(|path| dir.path().join(path)),
- build: self.latex_build.clone(),
- forward_search: self.latex_forward_search.clone(),
- lint: self.latex_lint.clone(),
- }),
- bibtex: Some(BibtexOptions {
- formatting: self.bibtex_formatting.clone(),
- }),
- };
-
- let test_client = Arc::new(TestLatexLspClient::new(tx1.clone()));
- let test_server = Arc::new(TestLatexLspServer::new(options));
-
- Endpoint {
- receiver: rx2,
- client: Arc::clone(&test_client),
- server: Arc::clone(&test_server),
- handler: MessageHandler {
- client: test_client,
- server: test_server,
- output: tx1,
- },
- }
- }
-}
-
-pub struct TestBed {
- pub dir: TempDir,
- pub server: Arc<TestLatexLspServer>,
- pub client: Arc<TestLatexLspClient>,
- endpoint1: Option<Endpoint<LatexLspServer<LatexLspClient>, LatexLspClient>>,
- endpoint2: Option<Endpoint<TestLatexLspServer, TestLatexLspClient>>,
- handle: Option<AbortHandle>,
-}
-
-impl TestBed {
- pub fn spawn(&mut self) {
- let (handle, reg) = AbortHandle::new_pair();
-
- let endpoint1 = self.endpoint1.take().unwrap();
- let endpoint2 = self.endpoint2.take().unwrap();
- let mut rx1 = endpoint1.receiver;
- let mut rx2 = endpoint2.receiver;
- let mut handler1 = endpoint1.handler;
- let mut handler2 = endpoint2.handler;
-
- tokio::spawn(Abortable::new(
- async move {
- let task1 = async move {
- while let Some(json) = rx2.next().await {
- handler2.handle(&json).await;
- }
- };
-
- let task2 = async move {
- while let Some(json) = rx1.next().await {
- handler1.handle(&json).await;
- }
- };
-
- join(task1, task2).await;
- },
- reg,
- ));
- self.handle = Some(handle);
- }
-
- pub async fn initialize(&self, capabilities: ClientCapabilities) {
- self.client
- .initialize(InitializeParams {
- capabilities,
- initialization_options: None,
- process_id: None,
- root_path: None,
- root_uri: None,
- trace: None,
- workspace_folders: None,
- client_info: None,
- })
- .await
- .unwrap();
- self.client.initialized(InitializedParams {}).await;
- }
-
- pub fn path(&self, relative_path: &str) -> PathBuf {
- self.dir.path().join(relative_path)
- }
-
- pub fn uri(&self, relative_path: &str) -> Uri {
- Uri::from_file_path(self.path(relative_path)).unwrap()
- }
-
- pub fn identifier(&self, relative_path: &str) -> TextDocumentIdentifier {
- TextDocumentIdentifier::new(self.uri(relative_path).into())
- }
-
- pub async fn open(&self, relative_path: &str) {
- let full_path = self.path(relative_path);
- let params = DidOpenTextDocumentParams {
- text_document: TextDocumentItem {
- language_id: if relative_path.ends_with("bib") {
- "bibtex".into()
- } else {
- "latex".into()
- },
- uri: Uri::from_file_path(&full_path).unwrap().into(),
- version: 0,
- text: fs::read_to_string(&full_path).await.unwrap(),
- },
- };
- self.client.did_open(params).await;
- }
-
- pub async fn edit<S: Into<String>>(&self, relative_path: &str, text: S) {
- let uri = self.uri(relative_path).into();
- let params = DidChangeTextDocumentParams {
- text_document: VersionedTextDocumentIdentifier::new(uri, 0),
- content_changes: vec![TextDocumentContentChangeEvent {
- range: None,
- range_length: None,
- text: text.into(),
- }],
- };
- self.client.did_change(params).await;
- }
-
- pub async fn push_options(&self) {
- let options = self.server.options.lock().await.clone();
- let params = DidChangeConfigurationParams {
- settings: serde_json::to_value::<Options>(options).unwrap(),
- };
- self.client.did_change_configuration(params).await
- }
-
- pub async fn definition_link(
- &self,
- relative_path: &str,
- line: u64,
- character: u64,
- ) -> Option<Vec<LocationLink>> {
- let params = TextDocumentPositionParams {
- text_document: self.identifier(relative_path),
- position: Position::new(line, character),
- };
- let response = self.client.definition(params).await.ok()?;
- match response {
- DefinitionResponse::LocationLinks(links) => Some(links),
- DefinitionResponse::Locations(_) => unreachable!(),
- }
- }
-
- pub async fn definition_location(
- &self,
- relative_path: &str,
- line: u64,
- character: u64,
- ) -> Option<Vec<Location>> {
- let params = TextDocumentPositionParams {
- text_document: self.identifier(relative_path),
- position: Position::new(line, character),
- };
- let response = self.client.definition(params).await.ok()?;
- match response {
- DefinitionResponse::LocationLinks(_) => unreachable!(),
- DefinitionResponse::Locations(locations) => Some(locations),
- }
- }
-
- pub async fn completion(
- &self,
- relative_path: &str,
- line: u64,
- character: u64,
- ) -> Option<Vec<CompletionItem>> {
- let params = CompletionParams {
- text_document_position: TextDocumentPositionParams {
- text_document: self.identifier(relative_path),
- position: Position::new(line, character),
- },
- context: None,
- work_done_progress_params: WorkDoneProgressParams::default(),
- partial_result_params: PartialResultParams::default(),
- };
- self.client
- .completion(params)
- .await
- .ok()
- .map(|list| list.items)
- }
-
- pub async fn folding_range(&self, relative_path: &str) -> Option<Vec<FoldingRange>> {
- let params = FoldingRangeParams {
- text_document: self.identifier(relative_path),
- work_done_progress_params: WorkDoneProgressParams::default(),
- partial_result_params: PartialResultParams::default(),
- };
- self.client.folding_range(params).await.ok()
- }
-
- pub async fn document_highlight(
- &self,
- relative_path: &str,
- line: u64,
- character: u64,
- ) -> Option<Vec<DocumentHighlight>> {
- let params = TextDocumentPositionParams {
- text_document: self.identifier(relative_path),
- position: Position::new(line, character),
- };
- self.client.document_highlight(params).await.ok()
- }
-
- pub async fn document_link(&self, relative_path: &str) -> Option<Vec<DocumentLink>> {
- let params = DocumentLinkParams {
- text_document: self.identifier(relative_path),
- work_done_progress_params: WorkDoneProgressParams::default(),
- partial_result_params: PartialResultParams::default(),
- };
- self.client.document_link(params).await.ok()
- }
-
- pub async fn references(
- &self,
- relative_path: &str,
- line: u64,
- character: u64,
- include_declaration: bool,
- ) -> Option<Vec<Location>> {
- let params = ReferenceParams {
- text_document_position: TextDocumentPositionParams::new(
- self.identifier(relative_path),
- Position::new(line, character),
- ),
- context: ReferenceContext {
- include_declaration,
- },
- work_done_progress_params: WorkDoneProgressParams::default(),
- partial_result_params: PartialResultParams::default(),
- };
- self.client.references(params).await.ok()
- }
-
- pub async fn prepare_rename(
- &self,
- relative_path: &str,
- line: u64,
- character: u64,
- ) -> Option<Option<Range>> {
- let pos = Position::new(line, character);
- let params = TextDocumentPositionParams::new(self.identifier(relative_path), pos);
- self.client.prepare_rename(params).await.ok()
- }
-
- pub async fn rename<S: Into<String>>(
- &self,
- relative_path: &str,
- line: u64,
- character: u64,
- new_name: S,
- ) -> Option<Option<WorkspaceEdit>> {
- let params = RenameParams {
- text_document_position: TextDocumentPositionParams::new(
- self.identifier(relative_path),
- Position::new(line, character),
- ),
- new_name: new_name.into(),
- work_done_progress_params: WorkDoneProgressParams::default(),
- };
- self.client.rename(params).await.ok()
- }
-
- pub async fn document_symbol_flat(
- &self,
- relative_path: &str,
- ) -> Option<Vec<SymbolInformation>> {
- let params = DocumentSymbolParams {
- text_document: self.identifier(relative_path),
- work_done_progress_params: WorkDoneProgressParams::default(),
- partial_result_params: PartialResultParams::default(),
- };
- match self.client.document_symbol(params).await.ok()? {
- DocumentSymbolResponse::Flat(symbols) => Some(symbols),
- DocumentSymbolResponse::Nested(_) => unreachable!(),
- }
- }
-
- pub async fn document_symbol_nested(&self, relative_path: &str) -> Option<Vec<DocumentSymbol>> {
- let params = DocumentSymbolParams {
- text_document: self.identifier(relative_path),
- work_done_progress_params: WorkDoneProgressParams::default(),
- partial_result_params: PartialResultParams::default(),
- };
- match self.client.document_symbol(params).await.ok()? {
- DocumentSymbolResponse::Flat(_) => unreachable!(),
- DocumentSymbolResponse::Nested(symbols) => Some(symbols),
- }
- }
-
- pub async fn hover(
- &self,
- relative_path: &str,
- line: u64,
- character: u64,
- ) -> Option<Option<Hover>> {
- let params = TextDocumentPositionParams {
- text_document: self.identifier(relative_path),
- position: Position::new(line, character),
- };
- self.client.hover(params).await.ok()
- }
-
- pub async fn detect_root(&self, relative_path: &str) {
- self.client
- .detect_root(self.identifier(relative_path))
- .await
- .unwrap();
- }
-
- pub async fn shutdown(&self) {
- self.client.shutdown(()).await.unwrap();
- self.client.exit(()).await;
- }
-}
-
-impl Drop for TestBed {
- fn drop(&mut self) {
- if let Some(handle) = self.handle.as_ref() {
- handle.abort();
- }
- }
-}
-
-pub static PULL_CAPABILITIES: ClientCapabilities = {
- ClientCapabilities {
- experimental: None,
- text_document: None,
- window: None,
- workspace: Some(WorkspaceClientCapabilities {
- apply_edit: None,
- configuration: Some(true),
- did_change_configuration: None,
- did_change_watched_files: None,
- execute_command: None,
- symbol: None,
- workspace_edit: None,
- workspace_folders: None,
- }),
- }
-};
-
-pub static PUSH_CAPABILITIES: ClientCapabilities = {
- ClientCapabilities {
- experimental: None,
- text_document: None,
- window: None,
- workspace: Some(WorkspaceClientCapabilities {
- apply_edit: None,
- configuration: None,
- did_change_configuration: Some(GenericCapability {
- dynamic_registration: Some(true),
- }),
- did_change_watched_files: None,
- execute_command: None,
- symbol: None,
- workspace_edit: None,
- workspace_folders: None,
- }),
- }
-};
-
-pub static NESTED_SYMBOL_CAPABILITIES: ClientCapabilities = {
- ClientCapabilities {
- experimental: None,
- text_document: Some(TextDocumentClientCapabilities {
- code_action: None,
- code_lens: None,
- color_provider: None,
- completion: None,
- declaration: None,
- definition: None,
- document_highlight: None,
- document_link: None,
- document_symbol: Some(DocumentSymbolCapability {
- hierarchical_document_symbol_support: Some(true),
- dynamic_registration: None,
- symbol_kind: None,
- }),
- folding_range: None,
- formatting: None,
- hover: None,
- implementation: None,
- on_type_formatting: None,
- publish_diagnostics: None,
- range_formatting: None,
- references: None,
- rename: None,
- signature_help: None,
- synchronization: None,
- type_definition: None,
- }),
- window: None,
- workspace: Some(WorkspaceClientCapabilities {
- apply_edit: None,
- configuration: Some(true),
- did_change_configuration: None,
- did_change_watched_files: None,
- execute_command: None,
- symbol: None,
- workspace_edit: None,
- workspace_folders: None,
- }),
- }
-};
-
-pub static LOCATION_LINK_CAPABILITIES: ClientCapabilities = {
- ClientCapabilities {
- experimental: None,
- text_document: Some(TextDocumentClientCapabilities {
- code_action: None,
- code_lens: None,
- color_provider: None,
- completion: None,
- declaration: None,
- definition: Some(GotoCapability {
- dynamic_registration: None,
- link_support: Some(true),
- }),
- document_highlight: None,
- document_link: None,
- document_symbol: None,
- folding_range: None,
- formatting: None,
- hover: None,
- implementation: None,
- on_type_formatting: None,
- publish_diagnostics: None,
- range_formatting: None,
- references: None,
- rename: None,
- signature_help: None,
- synchronization: None,
- type_definition: None,
- }),
- window: None,
- workspace: Some(WorkspaceClientCapabilities {
- apply_edit: None,
- configuration: Some(true),
- did_change_configuration: None,
- did_change_watched_files: None,
- execute_command: None,
- symbol: None,
- workspace_edit: None,
- workspace_folders: None,
- }),
- }
-};
diff --git a/support/texlab/src/test/server.rs b/support/texlab/src/test/server.rs
deleted file mode 100644
index 77bed68c07..0000000000
--- a/support/texlab/src/test/server.rs
+++ /dev/null
@@ -1,87 +0,0 @@
-use crate::protocol::*;
-use aovec::Aovec;
-use async_trait::async_trait;
-use chashmap::CHashMap;
-use futures::lock::Mutex;
-use jsonrpc::server::{Middleware, Result};
-use jsonrpc_derive::{jsonrpc_method, jsonrpc_server};
-
-pub struct TestLatexLspServer {
- pub options: Mutex<Options>,
- pub show_message_buf: Aovec<ShowMessageParams>,
- pub register_capability_buf: Aovec<RegistrationParams>,
- pub diagnostics_by_uri: CHashMap<Uri, Vec<Diagnostic>>,
- pub progress_buf: Aovec<ProgressParams>,
- pub work_done_progress_create_buf: Aovec<WorkDoneProgressCreateParams>,
- pub log_message_buf: Aovec<LogMessageParams>,
-}
-
-#[jsonrpc_server]
-impl TestLatexLspServer {
- pub fn new(options: Options) -> Self {
- let base = 16;
- Self {
- options: Mutex::new(options),
- show_message_buf: Aovec::new(base),
- register_capability_buf: Aovec::new(base),
- diagnostics_by_uri: CHashMap::new(),
- progress_buf: Aovec::new(base),
- work_done_progress_create_buf: Aovec::new(base),
- log_message_buf: Aovec::new(base),
- }
- }
-
- #[jsonrpc_method("workspace/configuration", kind = "request")]
- pub async fn configuration(&self, params: ConfigurationParams) -> Result<serde_json::Value> {
- let options = self.options.lock().await;
- if params.items[0].section.as_ref().unwrap() == "latex" {
- Ok(serde_json::to_value(vec![options.latex.clone().unwrap_or_default()]).unwrap())
- } else {
- Ok(serde_json::to_value(vec![options.bibtex.clone().unwrap_or_default()]).unwrap())
- }
- }
-
- #[jsonrpc_method("window/showMessage", kind = "notification")]
- pub async fn show_message(&self, params: ShowMessageParams) {
- self.show_message_buf.push(params);
- }
-
- #[jsonrpc_method("client/registerCapability", kind = "request")]
- pub async fn register_capability(&self, params: RegistrationParams) -> Result<()> {
- self.register_capability_buf.push(params);
- Ok(())
- }
-
- #[jsonrpc_method("textDocument/publishDiagnostics", kind = "notification")]
- pub async fn publish_diagnostics(&self, params: PublishDiagnosticsParams) {
- let _ = self
- .diagnostics_by_uri
- .insert(params.uri.into(), params.diagnostics);
- }
-
- #[jsonrpc_method("$/progress", kind = "notification")]
- pub async fn progress(&self, params: ProgressParams) {
- self.progress_buf.push(params);
- }
-
- #[jsonrpc_method("window/workDoneProgress/create", kind = "request")]
- pub async fn work_done_progress_create(
- &self,
- params: WorkDoneProgressCreateParams,
- ) -> Result<()> {
- self.work_done_progress_create_buf.push(params);
- Ok(())
- }
-
- #[jsonrpc_method("window/logMessage", kind = "notification")]
- pub async fn log_message(&self, params: LogMessageParams) {
- self.log_message_buf.push(params);
- }
-}
-
-#[async_trait]
-impl Middleware for TestLatexLspServer {
- async fn before_message(&self) {}
-
- async fn after_message(&self) {}
-}
diff --git a/support/texlab/src/tex/compile.rs b/support/texlab/src/tex/compile.rs
deleted file mode 100644
index a2b0d2f37d..0000000000
--- a/support/texlab/src/tex/compile.rs
+++ /dev/null
@@ -1,97 +0,0 @@
-use std::{io, process::Stdio, time::Duration};
-use tempfile::{tempdir, TempDir};
-use thiserror::Error;
-use tokio::{
- fs,
- process::Command,
- time::{timeout, Elapsed},
-};
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum Format {
- Latex,
- Pdflatex,
- Xelatex,
- Lualatex,
-}
-
-impl Format {
- pub fn executable(self) -> &'static str {
- match self {
- Self::Latex => "latex",
- Self::Pdflatex => "pdflatex",
- Self::Xelatex => "xelatex",
- Self::Lualatex => "lualatex",
- }
- }
-}
-
-#[derive(Debug)]
-pub struct Artifacts {
- pub dir: TempDir,
- pub log: String,
-}
-
-#[derive(Debug, Error)]
-pub enum CompileError {
- #[error("an I/O error occurred: `{0}`")]
- IO(#[from] io::Error),
- #[error("TeX engine is not installed")]
- NotInstalled,
- #[error("build timeout: `{0}`")]
- Timeout(#[from] Elapsed),
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub struct CompileParams<'a> {
- pub format: Format,
- pub file_name: &'a str,
- pub code: &'a str,
- pub timeout: Duration,
-}
-
-impl<'a> Default for CompileParams<'a> {
- fn default() -> Self {
- Self {
- format: Format::Lualatex,
- file_name: "code.tex",
- code: "",
- timeout: Duration::from_secs(15),
- }
- }
-}
-
-#[derive(Debug, Clone, Copy)]
-pub struct Compiler<'a> {
- pub executable: &'a str,
- pub args: &'a [&'a str],
- pub file_name: &'a str,
- pub timeout: Duration,
-}
-
-impl<'a> Compiler<'a> {
- pub async fn compile<'b>(&'a self, code: &'b str) -> Result<Artifacts, CompileError> {
- let directory = tempdir()?;
- let tex_file = directory.path().join(self.file_name);
- fs::write(&tex_file, code).await?;
-
- let child = Command::new(self.executable)
- .args(self.args)
- .current_dir(&directory)
- .stdout(Stdio::null())
- .stderr(Stdio::null())
- .status();
-
- timeout(self.timeout, child)
- .await?
- .map_err(|_| CompileError::NotInstalled)?;
-
- let log_file = tex_file.with_extension("log");
- let log_bytes = fs::read(log_file).await?;
- let log = String::from_utf8_lossy(&log_bytes).into_owned();
- Ok(Artifacts {
- dir: directory,
- log,
- })
- }
-}
diff --git a/support/texlab/src/tex/kpsewhich.rs b/support/texlab/src/tex/kpsewhich.rs
deleted file mode 100644
index cb2ab89de5..0000000000
--- a/support/texlab/src/tex/kpsewhich.rs
+++ /dev/null
@@ -1,112 +0,0 @@
-use super::Language;
-use futures::Future;
-use std::{
- collections::HashMap,
- env,
- ffi::OsStr,
- io,
- path::{Path, PathBuf},
- string::FromUtf8Error,
-};
-use thiserror::Error;
-use tokio::{fs, process::Command};
-
-#[derive(Debug, Error)]
-pub enum KpsewhichError {
- #[error("an I/O error occurred: `{0}`")]
- IO(#[from] io::Error),
- #[error("an utf8 error occurred: `{0}`")]
- Decode(#[from] FromUtf8Error),
- #[error("invalid output from kpsewhich")]
- InvalidOutput,
- #[error("kpsewhich not installed")]
- NotInstalled,
- #[error("no kpsewhich database")]
- NoDatabase,
- #[error("corrupt kpsewhich database")]
- CorruptDatabase,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Default)]
-pub struct Resolver {
- pub files_by_name: HashMap<String, PathBuf>,
-}
-
-impl Resolver {
- pub fn new(files_by_name: HashMap<String, PathBuf>) -> Self {
- Self { files_by_name }
- }
-}
-
-pub async fn parse_database<'a, R, F>(
- root_directories: &'a [PathBuf],
- reader: R,
-) -> Result<Resolver, KpsewhichError>
-where
- R: Fn(&'a Path) -> F,
- F: Future<Output = Result<Vec<PathBuf>, KpsewhichError>>,
-{
- let mut files_by_name = HashMap::new();
- for directory in root_directories {
- for path in reader(directory).await? {
- if is_tex_file(&path) {
- if let Some(path) = make_absolute(root_directories, &path).await {
- if let Some(name) = path
- .file_name()
- .and_then(OsStr::to_str)
- .map(ToString::to_string)
- {
- files_by_name.insert(name, path);
- }
- }
- }
- }
- }
- Ok(Resolver::new(files_by_name))
-}
-
-fn is_tex_file(path: &Path) -> bool {
- path.extension()
- .and_then(OsStr::to_str)
- .and_then(Language::by_extension)
- .is_some()
-}
-
-async fn make_absolute(root_directories: &[PathBuf], relative_path: &Path) -> Option<PathBuf> {
- for dir in root_directories.iter().rev() {
- if let Ok(path) = fs::canonicalize(dir.join(&relative_path)).await {
- return Some(path);
- }
- }
- None
-}
-
-pub async fn root_directories() -> Result<Vec<PathBuf>, KpsewhichError> {
- let texmf = run(&["-var-value", "TEXMF"]).await?;
- let expand_arg = format!("--expand-braces={}", texmf);
- let expanded = run(&[&expand_arg]).await?;
- let directories = env::split_paths(&expanded.replace("!", ""))
- .filter(|path| path.exists())
- .collect();
- Ok(directories)
-}
-
-async fn run<I, S>(args: I) -> Result<String, KpsewhichError>
-where
- I: IntoIterator<Item = S>,
- S: AsRef<OsStr>,
-{
- let output = Command::new("kpsewhich")
- .args(args)
- .output()
- .await
- .map_err(|_| KpsewhichError::NotInstalled)?;
-
- let result = String::from_utf8(output.stdout)?
- .lines()
- .next()
- .ok_or(KpsewhichError::InvalidOutput)?
- .into();
-
- Ok(result)
-}
diff --git a/support/texlab/src/tex/mod.rs b/support/texlab/src/tex/mod.rs
deleted file mode 100644
index 59318f9d22..0000000000
--- a/support/texlab/src/tex/mod.rs
+++ /dev/null
@@ -1,154 +0,0 @@
-mod compile;
-mod kpsewhich;
-mod miktex;
-mod tectonic;
-mod texlive;
-
-pub use self::{
- compile::{Artifacts, CompileError, CompileParams, Format},
- kpsewhich::{KpsewhichError, Resolver},
-};
-
-use self::{compile::Compiler, miktex::Miktex, tectonic::Tectonic, texlive::Texlive};
-use async_trait::async_trait;
-use std::{fmt, process::Stdio, sync::Arc};
-use tokio::process::Command;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum DistributionKind {
- Texlive,
- Miktex,
- Tectonic,
- Unknown,
-}
-
-impl fmt::Display for DistributionKind {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- let name = match self {
- Self::Texlive => "TeXLive",
- Self::Miktex => "MikTeX",
- Self::Tectonic => "Tectonic",
- Self::Unknown => "Unknown",
- };
- write!(f, "{}", name)
- }
-}
-
-impl DistributionKind {
- pub async fn detect() -> Self {
- if Command::new("tectonic")
- .arg("--version")
- .stdout(Stdio::null())
- .stderr(Stdio::null())
- .status()
- .await
- .is_ok()
- {
- return Self::Tectonic;
- }
-
- match Command::new("latex").arg("--version").output().await {
- Ok(output) => {
- let stdout = String::from_utf8_lossy(&output.stdout);
- if stdout.contains("TeX Live") {
- Self::Texlive
- } else if stdout.contains("MiKTeX") {
- Self::Miktex
- } else {
- Self::Unknown
- }
- }
- Err(_) => Self::Unknown,
- }
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum Language {
- Latex,
- Bibtex,
-}
-
-impl Language {
- pub fn by_extension(extension: &str) -> Option<Self> {
- match extension.to_lowercase().as_str() {
- "tex" | "sty" | "cls" | "def" | "lco" | "aux" | "rnw" => Some(Language::Latex),
- "bib" | "bibtex" => Some(Language::Bibtex),
- _ => None,
- }
- }
-
- pub fn by_language_id(language_id: &str) -> Option<Self> {
- match language_id {
- "latex" | "tex" => Some(Language::Latex),
- "bibtex" | "bib" => Some(Language::Bibtex),
- _ => None,
- }
- }
-}
-
-#[async_trait]
-pub trait Distribution: Send + Sync {
- fn kind(&self) -> DistributionKind;
-
- async fn compile<'a>(&'a self, params: CompileParams<'a>) -> Result<Artifacts, CompileError>;
-
- async fn load(&self) -> Result<(), KpsewhichError>;
-
- async fn resolver(&self) -> Arc<Resolver>;
-}
-
-impl dyn Distribution {
- pub async fn detect() -> Arc<dyn Distribution> {
- let kind = DistributionKind::detect().await;
- let distro: Arc<dyn Distribution + Send + Sync> = match kind {
- DistributionKind::Texlive => Arc::new(Texlive::default()),
- DistributionKind::Miktex => Arc::new(Miktex::default()),
- DistributionKind::Tectonic => Arc::new(Tectonic::default()),
- DistributionKind::Unknown => Arc::new(UnknownDistribution::default()),
- };
- distro
- }
-}
-
-impl fmt::Debug for dyn Distribution {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "{}", self.kind())
- }
-}
-
-async fn compile(params: CompileParams<'_>) -> Result<Artifacts, CompileError> {
- let executable = params.format.executable();
- let args = &["--interaction=batchmode", "-shell-escape", params.file_name];
- let compiler = Compiler {
- executable,
- args,
- file_name: params.file_name,
- timeout: params.timeout,
- };
- compiler.compile(params.code).await
-}
-
-#[derive(Debug, Default)]
-pub struct UnknownDistribution {
- resolver: Arc<Resolver>,
-}
-
-#[async_trait]
-impl Distribution for UnknownDistribution {
- fn kind(&self) -> DistributionKind {
- DistributionKind::Unknown
- }
-
- async fn compile<'a>(&'a self, _params: CompileParams<'a>) -> Result<Artifacts, CompileError> {
- Err(CompileError::NotInstalled)
- }
-
- async fn load(&self) -> Result<(), KpsewhichError> {
- Ok(())
- }
-
- async fn resolver(&self) -> Arc<Resolver> {
- Arc::clone(&self.resolver)
- }
-}
diff --git a/support/texlab/src/tex/tectonic.rs b/support/texlab/src/tex/tectonic.rs
deleted file mode 100644
index 1c19466830..0000000000
--- a/support/texlab/src/tex/tectonic.rs
+++ /dev/null
@@ -1,36 +0,0 @@
-use super::{
- compile::{Artifacts, CompileError, CompileParams, Compiler},
- kpsewhich::{KpsewhichError, Resolver},
- Distribution, DistributionKind,
-};
-use async_trait::async_trait;
-use std::sync::Arc;
-
-#[derive(Debug, Default)]
-pub struct Tectonic;
-
-#[async_trait]
-impl Distribution for Tectonic {
- fn kind(&self) -> DistributionKind {
- DistributionKind::Tectonic
- }
-
- async fn compile<'a>(&'a self, params: CompileParams<'a>) -> Result<Artifacts, CompileError> {
- let args = [params.file_name];
- let compiler = Compiler {
- executable: "tectonic",
- args: &args,
- file_name: params.file_name,
- timeout: params.timeout,
- };
- compiler.compile(params.code).await
- }
-
- async fn load(&self) -> Result<(), KpsewhichError> {
- Ok(())
- }
-
- async fn resolver(&self) -> Arc<Resolver> {
- Arc::new(Resolver::default())
- }
-}
diff --git a/support/texlab/src/tex/texlive.rs b/support/texlab/src/tex/texlive.rs
deleted file mode 100644
index 5afb763d9e..0000000000
--- a/support/texlab/src/tex/texlive.rs
+++ /dev/null
@@ -1,71 +0,0 @@
-use super::{
- compile,
- kpsewhich::{self, KpsewhichError, Resolver},
- Artifacts, CompileError, CompileParams, Distribution, DistributionKind,
-};
-use async_trait::async_trait;
-use futures::lock::Mutex;
-use std::{
- io,
- path::{Path, PathBuf},
- str::Lines,
- sync::Arc,
-};
-use tokio::fs;
-
-#[derive(Debug, Default)]
-pub struct Texlive {
- resolver: Mutex<Arc<Resolver>>,
-}
-
-#[async_trait]
-impl Distribution for Texlive {
- fn kind(&self) -> DistributionKind {
- DistributionKind::Texlive
- }
-
- async fn compile<'a>(&'a self, params: CompileParams<'a>) -> Result<Artifacts, CompileError> {
- compile(params).await
- }
-
- async fn load(&self) -> Result<(), KpsewhichError> {
- let root_directories = kpsewhich::root_directories().await?;
- let resolver = kpsewhich::parse_database(&root_directories, read_database).await?;
- *self.resolver.lock().await = Arc::new(resolver);
- Ok(())
- }
-
- async fn resolver(&self) -> Arc<Resolver> {
- let resolver = self.resolver.lock().await;
- Arc::clone(&resolver)
- }
-}
-
-const DATABASE_PATH: &str = "ls-R";
-
-async fn read_database(directory: &Path) -> Result<Vec<PathBuf>, KpsewhichError> {
- let file = directory.join(DATABASE_PATH);
- if !file.is_file() {
- return Ok(Vec::new());
- }
-
- let text = fs::read_to_string(file)
- .await
- .map_err(|_| KpsewhichError::NoDatabase)?;
- parse_database(text.lines()).map_err(|_| KpsewhichError::CorruptDatabase)
-}
-
-fn parse_database(lines: Lines) -> io::Result<Vec<PathBuf>> {
- let mut paths = Vec::new();
- let mut directory = "";
-
- for line in lines.filter(|x| !x.trim().is_empty() && !x.starts_with('%')) {
- if line.ends_with(':') {
- directory = &line[..line.len() - 1];
- } else {
- let path = PathBuf::from(directory).join(line);
- paths.push(path);
- }
- }
- Ok(paths)
-}
diff --git a/support/texlab/src/protocol/uri.rs b/support/texlab/src/uri.rs
index e8dd1ba160..b1ae9073e7 100644
--- a/support/texlab/src/protocol/uri.rs
+++ b/support/texlab/src/uri.rs
@@ -1,11 +1,11 @@
-use lsp_types::{TextDocumentIdentifier, TextDocumentPositionParams};
-use serde::{Deserialize, Serialize};
use std::{
fmt,
hash::{Hash, Hasher},
ops::Deref,
path::Path,
};
+
+use serde::{Deserialize, Serialize};
use url::{ParseError, Url};
#[derive(Eq, Clone, Serialize, Deserialize)]
@@ -27,6 +27,10 @@ impl Uri {
Url::parse(input).map(|url| url.into())
}
+ pub fn from_directory_path<P: AsRef<Path>>(path: P) -> Result<Self, ()> {
+ Url::from_directory_path(path).map(|url| url.into())
+ }
+
pub fn from_file_path<P: AsRef<Path>>(path: P) -> Result<Self, ()> {
Url::from_file_path(path).map(|url| url.into())
}
@@ -79,19 +83,3 @@ impl fmt::Display for Uri {
self.0.fmt(f)
}
}
-
-pub trait AsUri {
- fn as_uri(&self) -> Uri;
-}
-
-impl AsUri for TextDocumentIdentifier {
- fn as_uri(&self) -> Uri {
- self.uri.clone().into()
- }
-}
-
-impl AsUri for TextDocumentPositionParams {
- fn as_uri(&self) -> Uri {
- self.text_document.as_uri()
- }
-}
diff --git a/support/texlab/src/workspace.rs b/support/texlab/src/workspace.rs
index c3c2551325..12924e2cf4 100644
--- a/support/texlab/src/workspace.rs
+++ b/support/texlab/src/workspace.rs
@@ -1,964 +1,32 @@
-use crate::{
- components::COMPONENT_DATABASE,
- protocol::{Options, TextDocumentItem, Uri},
- syntax::{bibtex, latex, LatexIncludeKind},
- tex::{Distribution, Language, Resolver},
-};
-use futures::lock::Mutex;
-use log::{debug, error, warn};
-use petgraph::{graph::Graph, visit::Dfs};
-use std::{
- collections::HashMap,
- ffi::OsStr,
- hash::{Hash, Hasher},
- io,
- path::{Path, PathBuf},
- sync::Arc,
- time::SystemTime,
-};
-use thiserror::Error;
-use tokio::fs;
-
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub struct DocumentParams<'a> {
- pub uri: Uri,
- pub text: String,
- pub language: Language,
- pub resolver: &'a Resolver,
- pub options: &'a Options,
- pub current_dir: &'a Path,
-}
-
-#[derive(Debug, Clone)]
-pub enum DocumentContent {
- Latex(Box<latex::SymbolTable>),
- Bibtex(Box<bibtex::Tree>),
-}
-
-impl DocumentContent {
- pub fn as_latex(&self) -> Option<&latex::SymbolTable> {
- if let Self::Latex(table) = self {
- Some(&table)
- } else {
- None
- }
- }
-
- pub fn as_bibtex(&self) -> Option<&bibtex::Tree> {
- if let Self::Bibtex(tree) = self {
- Some(&tree)
- } else {
- None
- }
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct Document {
- pub uri: Uri,
- pub text: String,
- pub content: DocumentContent,
- pub modified: SystemTime,
-}
-
-impl Document {
- pub fn is_file(&self) -> bool {
- self.uri.scheme() == "file"
- }
-
- pub fn open(params: DocumentParams) -> Self {
- let DocumentParams {
- uri,
- text,
- language,
- resolver,
- options,
- current_dir,
- } = params;
-
- let content = match language {
- Language::Latex => {
- let table = latex::open(latex::OpenParams {
- uri: &uri,
- text: &text,
- resolver,
- options,
- current_dir,
- });
- DocumentContent::Latex(Box::new(table))
- }
- Language::Bibtex => {
- let tree = bibtex::open(&text);
- DocumentContent::Bibtex(Box::new(tree))
- }
- };
-
- Self {
- uri,
- text,
- content,
- modified: SystemTime::now(),
- }
- }
-}
-
-impl PartialEq for Document {
- fn eq(&self, other: &Self) -> bool {
- self.uri == other.uri
- }
-}
-
-impl Eq for Document {}
-
-impl Hash for Document {
- fn hash<H: Hasher>(&self, state: &mut H) {
- self.uri.hash(state);
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Default)]
-pub struct Snapshot(pub Vec<Arc<Document>>);
-
-impl Snapshot {
- pub fn new() -> Self {
- Self(Vec::new())
- }
-
- pub fn push(&mut self, doc: Document) {
- self.0.push(Arc::new(doc));
- }
-
- pub fn find(&self, uri: &Uri) -> Option<Arc<Document>> {
- self.0.iter().find(|doc| doc.uri == *uri).map(Arc::clone)
- }
-
- pub fn relations(
- &self,
- uri: &Uri,
- options: &Options,
- current_dir: &Path,
- ) -> Vec<Arc<Document>> {
- let mut graph = Graph::new_undirected();
- let mut indices_by_uri = HashMap::new();
- for document in &self.0 {
- indices_by_uri.insert(&document.uri, graph.add_node(document));
- }
-
- for parent in &self.0 {
- if let DocumentContent::Latex(table) = &parent.content {
- table
- .includes
- .iter()
- .flat_map(|include| include.all_targets.iter())
- .filter_map(|targets| targets.iter().find_map(|target| self.find(target)))
- .for_each(|child| {
- graph.add_edge(indices_by_uri[&parent.uri], indices_by_uri[&child.uri], ());
- });
-
- table
- .imports
- .iter()
- .flat_map(|import| import.targets.iter())
- .find_map(|target| self.find(target))
- .into_iter()
- .for_each(|child| {
- graph.add_edge(indices_by_uri[&parent.uri], indices_by_uri[&child.uri], ());
- });
-
- self.resolve_aux_targets(&parent.uri, options, current_dir, "aux")
- .into_iter()
- .flatten()
- .find_map(|target| self.find(&target))
- .into_iter()
- .for_each(|child| {
- graph.add_edge(indices_by_uri[&parent.uri], indices_by_uri[&child.uri], ());
- });
- }
- }
-
- let mut documents = Vec::new();
- if self.find(uri).is_some() {
- let mut dfs = Dfs::new(&graph, indices_by_uri[uri]);
- while let Some(index) = dfs.next(&graph) {
- documents.push(Arc::clone(&graph[index]));
- }
- }
- documents
- }
-
- pub fn parent(
- &self,
- uri: &Uri,
- options: &Options,
- current_dir: &Path,
- ) -> Option<Arc<Document>> {
- for doc in self.relations(uri, options, current_dir) {
- if let DocumentContent::Latex(table) = &doc.content {
- if table.is_standalone {
- return Some(doc);
- }
- }
- }
- None
- }
-
- pub fn parent_subfile(
- &self,
- uri: &Uri,
- options: &Options,
- current_dir: &Path,
- ) -> Option<Arc<Document>> {
- for doc in self.relations(uri, options, current_dir) {
- if let DocumentContent::Latex(table) = &doc.content {
- if table.is_standalone
- && !table.components.iter().any(|comp| comp == "subfiles.cls")
- {
- return Some(doc);
- }
- }
- }
- None
- }
-
- pub fn expand(&self, options: &Options, current_dir: &Path) -> Vec<Uri> {
- let mut unknown_targets = Vec::new();
- for parent in &self.0 {
- if let DocumentContent::Latex(table) = &parent.content {
- table
- .includes
- .iter()
- .filter(|include| Self::should_expand_include(&table, include))
- .flat_map(|include| include.all_targets.iter())
- .filter(|targets| targets.iter().all(|target| self.find(target).is_none()))
- .flatten()
- .for_each(|target| unknown_targets.push(target.clone()));
-
- table
- .imports
- .iter()
- .filter(|import| {
- import
- .targets
- .iter()
- .all(|target| self.find(target).is_none())
- })
- .flat_map(|import| import.targets.iter())
- .for_each(|target| unknown_targets.push(target.clone()));
-
- self.resolve_aux_targets(&parent.uri, options, current_dir, "aux")
- .into_iter()
- .filter(|targets| targets.iter().all(|target| self.find(target).is_none()))
- .flatten()
- .for_each(|target| unknown_targets.push(target));
- }
- }
- unknown_targets
- }
-
- fn should_expand_include(tree: &latex::Tree, include: &latex::Include) -> bool {
- match include.kind {
- LatexIncludeKind::Bibliography | LatexIncludeKind::Latex => true,
- LatexIncludeKind::Everything
- | LatexIncludeKind::Image
- | LatexIncludeKind::Pdf
- | LatexIncludeKind::Svg => false,
- LatexIncludeKind::Package | LatexIncludeKind::Class => !include
- .paths(tree)
- .into_iter()
- .all(|name| COMPONENT_DATABASE.contains(name.text())),
- }
- }
-
- pub fn resolve_aux_targets(
- &self,
- tex_uri: &Uri,
- options: &Options,
- current_dir: &Path,
- extension: &str,
- ) -> Option<Vec<Uri>> {
- let mut targets = Vec::new();
- targets.push(tex_uri.with_extension(extension)?);
- if tex_uri.scheme() == "file" {
- let tex_path = tex_uri.to_file_path().ok()?;
- let file_stem = tex_path.file_stem()?;
- let aux_name = format!("{}.{}", file_stem.to_str()?, extension);
-
- if let Some(root_dir) = options
- .latex
- .as_ref()
- .and_then(|opts| opts.root_directory.as_ref())
- {
- let path = current_dir.join(root_dir).join(&aux_name);
- targets.push(Uri::from_file_path(path).ok()?);
- }
-
- if let Some(build_dir) = options
- .latex
- .as_ref()
- .and_then(|opts| opts.build.as_ref())
- .and_then(|opts| opts.output_directory.as_ref())
- {
- let path = current_dir.join(build_dir).join(&aux_name);
- targets.push(Uri::from_file_path(path).ok()?);
- }
- }
- Some(targets)
- }
-}
-
-#[derive(Debug, Error)]
-pub enum WorkspaceLoadError {
- #[error("invalid language id")]
- UnknownLanguage,
- #[error("invalid file path")]
- InvalidPath,
- #[error("an I/O error occurred: `{0}`")]
- IO(#[from] io::Error),
-}
-
-pub struct Workspace {
- distro: Arc<dyn Distribution>,
- current_dir: Arc<PathBuf>,
- snapshot: Mutex<Arc<Snapshot>>,
-}
-
-impl Workspace {
- pub fn new(distro: Arc<dyn Distribution>, current_dir: Arc<PathBuf>) -> Self {
- Self {
- distro,
- current_dir,
- snapshot: Mutex::default(),
- }
- }
-
- pub async fn get(&self) -> Arc<Snapshot> {
- let snapshot = self.snapshot.lock().await;
- Arc::clone(&snapshot)
- }
-
- pub async fn add(&self, document: TextDocumentItem, options: &Options) {
- let language = match Language::by_language_id(&document.language_id) {
- Some(language) => language,
- None => {
- error!(
- "Invalid language id: {} ({})",
- &document.language_id, &document.uri,
- );
- return;
- }
- };
-
- debug!("Adding document: {}", document.uri);
- let mut snapshot = self.snapshot.lock().await;
- *snapshot = self
- .add_or_update(
- &snapshot,
- document.uri.into(),
- document.text,
- language,
- options,
- )
- .await;
- }
-
- pub async fn load(&self, path: &Path, options: &Options) -> Result<(), WorkspaceLoadError> {
- let language = match path
- .extension()
- .and_then(OsStr::to_str)
- .and_then(Language::by_extension)
- {
- Some(language) => language,
- None => {
- warn!("Could not determine language: {}", path.to_string_lossy());
- return Err(WorkspaceLoadError::UnknownLanguage);
- }
- };
-
- let uri = match Uri::from_file_path(path) {
- Ok(uri) => uri,
- Err(_) => {
- error!("Invalid path: {}", path.to_string_lossy());
- return Err(WorkspaceLoadError::InvalidPath);
- }
- };
-
- let text = match fs::read_to_string(path).await {
- Ok(text) => text,
- Err(why) => {
- warn!("Could not open file: {}", uri);
- return Err(WorkspaceLoadError::IO(why));
- }
- };
+mod api;
+mod children_expand;
+mod document;
+mod parent_expand;
+mod storage;
+mod watch;
- debug!("Loading document: {}", uri);
- let mut snapshot = self.snapshot.lock().await;
- *snapshot = self
- .add_or_update(&snapshot, uri, text, language, options)
- .await;
- Ok(())
- }
+use std::sync::Arc;
- pub async fn update(&self, uri: Uri, text: String, options: &Options) {
- let mut snapshot = self.snapshot.lock().await;
+use anyhow::Result;
- let old_document = match snapshot.0.iter().find(|x| x.uri == uri) {
- Some(document) => document,
- None => {
- warn!("Document not found: {}", uri);
- return;
- }
- };
+use crate::ServerContext;
- let language = match old_document.content {
- DocumentContent::Latex(_) => Language::Latex,
- DocumentContent::Bibtex(_) => Language::Bibtex,
- };
-
- *snapshot = self
- .add_or_update(&snapshot, uri, text, language, options)
- .await;
- }
-
- pub async fn reparse(&self, options: &Options) {
- let snapshot = self.get().await;
- for doc in &snapshot.0 {
- let language = match doc.content {
- DocumentContent::Latex(_) => Language::Latex,
- DocumentContent::Bibtex(_) => Language::Bibtex,
- };
-
- let mut snapshot = self.snapshot.lock().await;
- debug!("Reparsing document: {}", doc.uri);
- *snapshot = self
- .add_or_update(
- &snapshot,
- doc.uri.clone(),
- doc.text.clone(),
- language,
- options,
- )
- .await;
- }
- }
-
- pub async fn detect_root(&self, uri: &Uri, options: &Options) -> io::Result<()> {
- if uri.scheme() != "file" {
- return Ok(());
- }
-
- if let Ok(mut path) = uri.to_file_path() {
- while path.pop() {
- let snapshot = self.get().await;
- if snapshot
- .parent_subfile(&uri, &options, &self.current_dir)
- .is_some()
- {
- break;
- }
-
- let mut entries = fs::read_dir(&path).await?;
- while let Some(entry) = entries.next_entry().await? {
- if entry.file_type().await?.is_file() {
- let path = entry.path();
- if path
- .extension()
- .and_then(OsStr::to_str)
- .and_then(Language::by_extension)
- .is_some()
- {
- if let Ok(parent_uri) = Uri::from_file_path(&path) {
- if snapshot.find(&parent_uri).is_none() {
- let _ = self.load(&path, options).await;
- }
- }
- }
- }
- }
- }
- }
- Ok(())
- }
-
- pub async fn detect_children(&self, options: &Options) {
- loop {
- let mut changed = false;
-
- let snapshot = self.get().await;
- for path in snapshot
- .expand(&options, &self.current_dir)
- .into_iter()
- .filter(|uri| uri.scheme() == "file" && uri.fragment().is_none())
- .filter_map(|uri| uri.to_file_path().ok())
- {
- if path.exists() {
- changed |= self.load(&path, &options).await.is_ok();
- }
- }
-
- if !changed {
- break;
- }
- }
- }
-
- pub async fn reparse_all_if_newer(&self, options: &Options) {
- let snapshot = self.get().await;
- for doc in &snapshot.0 {
- if let Err(WorkspaceLoadError::IO(why)) = self.reparse_if_newer(doc, options).await {
- warn!("Reparsing document {} failed: {}", doc.uri, why);
- }
- }
- }
-
- async fn reparse_if_newer(
- &self,
- doc: &Document,
- options: &Options,
- ) -> Result<(), WorkspaceLoadError> {
- if !doc.is_file() {
- return Ok(());
- }
-
- if let Ok(path) = doc.uri.to_file_path() {
- let data = fs::metadata(&path).await?;
- if data.modified()? > doc.modified {
- self.load(&path, options).await?;
- }
- }
- Ok(())
- }
-
- async fn add_or_update(
- &self,
- snapshot: &Snapshot,
- uri: Uri,
- text: String,
- language: Language,
- options: &Options,
- ) -> Arc<Snapshot> {
- let resolver = self.distro.resolver().await;
- let document = Document::open(DocumentParams {
- uri,
- text,
- language,
- resolver: &resolver,
- options,
- current_dir: &self.current_dir,
- });
-
- let mut documents: Vec<Arc<Document>> = snapshot
- .0
- .iter()
- .filter(|x| x.uri != document.uri)
- .cloned()
- .collect();
+pub use self::{api::*, document::*};
+use self::{
+ children_expand::ChildrenExpander, parent_expand::ParentExpander, storage::Storage,
+ watch::DocumentWatcher,
+};
- documents.push(Arc::new(document));
- Arc::new(Snapshot(documents))
- }
+pub fn create_workspace_fast(context: Arc<ServerContext>) -> Result<impl Workspace> {
+ let workspace = Storage::new(context);
+ let workspace = ChildrenExpander::new(Arc::new(workspace));
+ Ok(workspace)
}
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::protocol::{LatexBuildOptions, LatexOptions};
- use itertools::Itertools;
- use std::env;
-
- fn create_simple_document(uri: &Uri, language: Language, text: &str) -> Arc<Document> {
- Arc::new(Document::open(DocumentParams {
- uri: uri.clone(),
- text: text.into(),
- language,
- resolver: &Resolver::default(),
- options: &Options::default(),
- current_dir: &env::current_dir().unwrap(),
- }))
- }
-
- #[test]
- fn relations_append_missing_extension() {
- let uri1 = Uri::parse("http://www.example.com/foo.tex").unwrap();
- let uri2 = Uri::parse("http://www.example.com/bar/baz.tex").unwrap();
- let mut snapshot = Snapshot::new();
- snapshot.0 = vec![
- create_simple_document(&uri1, Language::Latex, r#"\include{bar/baz}"#),
- create_simple_document(&uri2, Language::Latex, r#""#),
- ];
- let actual_uris: Vec<_> = snapshot
- .relations(&uri1, &Options::default(), &env::current_dir().unwrap())
- .into_iter()
- .map(|doc| doc.uri.clone())
- .collect();
-
- assert_eq!(actual_uris, vec![uri1, uri2]);
- }
-
- #[test]
- fn relations_parent_directory() {
- let uri1 = Uri::parse("http://www.example.com/foo.tex").unwrap();
- let uri2 = Uri::parse("http://www.example.com/bar/baz.tex").unwrap();
- let mut snapshot = Snapshot::new();
- snapshot.0 = vec![
- create_simple_document(&uri1, Language::Latex, r#""#),
- create_simple_document(&uri2, Language::Latex, r#"\input{../foo.tex}"#),
- ];
- let actual_uris: Vec<_> = snapshot
- .relations(&uri1, &Options::default(), &env::current_dir().unwrap())
- .into_iter()
- .map(|doc| doc.uri.clone())
- .collect();
-
- assert_eq!(actual_uris, vec![uri1, uri2]);
- }
-
- #[test]
- fn relations_invalid_include() {
- let uri = Uri::parse("http://www.example.com/foo.tex").unwrap();
- let mut snapshot = Snapshot::new();
- snapshot.0 = vec![create_simple_document(
- &uri,
- Language::Latex,
- r#"\include{<foo>?|bar|:}"#,
- )];
- let actual_uris: Vec<_> = snapshot
- .relations(&uri, &Options::default(), &env::current_dir().unwrap())
- .into_iter()
- .map(|doc| doc.uri.clone())
- .collect();
-
- assert_eq!(actual_uris, vec![uri]);
- }
-
- #[test]
- fn relations_bibliography() {
- let uri1 = Uri::parse("http://www.example.com/foo.tex").unwrap();
- let uri2 = Uri::parse("http://www.example.com/bar.bib").unwrap();
- let mut snapshot = Snapshot::new();
- snapshot.0 = vec![
- create_simple_document(&uri1, Language::Latex, r#"\addbibresource{bar.bib}"#),
- create_simple_document(&uri2, Language::Bibtex, r#""#),
- ];
- let actual_uris: Vec<_> = snapshot
- .relations(&uri2, &Options::default(), &env::current_dir().unwrap())
- .into_iter()
- .map(|doc| doc.uri.clone())
- .collect();
-
- assert_eq!(actual_uris, vec![uri2, uri1]);
- }
-
- #[test]
- fn relations_unknown_include() {
- let uri = Uri::parse("http://www.example.com/foo.tex").unwrap();
- let mut snapshot = Snapshot::new();
- snapshot.0 = vec![create_simple_document(
- &uri,
- Language::Latex,
- r#"\input{bar.tex}"#,
- )];
- let actual_uris: Vec<_> = snapshot
- .relations(&uri, &Options::default(), &env::current_dir().unwrap())
- .into_iter()
- .map(|doc| doc.uri.clone())
- .collect();
-
- assert_eq!(actual_uris, vec![uri]);
- }
-
- #[test]
- fn relations_include_cycle() {
- let uri1 = Uri::parse("http://www.example.com/foo.tex").unwrap();
- let uri2 = Uri::parse("http://www.example.com/bar.tex").unwrap();
- let mut snapshot = Snapshot::new();
- snapshot.0 = vec![
- create_simple_document(&uri1, Language::Latex, r#"\include{bar}"#),
- create_simple_document(&uri2, Language::Latex, r#"\input{foo.tex}"#),
- ];
- let actual_uris: Vec<_> = snapshot
- .relations(&uri1, &Options::default(), &env::current_dir().unwrap())
- .into_iter()
- .map(|doc| doc.uri.clone())
- .collect();
-
- assert_eq!(actual_uris, vec![uri1, uri2]);
- }
-
- #[test]
- fn relations_same_parent() {
- let uri1 = Uri::parse("http://www.example.com/foo.tex").unwrap();
- let uri2 = Uri::parse("http://www.example.com/bar.tex").unwrap();
- let uri3 = Uri::parse("http://www.example.com/baz.tex").unwrap();
- let mut snapshot = Snapshot::new();
- snapshot.0 = vec![
- create_simple_document(&uri1, Language::Latex, r#"\input{bar.tex}\input{baz.tex}"#),
- create_simple_document(&uri2, Language::Latex, r#""#),
- create_simple_document(&uri3, Language::Latex, r#""#),
- ];
- let actual_uris: Vec<_> = snapshot
- .relations(&uri3, &Options::default(), &env::current_dir().unwrap())
- .into_iter()
- .map(|doc| doc.uri.clone())
- .collect();
-
- assert_eq!(actual_uris, vec![uri3, uri1, uri2]);
- }
-
- #[test]
- fn relations_aux_default_options() {
- let uri1 = Uri::parse("http://www.example.com/foo.tex").unwrap();
- let uri2 = Uri::parse("http://www.example.com/foo.aux").unwrap();
- let mut snapshot = Snapshot::new();
- snapshot.0 = vec![
- create_simple_document(&uri1, Language::Latex, r#""#),
- create_simple_document(&uri2, Language::Latex, r#""#),
- ];
- let actual_uris: Vec<_> = snapshot
- .relations(&uri1, &Options::default(), &env::current_dir().unwrap())
- .into_iter()
- .map(|doc| doc.uri.clone())
- .collect();
-
- assert_eq!(actual_uris, vec![uri1, uri2]);
- }
-
- #[test]
- fn relations_aux_output_directory() {
- let cwd = env::current_dir().unwrap();
- let options = Options {
- latex: Some(LatexOptions {
- build: Some(LatexBuildOptions {
- output_directory: Some(PathBuf::from("build")),
- ..LatexBuildOptions::default()
- }),
- ..LatexOptions::default()
- }),
- ..Options::default()
- };
-
- let uri1 = Uri::from_file_path(cwd.join("foo.tex")).unwrap();
- let uri2 = Uri::from_file_path(cwd.join("build/foo.aux")).unwrap();
- let mut snapshot = Snapshot::new();
- snapshot.0 = vec![
- Arc::new(Document::open(DocumentParams {
- uri: uri1.clone(),
- text: String::new(),
- language: Language::Latex,
- resolver: &Resolver::default(),
- options: &options,
- current_dir: &cwd,
- })),
- Arc::new(Document::open(DocumentParams {
- uri: uri2.clone(),
- text: String::new(),
- language: Language::Latex,
- resolver: &Resolver::default(),
- options: &options,
- current_dir: &cwd,
- })),
- ];
- let actual_uris: Vec<_> = snapshot
- .relations(&uri1, &options, &cwd)
- .into_iter()
- .map(|doc| doc.uri.clone())
- .collect();
-
- assert_eq!(actual_uris, vec![uri1, uri2]);
- }
-
- #[test]
- fn relations_aux_root_directory() {
- let cwd = env::current_dir().unwrap();
- let options = Options {
- latex: Some(LatexOptions {
- root_directory: Some(PathBuf::from(".")),
- ..LatexOptions::default()
- }),
- ..Options::default()
- };
-
- let uri1 = Uri::from_file_path(cwd.join("src/foo.tex")).unwrap();
- let uri2 = Uri::from_file_path(cwd.join("foo.aux")).unwrap();
- let mut snapshot = Snapshot::new();
- snapshot.0 = vec![
- Arc::new(Document::open(DocumentParams {
- uri: uri1.clone(),
- text: String::new(),
- language: Language::Latex,
- resolver: &Resolver::default(),
- options: &options,
- current_dir: &cwd,
- })),
- Arc::new(Document::open(DocumentParams {
- uri: uri2.clone(),
- text: String::new(),
- language: Language::Latex,
- resolver: &Resolver::default(),
- options: &options,
- current_dir: &cwd,
- })),
- ];
- let actual_uris: Vec<_> = snapshot
- .relations(&uri1, &options, &cwd)
- .into_iter()
- .map(|doc| doc.uri.clone())
- .collect();
-
- assert_eq!(actual_uris, vec![uri1, uri2]);
- }
-
- #[test]
- fn relations_import() {
- let uri1 = Uri::parse("http://www.example.com/foo.tex").unwrap();
- let uri2 = Uri::parse("http://www.example.com/bar/baz.tex").unwrap();
- let uri3 = Uri::parse("http://www.example.com/bar/qux/foo-bar.tex").unwrap();
- let mut snapshot = Snapshot::new();
- snapshot.0 = vec![
- create_simple_document(&uri1, Language::Latex, r#"\import{bar/}{baz.tex}"#),
- create_simple_document(&uri2, Language::Latex, r#"\subimport{qux/}{foo-bar}"#),
- create_simple_document(&uri3, Language::Latex, r#""#),
- ];
-
- let actual_uris: Vec<_> = snapshot
- .relations(&uri1, &Options::default(), &env::current_dir().unwrap())
- .into_iter()
- .map(|doc| doc.uri.clone())
- .collect();
-
- assert_eq!(actual_uris, vec![uri1, uri2, uri3]);
- }
-
- #[test]
- fn parent() {
- let uri1 = Uri::parse("http://www.example.com/foo.tex").unwrap();
- let uri2 = Uri::parse("http://www.example.com/bar.tex").unwrap();
- let mut snapshot = Snapshot::new();
- snapshot.0 = vec![
- create_simple_document(&uri1, Language::Latex, r#""#),
- create_simple_document(
- &uri2,
- Language::Latex,
- r#"\begin{document}\include{foo}\end{document}"#,
- ),
- ];
- let doc = snapshot
- .parent(&uri1, &Options::default(), &env::current_dir().unwrap())
- .unwrap();
- assert_eq!(doc.uri, uri2);
- }
-
- #[test]
- fn parent_nothing_found() {
- let uri1 = Uri::parse("http://www.example.com/foo.tex").unwrap();
- let uri2 = Uri::parse("http://www.example.com/bar.tex").unwrap();
- let mut snapshot = Snapshot::new();
- snapshot.0 = vec![
- create_simple_document(&uri1, Language::Latex, r#""#),
- create_simple_document(&uri2, Language::Latex, r#"\begin{document}\end{document}"#),
- ];
- let doc = snapshot.parent(&uri1, &Options::default(), &env::current_dir().unwrap());
- assert_eq!(doc, None);
- }
-
- #[test]
- fn expand_aux_file() {
- let uri = Uri::parse("http://www.example.com/foo.tex").unwrap();
- let mut snapshot = Snapshot::new();
- snapshot.0 = vec![create_simple_document(&uri, Language::Latex, r#""#)];
- let expansion = snapshot.expand(&Options::default(), &env::current_dir().unwrap());
- assert_eq!(
- expansion
- .iter()
- .map(|uri| uri.as_str())
- .filter(|uri| uri.ends_with(".aux"))
- .collect_vec(),
- vec!["http://www.example.com/foo.aux"]
- );
- }
-
- #[test]
- fn expand_local_package() {
- let uri = Uri::parse("http://www.example.com/foo.tex").unwrap();
- let mut snapshot = Snapshot::new();
- snapshot.0 = vec![create_simple_document(
- &uri,
- Language::Latex,
- r#"\usepackage{foo-bar-baz}"#,
- )];
- let expansion = snapshot.expand(&Options::default(), &env::current_dir().unwrap());
-
- assert_eq!(
- expansion
- .iter()
- .map(|uri| uri.as_str())
- .filter(|uri| uri.ends_with(".sty"))
- .collect_vec(),
- vec!["http://www.example.com/foo-bar-baz.sty"]
- );
- }
-
- #[test]
- fn expand_system_package() {
- let uri = Uri::parse("http://www.example.com/foo.tex").unwrap();
- let mut snapshot = Snapshot::new();
- snapshot.0 = vec![create_simple_document(
- &uri,
- Language::Latex,
- r#"\usepackage{amsmath}"#,
- )];
- let expansion = snapshot.expand(&Options::default(), &env::current_dir().unwrap());
-
- assert_eq!(
- expansion
- .iter()
- .map(|uri| uri.as_str())
- .filter(|uri| uri.ends_with(".sty"))
- .collect_vec(),
- Vec::<&str>::new()
- );
- }
-
- #[test]
- fn expand_subdirectory() {
- let uri = Uri::parse("http://www.example.com/foo.tex").unwrap();
- let mut snapshot = Snapshot::new();
- snapshot.0 = vec![create_simple_document(
- &uri,
- Language::Latex,
- r#"\include{bar/baz}"#,
- )];
- let expansion = snapshot.expand(&Options::default(), &env::current_dir().unwrap());
- assert_eq!(
- expansion
- .iter()
- .map(|uri| uri.as_str())
- .filter(|uri| uri.ends_with(".tex"))
- .collect_vec(),
- vec!["http://www.example.com/bar/baz.tex"]
- );
- }
-
- #[test]
- fn expand_import() {
- let uri1 = Uri::parse("http://www.example.com/qux/foo.tex").unwrap();
- let uri2 = Uri::parse("http://www.example.com/qux/baz/bar.tex").unwrap();
- let mut snapshot = Snapshot::new();
- snapshot.0 = vec![
- create_simple_document(
- &uri1,
- Language::Latex,
- r#"\import{.}{foo}\import{baz/}{bar}\import{baz/foo-bar/}{qux}"#,
- ),
- create_simple_document(&uri2, Language::Latex, r#""#),
- ];
- let expansion = snapshot.expand(&Options::default(), &env::current_dir().unwrap());
- assert_eq!(
- expansion
- .iter()
- .map(|uri| uri.as_str())
- .filter(|uri| uri.ends_with(".tex"))
- .collect_vec(),
- vec!["http://www.example.com/qux/baz/foo-bar/qux.tex"]
- )
- }
+pub fn create_workspace_full(context: Arc<ServerContext>) -> Result<impl Workspace> {
+ let workspace = Storage::new(context);
+ let workspace = DocumentWatcher::new(Arc::new(workspace))?;
+ let workspace = ParentExpander::new(workspace);
+ let workspace = ChildrenExpander::new(Arc::new(workspace));
+ Ok(workspace)
}
diff --git a/support/texlab/src/workspace/api.rs b/support/texlab/src/workspace/api.rs
new file mode 100644
index 0000000000..38fa0ce530
--- /dev/null
+++ b/support/texlab/src/workspace/api.rs
@@ -0,0 +1,86 @@
+use std::{fs, path::PathBuf, sync::Arc};
+
+use anyhow::Result;
+
+use crate::{DocumentLanguage, Uri};
+
+use super::Document;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
+pub enum WorkspaceSource {
+ Client,
+ Server,
+}
+
+#[derive(Debug, Clone)]
+pub struct WorkspaceSubset {
+ pub documents: Vec<Arc<Document>>,
+}
+
+pub type OpenHandler = Arc<dyn Fn(Arc<dyn Workspace>, Arc<Document>) + Send + Sync + 'static>;
+
+pub trait Workspace: Send + Sync {
+ fn open(
+ &self,
+ uri: Arc<Uri>,
+ text: String,
+ language: DocumentLanguage,
+ source: WorkspaceSource,
+ ) -> Arc<Document>;
+
+ fn register_open_handler(&self, handler: OpenHandler);
+
+ fn reload(&self, path: PathBuf) -> Result<Option<Arc<Document>>> {
+ let uri = Arc::new(Uri::from_file_path(path.clone()).unwrap());
+
+ if self.is_open(&uri) {
+ return Ok(self.get(&uri));
+ }
+
+ let data = fs::read(&path)?;
+ let text = String::from_utf8_lossy(&data).into_owned();
+ if let Some(language) = DocumentLanguage::by_path(&path) {
+ Ok(Some(self.open(
+ uri,
+ text,
+ language,
+ WorkspaceSource::Server,
+ )))
+ } else {
+ Ok(None)
+ }
+ }
+
+ fn load(&self, path: PathBuf) -> Result<Option<Arc<Document>>> {
+ let uri = Arc::new(Uri::from_file_path(path.clone()).unwrap());
+
+ if let Some(document) = self.get(&uri) {
+ return Ok(Some(document));
+ }
+
+ let data = fs::read(&path)?;
+ let text = String::from_utf8_lossy(&data).into_owned();
+ if let Some(language) = DocumentLanguage::by_path(&path) {
+ Ok(Some(self.open(
+ uri,
+ text,
+ language,
+ WorkspaceSource::Server,
+ )))
+ } else {
+ Ok(None)
+ }
+ }
+
+ fn documents(&self) -> Vec<Arc<Document>>;
+
+ fn has(&self, uri: &Uri) -> bool;
+
+ fn get(&self, uri: &Uri) -> Option<Arc<Document>>;
+
+ fn close(&self, uri: &Uri);
+
+ fn is_open(&self, uri: &Uri) -> bool;
+
+ fn subset(&self, uri: Arc<Uri>) -> Option<WorkspaceSubset>;
+}
diff --git a/support/texlab/src/workspace/children_expand.rs b/support/texlab/src/workspace/children_expand.rs
new file mode 100644
index 0000000000..006e7b17e2
--- /dev/null
+++ b/support/texlab/src/workspace/children_expand.rs
@@ -0,0 +1,92 @@
+use std::sync::Arc;
+
+use rayon::iter::{IntoParallelIterator, ParallelIterator};
+
+use crate::{
+ component_db::COMPONENT_DATABASE, Document, DocumentLanguage, OpenHandler, Uri, Workspace,
+ WorkspaceSource, WorkspaceSubset,
+};
+
+pub struct ChildrenExpander<W> {
+ workspace: Arc<W>,
+}
+
+impl<W> ChildrenExpander<W>
+where
+ W: Workspace + Send + Sync + 'static,
+{
+ pub fn new(workspace: Arc<W>) -> Self {
+ workspace.register_open_handler(Arc::new(move |workspace, document| {
+ Self::expand(workspace.as_ref(), document.as_ref());
+ }));
+ Self { workspace }
+ }
+
+ fn expand(workspace: &dyn Workspace, document: &Document) {
+ if let Some(data) = document.data.as_latex() {
+ let extras = &data.extras;
+ let mut all_targets = vec![&extras.implicit_links.aux, &extras.implicit_links.log];
+ for link in &extras.explicit_links {
+ if link
+ .as_component_name()
+ .and_then(|name| COMPONENT_DATABASE.find(&name))
+ .is_none()
+ {
+ all_targets.push(&link.targets);
+ }
+ }
+
+ all_targets.into_par_iter().for_each(|targets| {
+ for path in targets
+ .iter()
+ .filter(|uri| uri.scheme() == "file" && uri.fragment().is_none())
+ .filter_map(|uri| uri.to_file_path().ok())
+ {
+ if workspace.load(path).is_ok() {
+ break;
+ }
+ }
+ });
+ }
+ }
+}
+
+impl<W: Workspace> Workspace for ChildrenExpander<W> {
+ fn open(
+ &self,
+ uri: Arc<Uri>,
+ text: String,
+ language: DocumentLanguage,
+ source: WorkspaceSource,
+ ) -> Arc<Document> {
+ self.workspace.open(uri, text, language, source)
+ }
+
+ fn register_open_handler(&self, handler: OpenHandler) {
+ self.workspace.register_open_handler(handler)
+ }
+
+ fn documents(&self) -> Vec<Arc<Document>> {
+ self.workspace.documents()
+ }
+
+ fn has(&self, uri: &Uri) -> bool {
+ self.workspace.has(uri)
+ }
+
+ fn get(&self, uri: &Uri) -> Option<Arc<Document>> {
+ self.workspace.get(uri)
+ }
+
+ fn close(&self, uri: &Uri) {
+ self.workspace.close(uri)
+ }
+
+ fn is_open(&self, uri: &Uri) -> bool {
+ self.workspace.is_open(uri)
+ }
+
+ fn subset(&self, uri: Arc<Uri>) -> Option<WorkspaceSubset> {
+ self.workspace.subset(uri)
+ }
+}
diff --git a/support/texlab/src/workspace/document.rs b/support/texlab/src/workspace/document.rs
new file mode 100644
index 0000000000..a98be226c1
--- /dev/null
+++ b/support/texlab/src/workspace/document.rs
@@ -0,0 +1,127 @@
+use std::{fmt, sync::Arc};
+
+use derive_more::From;
+
+use crate::{
+ line_index::LineIndex,
+ syntax::{
+ bibtex, build_log,
+ latex::{self, LatexAnalyzerContext},
+ },
+ DocumentLanguage, ServerContext, Uri,
+};
+
+#[derive(Debug, Clone)]
+pub struct LatexDocumentData {
+ pub root: latex::SyntaxNode,
+ pub extras: latex::Extras,
+}
+
+#[derive(Debug, Clone)]
+pub struct BibtexDocumentData {
+ pub root: bibtex::SyntaxNode,
+}
+
+#[derive(Debug, Clone, From)]
+pub enum DocumentData {
+ Latex(LatexDocumentData),
+ Bibtex(BibtexDocumentData),
+ BuildLog(build_log::Parse),
+}
+
+impl DocumentData {
+ pub fn language(&self) -> DocumentLanguage {
+ match self {
+ Self::Latex(_) => DocumentLanguage::Latex,
+ Self::Bibtex(_) => DocumentLanguage::Bibtex,
+ Self::BuildLog(_) => DocumentLanguage::BuildLog,
+ }
+ }
+
+ pub fn as_latex(&self) -> Option<&LatexDocumentData> {
+ if let Self::Latex(data) = self {
+ Some(data)
+ } else {
+ None
+ }
+ }
+
+ pub fn as_bibtex(&self) -> Option<&BibtexDocumentData> {
+ if let Self::Bibtex(data) = self {
+ Some(data)
+ } else {
+ None
+ }
+ }
+
+ pub fn as_build_log(&self) -> Option<&build_log::Parse> {
+ if let Self::BuildLog(v) = self {
+ Some(v)
+ } else {
+ None
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct Document {
+ pub uri: Arc<Uri>,
+ pub text: String,
+ pub line_index: LineIndex,
+ pub data: DocumentData,
+}
+
+impl fmt::Debug for Document {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.uri)
+ }
+}
+
+impl Document {
+ pub fn parse(
+ context: Arc<ServerContext>,
+ uri: Arc<Uri>,
+ text: String,
+ language: DocumentLanguage,
+ ) -> Self {
+ let line_index = LineIndex::new(&text);
+ let data = match language {
+ DocumentLanguage::Latex => {
+ let root = latex::parse(&text).root;
+
+ let base_uri = match &context.options.read().unwrap().root_directory {
+ Some(root_dir) => Uri::from_directory_path(root_dir)
+ .map(Arc::new)
+ .unwrap_or_else(|()| Arc::clone(&uri)),
+ None => Arc::clone(&uri),
+ };
+ let mut context = LatexAnalyzerContext {
+ inner: context,
+ extras: latex::Extras::default(),
+ document_uri: Arc::clone(&uri),
+ base_uri,
+ };
+ latex::analyze(&mut context, &root);
+ let extras = context.extras;
+
+ LatexDocumentData { root, extras }.into()
+ }
+ DocumentLanguage::Bibtex => {
+ let root = bibtex::parse(&text).root;
+ BibtexDocumentData { root }.into()
+ }
+ DocumentLanguage::BuildLog => DocumentData::BuildLog(build_log::parse(&text)),
+ };
+
+ Self {
+ uri,
+ text,
+ line_index,
+ data,
+ }
+ }
+
+ pub fn language(&self) -> DocumentLanguage {
+ self.data.language()
+ }
+}
diff --git a/support/texlab/src/workspace/parent_expand.rs b/support/texlab/src/workspace/parent_expand.rs
new file mode 100644
index 0000000000..33b0ca3c5e
--- /dev/null
+++ b/support/texlab/src/workspace/parent_expand.rs
@@ -0,0 +1,123 @@
+use std::{fs, sync::Arc};
+
+use rayon::iter::{IntoParallelIterator, ParallelIterator};
+use rustc_hash::FxHashSet;
+
+use crate::{
+ Document, DocumentLanguage, OpenHandler, Uri, Workspace, WorkspaceSource, WorkspaceSubset,
+};
+
+pub struct ParentExpander<W> {
+ workspace: W,
+}
+
+impl<W: Workspace> ParentExpander<W> {
+ pub fn new(workspace: W) -> Self {
+ Self { workspace }
+ }
+}
+
+impl<W> Workspace for ParentExpander<W>
+where
+ W: Workspace + Send + Sync + 'static,
+{
+ fn open(
+ &self,
+ uri: Arc<Uri>,
+ text: String,
+ language: DocumentLanguage,
+ source: WorkspaceSource,
+ ) -> Arc<Document> {
+ let document = self
+ .workspace
+ .open(Arc::clone(&uri), text, language, source);
+
+ let all_current_paths = self
+ .workspace
+ .documents()
+ .into_iter()
+ .filter_map(|doc| doc.uri.to_file_path().ok())
+ .collect::<FxHashSet<_>>();
+
+ if uri.scheme() == "file" {
+ if let Ok(mut path) = uri.to_file_path() {
+ while path.pop() && !self.has_parent(Arc::clone(&uri)).unwrap_or(false) {
+ let mut files = Vec::new();
+ fs::read_dir(&path)
+ .into_iter()
+ .flatten()
+ .filter_map(|entry| entry.ok())
+ .filter(|entry| entry.file_type().ok().filter(|ty| ty.is_file()).is_some())
+ .map(|entry| entry.path())
+ .filter(|path| {
+ matches!(
+ DocumentLanguage::by_path(&path),
+ Some(DocumentLanguage::Latex)
+ )
+ })
+ .filter(|path| !all_current_paths.contains(path))
+ .for_each(|path| {
+ files.push(path);
+ });
+ files.into_par_iter().for_each(|path| {
+ let _ = self.workspace.load(path);
+ });
+ }
+ }
+ }
+
+ document
+ }
+
+ fn register_open_handler(&self, handler: OpenHandler) {
+ self.workspace.register_open_handler(handler)
+ }
+
+ fn documents(&self) -> Vec<Arc<Document>> {
+ self.workspace.documents()
+ }
+
+ fn has(&self, uri: &Uri) -> bool {
+ self.workspace.has(uri)
+ }
+
+ fn get(&self, uri: &Uri) -> Option<Arc<Document>> {
+ self.workspace.get(uri)
+ }
+
+ fn close(&self, uri: &Uri) {
+ self.workspace.close(uri)
+ }
+
+ fn is_open(&self, uri: &Uri) -> bool {
+ self.workspace.is_open(uri)
+ }
+
+ fn subset(&self, uri: Arc<Uri>) -> Option<WorkspaceSubset> {
+ self.workspace.subset(uri)
+ }
+}
+
+impl<W> ParentExpander<W>
+where
+ W: Workspace + Send + Sync + 'static,
+{
+ fn has_parent(&self, uri: Arc<Uri>) -> Option<bool> {
+ let subset = self.subset(Arc::clone(&uri))?;
+ Some(subset.documents.iter().any(|document| {
+ document
+ .data
+ .as_latex()
+ .map(|data| {
+ data.extras.has_document_environment
+ && !data
+ .extras
+ .explicit_links
+ .iter()
+ .filter_map(|link| link.as_component_name())
+ .any(|name| name == "subfiles.cls")
+ })
+ .unwrap_or(false)
+ }))
+ }
+}
diff --git a/support/texlab/src/workspace/storage.rs b/support/texlab/src/workspace/storage.rs
new file mode 100644
index 0000000000..964535a718
--- /dev/null
+++ b/support/texlab/src/workspace/storage.rs
@@ -0,0 +1,136 @@
+use std::sync::{Arc, Mutex};
+
+use petgraph::{graphmap::UnGraphMap, visit::Dfs};
+use rustc_hash::{FxHashMap, FxHashSet};
+
+use crate::{
+ Document, DocumentLanguage, OpenHandler, ServerContext, Uri, Workspace, WorkspaceSource,
+ WorkspaceSubset,
+};
+
+#[derive(Clone)]
+pub struct Storage {
+ context: Arc<ServerContext>,
+ documents_by_uri: Arc<Mutex<FxHashMap<Arc<Uri>, Arc<Document>>>>,
+ opened_documents: Arc<Mutex<FxHashSet<Arc<Uri>>>>,
+ open_handlers: Arc<Mutex<Vec<OpenHandler>>>,
+}
+
+impl Storage {
+ pub fn new(context: Arc<ServerContext>) -> Self {
+ Self {
+ context,
+ documents_by_uri: Arc::default(),
+ opened_documents: Arc::default(),
+ open_handlers: Arc::default(),
+ }
+ }
+}
+
+impl Workspace for Storage {
+ fn open(
+ &self,
+ uri: Arc<Uri>,
+ text: String,
+ language: DocumentLanguage,
+ source: WorkspaceSource,
+ ) -> Arc<Document> {
+ log::debug!("(Re)Loading document: {}", uri);
+ let document = Arc::new(Document::parse(
+ Arc::clone(&self.context),
+ Arc::clone(&uri),
+ text,
+ language,
+ ));
+ {
+ self.documents_by_uri
+ .lock()
+ .unwrap()
+ .insert(Arc::clone(&uri), Arc::clone(&document));
+ }
+
+ if source == WorkspaceSource::Client {
+ self.opened_documents.lock().unwrap().insert(uri);
+ }
+
+ let handlers = { self.open_handlers.lock().unwrap().clone() };
+ for handler in handlers {
+ handler(Arc::new(self.clone()), Arc::clone(&document));
+ }
+
+ document
+ }
+
+ fn register_open_handler(&self, handler: OpenHandler) {
+ self.open_handlers.lock().unwrap().push(handler);
+ }
+
+ fn documents(&self) -> Vec<Arc<Document>> {
+ self.documents_by_uri
+ .lock()
+ .unwrap()
+ .values()
+ .cloned()
+ .collect()
+ }
+
+ fn has(&self, uri: &Uri) -> bool {
+ self.documents_by_uri.lock().unwrap().contains_key(uri)
+ }
+
+ fn get(&self, uri: &Uri) -> Option<Arc<Document>> {
+ self.documents_by_uri.lock().unwrap().get(uri).cloned()
+ }
+
+ fn close(&self, uri: &Uri) {
+ self.opened_documents.lock().unwrap().remove(uri);
+ }
+
+ fn is_open(&self, uri: &Uri) -> bool {
+ self.opened_documents.lock().unwrap().contains(uri)
+ }
+
+ fn subset(&self, uri: Arc<Uri>) -> Option<WorkspaceSubset> {
+ let all_current_uris: Vec<Arc<Uri>> = self
+ .documents_by_uri
+ .lock()
+ .unwrap()
+ .keys()
+ .cloned()
+ .collect();
+
+ let mut edges = Vec::new();
+ for (i, uri) in all_current_uris.iter().enumerate() {
+ let document = self.get(uri);
+ if let Some(data) = document
+ .as_ref()
+ .and_then(|document| document.data.as_latex())
+ {
+ let extras = &data.extras;
+ let mut all_targets = vec![&extras.implicit_links.aux, &extras.implicit_links.log];
+ for link in &extras.explicit_links {
+ all_targets.push(&link.targets);
+ }
+
+ for targets in all_targets {
+ for target in targets {
+ if let Some(j) = all_current_uris.iter().position(|uri| uri == target) {
+ edges.push((i, j, ()));
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ let graph = UnGraphMap::from_edges(edges);
+ let start = all_current_uris.iter().position(|u| *u == uri)?;
+ let mut dfs = Dfs::new(&graph, start);
+ let mut documents = Vec::new();
+ while let Some(i) = dfs.next(&graph) {
+ documents.push(self.get(&all_current_uris[i])?);
+ }
+
+ Some(WorkspaceSubset { documents })
+ }
+}
diff --git a/support/texlab/src/workspace/watch.rs b/support/texlab/src/workspace/watch.rs
new file mode 100644
index 0000000000..655872f603
--- /dev/null
+++ b/support/texlab/src/workspace/watch.rs
@@ -0,0 +1,107 @@
+use std::{
+ path::PathBuf,
+ sync::{Arc, Mutex},
+};
+
+use anyhow::Result;
+use log::warn;
+use notify::{RecommendedWatcher, RecursiveMode, Watcher};
+use rustc_hash::FxHashSet;
+
+use crate::{
+ Document, DocumentLanguage, OpenHandler, Uri, Workspace, WorkspaceSource, WorkspaceSubset,
+};
+
+pub struct DocumentWatcher<W> {
+ workspace: Arc<W>,
+ watcher: Mutex<RecommendedWatcher>,
+ watched_paths: Mutex<FxHashSet<PathBuf>>,
+}
+
+impl<W> DocumentWatcher<W>
+where
+ W: Workspace + Send + Sync + 'static,
+{
+ pub fn new(workspace: Arc<W>) -> Result<Self> {
+ let watcher = Self::create_watcher(Arc::clone(&workspace))?;
+ Ok(Self {
+ workspace,
+ watcher: Mutex::new(watcher),
+ watched_paths: Mutex::default(),
+ })
+ }
+
+ fn create_watcher(workspace: Arc<W>) -> Result<RecommendedWatcher> {
+ let watcher = Watcher::new_immediate(move |event: notify::Result<notify::Event>| {
+ if let Ok(event) = event {
+ if event.kind.is_modify() {
+ for path in event.paths {
+ let _ = workspace.reload(path);
+ }
+ }
+ }
+ })?;
+ Ok(watcher)
+ }
+}
+
+impl<W: Workspace> Workspace for DocumentWatcher<W> {
+ fn open(
+ &self,
+ uri: Arc<Uri>,
+ text: String,
+ language: DocumentLanguage,
+ source: WorkspaceSource,
+ ) -> Arc<Document> {
+ let document = self.workspace.open(uri, text, language, source);
+ if document.uri.scheme() == "file" {
+ if let Ok(mut path) = document.uri.to_file_path() {
+ path.pop();
+ let mut watched_paths = self.watched_paths.lock().unwrap();
+ if !watched_paths.contains(&path) {
+ if let Err(why) = self
+ .watcher
+ .lock()
+ .unwrap()
+ .watch(&path, RecursiveMode::NonRecursive)
+ {
+ warn!(
+ "Failed to watch folder of document \"{}\": {}",
+ document.uri, why
+ );
+ }
+ watched_paths.insert(path);
+ }
+ }
+ }
+ document
+ }
+
+ fn register_open_handler(&self, handler: OpenHandler) {
+ self.workspace.register_open_handler(handler);
+ }
+
+ fn documents(&self) -> Vec<Arc<Document>> {
+ self.workspace.documents()
+ }
+
+ fn has(&self, uri: &Uri) -> bool {
+ self.workspace.has(uri)
+ }
+
+ fn get(&self, uri: &Uri) -> Option<Arc<Document>> {
+ self.workspace.get(uri)
+ }
+
+ fn close(&self, uri: &Uri) {
+ self.workspace.close(uri)
+ }
+
+ fn is_open(&self, uri: &Uri) -> bool {
+ self.workspace.is_open(uri)
+ }
+
+ fn subset(&self, uri: Arc<Uri>) -> Option<WorkspaceSubset> {
+ self.workspace.subset(uri)
+ }
+}
diff --git a/support/texlab/tests/integration/common.rs b/support/texlab/tests/integration/common.rs
new file mode 100644
index 0000000000..5430bfd2be
--- /dev/null
+++ b/support/texlab/tests/integration/common.rs
@@ -0,0 +1,318 @@
+use std::{
+ fs,
+ sync::{
+ atomic::{AtomicI32, Ordering},
+ Mutex,
+ },
+ thread::{self, JoinHandle},
+};
+
+use anyhow::Result;
+use lsp_server::{Connection, RequestId};
+use lsp_types::{notification::Notification, request::Request, *};
+use rustc_hash::FxHashMap;
+use tempfile::{tempdir, TempDir};
+use texlab::Server;
+use unindent::unindent;
+
+pub struct ServerTester {
+ pub directory: TempDir,
+ client: Connection,
+ handle: Option<JoinHandle<()>>,
+ request_id: AtomicI32,
+ pub diagnostics_by_uri: Mutex<FxHashMap<Url, Vec<Diagnostic>>>,
+}
+
+impl ServerTester {
+ pub fn launch_new_instance() -> Result<Self> {
+ let directory = tempdir()?;
+ let (conn, client) = Connection::memory();
+ let server = Server::with_connection(conn, directory.path().to_path_buf(), false)?;
+ let handle = thread::spawn(move || server.run().unwrap());
+ Ok(Self {
+ directory,
+ client,
+ handle: Some(handle),
+ request_id: AtomicI32::new(0),
+ diagnostics_by_uri: Mutex::default(),
+ })
+ }
+
+ fn wait_for_response(&self, request_id: RequestId) -> Result<lsp_server::Response> {
+ loop {
+ match self.client.receiver.recv()? {
+ lsp_server::Message::Request(request) => {
+ match request.method.as_str() {
+ request::RegisterCapability::METHOD => {
+ self.client
+ .sender
+ .send(lsp_server::Response::new_ok(request.id, ()).into())?;
+ }
+ method => {
+ panic!("unknown request: {}", method);
+ }
+ };
+ }
+ lsp_server::Message::Notification(notification) => {
+ match notification.method.as_str() {
+ notification::PublishDiagnostics::METHOD => {
+ let params = serde_json::from_value::<PublishDiagnosticsParams>(
+ notification.params,
+ )?;
+ let mut diagnostics_by_uri = self.diagnostics_by_uri.lock().unwrap();
+ diagnostics_by_uri.insert(params.uri, params.diagnostics);
+ }
+ method => {
+ panic!("unknown notification: {}", method);
+ }
+ };
+ }
+ lsp_server::Message::Response(response) => {
+ assert_eq!(response.id, request_id);
+ return Ok(response);
+ }
+ }
+ }
+ }
+
+ #[allow(deprecated)]
+ pub fn initialize(
+ &self,
+ client_capabilities: ClientCapabilities,
+ client_info: Option<ClientInfo>,
+ ) -> anyhow::Result<()> {
+ let request_id = RequestId::from(self.request_id.fetch_add(1, Ordering::SeqCst));
+ self.client.sender.send(
+ lsp_server::Request::new(
+ request_id.clone(),
+ request::Initialize::METHOD.to_string(),
+ InitializeParams {
+ process_id: None,
+ root_path: None,
+ root_uri: None,
+ initialization_options: None,
+ capabilities: client_capabilities,
+ trace: None,
+ workspace_folders: None,
+ client_info,
+ locale: None,
+ },
+ )
+ .into(),
+ )?;
+ self.wait_for_response(request_id)?;
+
+ self.client.sender.send(
+ lsp_server::Notification::new(notification::Initialized::METHOD.to_string(), ()).into(),
+ )?;
+
+ Ok(())
+ }
+
+ pub fn open(&self, name: &str, text: &str, language_id: &str, store: bool) -> Result<Url> {
+ let text = unindent(text).trim().to_string();
+ let path = self.directory.path().join(name);
+ if store {
+ fs::create_dir_all(path.parent().unwrap())?;
+ fs::write(&path, &text)?;
+ }
+
+ let uri = Url::from_file_path(path).unwrap();
+ self.client.sender.send(
+ lsp_server::Notification::new(
+ notification::DidOpenTextDocument::METHOD.to_string(),
+ DidOpenTextDocumentParams {
+ text_document: TextDocumentItem::new(
+ uri.clone(),
+ language_id.to_string(),
+ 0,
+ text,
+ ),
+ },
+ )
+ .into(),
+ )?;
+ Ok(uri)
+ }
+
+ pub fn open_memory(&self, uri: Url, text: &str, language_id: &str) -> Result<()> {
+ let text = unindent(text).trim().to_string();
+ let text_document = TextDocumentItem::new(uri, language_id.to_string(), 0, text);
+ self.client.sender.send(
+ lsp_server::Notification::new(
+ notification::DidOpenTextDocument::METHOD.to_string(),
+ DidOpenTextDocumentParams { text_document },
+ )
+ .into(),
+ )?;
+ Ok(())
+ }
+
+ // pub fn edit(&self, uri: Url, text: &str) -> Result<()> {
+ // let text = unindent(text).trim().to_string();
+ // self.client.sender.send(
+ // lsp_server::Notification::new(
+ // notification::DidChangeTextDocument::METHOD.to_string(),
+ // DidChangeTextDocumentParams {
+ // text_document: VersionedTextDocumentIdentifier::new(uri, 0),
+ // content_changes: vec![TextDocumentContentChangeEvent {
+ // text,
+ // range: None,
+ // range_length: None,
+ // }],
+ // },
+ // )
+ // .into(),
+ // )?;
+ // Ok(())
+ // }
+
+ pub fn complete(&self, uri: Url, line: u32, character: u32) -> Result<CompletionList> {
+ let request_id = RequestId::from(self.request_id.fetch_add(1, Ordering::SeqCst));
+
+ self.client.sender.send(
+ lsp_server::Request::new(
+ request_id.clone(),
+ request::Completion::METHOD.to_string(),
+ CompletionParams {
+ text_document_position: TextDocumentPositionParams::new(
+ TextDocumentIdentifier::new(uri),
+ Position::new(line, character),
+ ),
+ work_done_progress_params: WorkDoneProgressParams::default(),
+ partial_result_params: PartialResultParams::default(),
+ context: None,
+ },
+ )
+ .into(),
+ )?;
+
+ let response = self.wait_for_response(request_id)?;
+ let list = serde_json::from_value(response.result.expect("completion request failed"))?;
+ Ok(list)
+ }
+
+ pub fn resolve_completion_item(&self, item: CompletionItem) -> Result<CompletionItem> {
+ let request_id = RequestId::from(self.request_id.fetch_add(1, Ordering::SeqCst));
+
+ self.client.sender.send(
+ lsp_server::Request::new(
+ request_id.clone(),
+ request::ResolveCompletionItem::METHOD.to_string(),
+ item,
+ )
+ .into(),
+ )?;
+
+ let response = self.wait_for_response(request_id)?;
+ let result = serde_json::from_value(
+ response
+ .result
+ .expect("resolve completion item request failed"),
+ )?;
+ Ok(result)
+ }
+
+ pub fn hover(&self, uri: Url, line: u32, character: u32) -> Result<Option<Hover>> {
+ let request_id = RequestId::from(self.request_id.fetch_add(1, Ordering::SeqCst));
+
+ self.client.sender.send(
+ lsp_server::Request::new(
+ request_id.clone(),
+ request::HoverRequest::METHOD.to_string(),
+ HoverParams {
+ text_document_position_params: TextDocumentPositionParams::new(
+ TextDocumentIdentifier::new(uri),
+ Position::new(line, character),
+ ),
+ work_done_progress_params: WorkDoneProgressParams::default(),
+ },
+ )
+ .into(),
+ )?;
+
+ let response = self.wait_for_response(request_id)?;
+ let hover = serde_json::from_value(response.result.expect("hover request failed"))?;
+ Ok(hover)
+ }
+
+ pub fn find_document_symbols(&self, uri: Url) -> Result<DocumentSymbolResponse> {
+ let request_id = RequestId::from(self.request_id.fetch_add(1, Ordering::SeqCst));
+
+ self.client.sender.send(
+ lsp_server::Request::new(
+ request_id.clone(),
+ request::DocumentSymbolRequest::METHOD.to_string(),
+ DocumentSymbolParams {
+ text_document: TextDocumentIdentifier::new(uri),
+ work_done_progress_params: WorkDoneProgressParams::default(),
+ partial_result_params: PartialResultParams::default(),
+ },
+ )
+ .into(),
+ )?;
+
+ let response = self.wait_for_response(request_id)?;
+ let symbols =
+ serde_json::from_value(response.result.expect("document symbol request failed"))?;
+ Ok(symbols)
+ }
+
+ pub fn find_workspace_symbols(&self, query: &str) -> Result<Vec<SymbolInformation>> {
+ let request_id = RequestId::from(self.request_id.fetch_add(1, Ordering::SeqCst));
+
+ self.client.sender.send(
+ lsp_server::Request::new(
+ request_id.clone(),
+ request::WorkspaceSymbol::METHOD.to_string(),
+ WorkspaceSymbolParams {
+ partial_result_params: PartialResultParams::default(),
+ work_done_progress_params: WorkDoneProgressParams::default(),
+ query: query.to_string(),
+ },
+ )
+ .into(),
+ )?;
+
+ let response = self.wait_for_response(request_id)?;
+ let symbols =
+ serde_json::from_value(response.result.expect("workspace symbol request failed"))?;
+ Ok(symbols)
+ }
+
+ // pub fn change_configuration(&self, options: Options) -> Result<()> {
+ // self.client.sender.send(
+ // lsp_server::Notification::new(
+ // notification::DidChangeConfiguration::METHOD.to_string(),
+ // DidChangeConfigurationParams {
+ // settings: serde_json::to_value(options)?,
+ // },
+ // )
+ // .into(),
+ // )?;
+ // Ok(())
+ // }
+}
+
+impl Drop for ServerTester {
+ fn drop(&mut self) {
+ self.client
+ .sender
+ .send(
+ lsp_server::Request::new(
+ RequestId::from(self.request_id.fetch_add(1, Ordering::SeqCst)),
+ request::Shutdown::METHOD.to_string(),
+ (),
+ )
+ .into(),
+ )
+ .unwrap();
+
+ self.client
+ .sender
+ .send(lsp_server::Notification::new(notification::Exit::METHOD.to_string(), ()).into())
+ .unwrap();
+
+ self.handle.take().unwrap().join().unwrap();
+ }
+}
diff --git a/support/texlab/tests/integration/completion.rs b/support/texlab/tests/integration/completion.rs
index 4b3b4ecd83..33af77f003 100644
--- a/support/texlab/tests/integration/completion.rs
+++ b/support/texlab/tests/integration/completion.rs
@@ -1,1227 +1,762 @@
-#[cfg(feature = "citation")]
-use texlab::protocol::{MarkupContent, MarkupKind};
-
-use indoc::indoc;
-use itertools::Itertools;
-use texlab::{
- protocol::{CompletionItem, CompletionTextEdit, Documentation, Range, RangeExt, TextEdit},
- test::{TestBed, TestBedBuilder, TestLspClient, PULL_CAPABILITIES},
-};
-
-async fn run_item(
- test_bed: &TestBed,
- relative_path: &str,
- line: u64,
- character: u64,
- label: &str,
-) -> CompletionItem {
- let item = test_bed
- .completion(relative_path, line, character)
- .await
- .unwrap()
- .into_iter()
- .find(|item| item.label == label)
- .unwrap();
-
- test_bed.client.completion_resolve(item).await.unwrap()
-}
-
-async fn run_list(
- test_bed: &TestBed,
- relative_path: &str,
- line: u64,
- character: u64,
-) -> Vec<String> {
- test_bed
- .completion(relative_path, line, character)
- .await
- .unwrap()
- .into_iter()
- .map(|item| item.label)
- .sorted()
- .collect()
-}
-
-fn verify_text_edit(
- item: &CompletionItem,
- start_line: u64,
- start_character: u64,
- end_line: u64,
- end_character: u64,
- text: &str,
-) {
- assert_eq!(
- *item.text_edit.as_ref().unwrap(),
- CompletionTextEdit::Edit(TextEdit::new(
- Range::new_simple(start_line, start_character, end_line, end_character),
- text.into()
- ))
- );
-}
-
-fn verify_detail(item: &CompletionItem, detail: &str) {
- assert_eq!(item.detail.as_ref().unwrap(), detail);
-}
-
-#[tokio::test]
-async fn empty_latex_document() {
- let mut test_bed = TestBedBuilder::new().file("main.tex", "").build().await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_items = test_bed.completion("main.tex", 0, 0).await.unwrap();
-
- test_bed.shutdown().await;
-
- assert!(actual_items.is_empty());
-}
-
-#[tokio::test]
-async fn empty_bibtex_document() {
- let mut test_bed = TestBedBuilder::new().file("main.bib", "").build().await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.bib").await;
-
- let actual_items = test_bed.completion("main.bib", 0, 0).await.unwrap();
-
- test_bed.shutdown().await;
-
- assert!(actual_items.is_empty());
-}
-
-#[tokio::test]
-async fn bibtex_comment() {
- let mut test_bed = TestBedBuilder::new().file("main.bib", "foo").build().await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.bib").await;
-
- let actual_items = test_bed.completion("main.bib", 0, 2).await.unwrap();
-
- test_bed.shutdown().await;
-
- assert!(actual_items.is_empty());
-}
-
-#[tokio::test]
-async fn bibtex_command_incomplete_entry() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+use anyhow::Result;
+use lsp_types::{CompletionList, Url};
+
+use crate::common::ServerTester;
+
+fn complete_and_resolve(
+ server: &ServerTester,
+ uri: Url,
+ line: u32,
+ character: u32,
+) -> Result<CompletionList> {
+ let mut list = server.complete(uri, line, character)?;
+ let mut new_items = Vec::new();
+ for item in list.items.into_iter().take(7) {
+ let mut new_item = server.resolve_completion_item(item)?;
+ new_item.data = None;
+ new_items.push(new_item);
+ }
+ list.items = new_items;
+ Ok(list)
+}
+
+mod bibtex {
+ use insta::assert_json_snapshot;
+ use lsp_types::ClientCapabilities;
+
+ use super::*;
+
+ #[test]
+ fn test_empty_document() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open("main.bib", "", "bibtex", false)?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 0, 0)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_junk() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open("main.bib", "foo", "bibtex", false)?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 0, 0)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_command_incomplete_entry() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.bib",
- indoc!(
- r#"
- @article{foo,
- author = {\LaT
- }
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.bib").await;
-
- let actual_item = run_item(&test_bed, "main.bib", 1, 18, "LaTeX").await;
-
- test_bed.shutdown().await;
-
- verify_detail(&actual_item, "built-in");
- verify_text_edit(&actual_item, 1, 15, 1, 18, "LaTeX");
-}
-
-#[tokio::test]
-async fn bibtex_command_complete_entry() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ @article{foo,
+ author = {\LaT
+ }
+ "#,
+ "bibtex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 1, 18)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_command_complete_entry() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.bib",
- indoc!(
- r#"
- @article{foo,
- author = {\LaT}
- }
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.bib").await;
-
- let actual_item = run_item(&test_bed, "main.bib", 1, 18, "LaTeX").await;
-
- test_bed.shutdown().await;
-
- verify_detail(&actual_item, "built-in");
- verify_text_edit(&actual_item, 1, 15, 1, 18, "LaTeX");
-}
-
-#[tokio::test]
-async fn bibtex_type_empty() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ @article{foo,
+ author = {\LaT}
+ }
+ "#,
+ "bibtex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 1, 18)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_entry_type_empty_name() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.bib",
- indoc!(
- r#"
- @
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.bib").await;
-
- let actual_item = run_item(&test_bed, "main.bib", 0, 1, "article").await;
-
- test_bed.shutdown().await;
-
- assert!(actual_item.documentation.is_some());
- verify_text_edit(&actual_item, 0, 1, 0, 1, "article");
-}
-
-#[tokio::test]
-async fn bibtex_type_incomplete() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ @
+ "#,
+ "bibtex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 0, 1)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_entry_type_empty_name_before() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.bib",
- indoc!(
- r#"
- @art
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.bib").await;
-
- let actual_item = run_item(&test_bed, "main.bib", 0, 1, "article").await;
-
- test_bed.shutdown().await;
-
- assert!(actual_item.documentation.is_some());
- verify_text_edit(&actual_item, 0, 1, 0, 4, "article");
-}
-
-#[tokio::test]
-async fn bibtex_type_complete() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ @
+ "#,
+ "bibtex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 0, 0)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_entry_type_incomplete() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.bib",
- indoc!(
- r#"
- @article
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.bib").await;
-
- let actual_item = run_item(&test_bed, "main.bib", 0, 1, "article").await;
-
- test_bed.shutdown().await;
-
- assert!(actual_item.documentation.is_some());
- verify_text_edit(&actual_item, 0, 1, 0, 8, "article");
-}
-
-#[tokio::test]
-async fn bibtex_field_incomplete_entry() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ @art
+ "#,
+ "bibtex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 0, 1)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_entry_type_complete() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.bib",
- indoc!(
- r#"
- @article{foo,
- titl
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.bib").await;
-
- let actual_item = run_item(&test_bed, "main.bib", 1, 6, "title").await;
-
- test_bed.shutdown().await;
-
- assert!(actual_item.documentation.is_some());
- verify_text_edit(&actual_item, 1, 4, 1, 8, "title");
-}
-
-#[tokio::test]
-async fn bibtex_field_complete_entry() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ @article
+ "#,
+ "bibtex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 0, 1)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_field_incomplete_entry() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.bib",
- indoc!(
- r#"
- @article{foo,
- title = {}
- }
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.bib").await;
-
- let actual_item = run_item(&test_bed, "main.bib", 1, 6, "title").await;
-
- test_bed.shutdown().await;
-
- assert!(actual_item.documentation.is_some());
- verify_text_edit(&actual_item, 1, 4, 1, 9, "title");
-}
-
-#[tokio::test]
-async fn latex_begin_command() {
- let mut test_bed = TestBedBuilder::new().file("main.tex", r#"\"#).build().await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_item = run_item(&test_bed, "main.tex", 0, 1, "begin").await;
-
- test_bed.shutdown().await;
-
- verify_detail(&actual_item, "built-in");
-}
-
-#[cfg(feature = "citation")]
-#[tokio::test]
-async fn latex_citation_valid() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \documentclass{article}
- \bibliography{main}
- \begin{document}
- \cite{
- \end{document}
- "#
- ),
- )
- .file(
+ r#"
+ @article{foo,
+ titl
+ "#,
+ "bibtex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 1, 6)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_field_complete_entry() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.bib",
- indoc!(
- r#"
- @article{foo:2019,
- author = {Foo Bar},
- title = {Baz Qux},
- year = {2019},
- }
-
- @article{bar:2005,}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
- test_bed.open("main.bib").await;
-
- let actual_item = run_item(&test_bed, "main.tex", 3, 6, "foo:2019").await;
-
- test_bed.shutdown().await;
-
- verify_text_edit(&actual_item, 3, 6, 3, 6, "foo:2019");
- assert_eq!(
- actual_item.documentation.unwrap(),
- Documentation::MarkupContent(MarkupContent {
- kind: MarkupKind::Markdown,
- value: "Bar, F. (2019). *Baz Qux*.".into()
- })
- );
-}
-
-#[cfg(feature = "citation")]
-#[tokio::test]
-async fn latex_citation_invalid() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ @article{foo,
+ title = {}
+ }
+ "#,
+ "bibtex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 1, 6)?);
+ Ok(())
+ }
+}
+
+mod latex {
+ use insta::assert_json_snapshot;
+ use lsp_types::ClientCapabilities;
+
+ use super::*;
+
+ #[test]
+ fn test_empty_document() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open("main.tex", "", "latex", false)?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 0, 0)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_begin_command() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open("main.tex", r#"\b"#, "latex", false)?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 0, 1)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_citation() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let tex_uri = server.open(
"main.tex",
- indoc!(
- r#"
- \documentclass{article}
- \bibliography{main}
- \begin{document}
- \cite{
- \end{document}
- "#
- ),
- )
- .file(
+ r#"
+ \documentclass{article}
+ \bibliography{main}
+ \begin{document}
+ \cite{
+ \end{document}
+ "#,
+ "latex",
+ false,
+ )?;
+ server.open(
"main.bib",
- indoc!(
- r#"
- @article{foo:2019,
- author = {Foo Bar},
- title = {Baz Qux},
- year = {2019},
- }
-
- @article{bar:2005,}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
- test_bed.open("main.bib").await;
-
- let actual_item = run_item(&test_bed, "main.tex", 3, 6, "bar:2005").await;
-
- test_bed.shutdown().await;
-
- verify_text_edit(&actual_item, 3, 6, 3, 6, "bar:2005");
- assert_eq!(actual_item.documentation, None);
-}
-
-#[tokio::test]
-async fn latex_color_name() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \color{re}
- \definecolor{foo}{
- \definecolorset{R}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_item = run_item(&test_bed, "main.tex", 0, 9, "red").await;
-
- test_bed.shutdown().await;
-
- verify_text_edit(&actual_item, 0, 7, 0, 9, "red");
-}
-
-#[tokio::test]
-async fn latex_color_model_define_color() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \color{re}
- \definecolor{foo}{
- \definecolorset{R}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_item = run_item(&test_bed, "main.tex", 1, 18, "rgb").await;
-
- test_bed.shutdown().await;
-
- verify_text_edit(&actual_item, 1, 18, 1, 18, "rgb");
-}
-
-#[tokio::test]
-async fn latex_model_define_color_set() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ @article{foo:2019,
+ author = {Foo Bar},
+ title = {Baz Qux},
+ year = {2019},
+ }
+
+ @article{bar:2005,}
+ "#,
+ "bibtex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, tex_uri, 3, 6)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_citation_after() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let tex_uri = server.open(
"main.tex",
- indoc!(
- r#"
- \color{re}
- \definecolor{foo}{
- \definecolorset{R}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_item = run_item(&test_bed, "main.tex", 2, 17, "RGB").await;
-
- test_bed.shutdown().await;
-
- verify_text_edit(&actual_item, 2, 16, 2, 17, "RGB");
-}
-
-#[tokio::test]
-async fn latex_component_kernel_command() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \documentclass{book}
- \usepackage{amsmath}
- \chap
- \varDel
- \begin{theind}
- \end{alig}
- \begin{doc}
- \vareps
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_item = run_item(&test_bed, "main.tex", 0, 1, "documentclass").await;
-
- test_bed.shutdown().await;
-
- verify_detail(&actual_item, "built-in");
- verify_text_edit(&actual_item, 0, 1, 0, 14, "documentclass");
-}
-
-#[tokio::test]
-async fn latex_component_kernel_command_glyph() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ \documentclass{article}
+ \bibliography{main}
+ \begin{document}
+ \cite{}
+ \end{document}
+ "#,
+ "latex",
+ false,
+ )?;
+ server.open(
+ "main.bib",
+ r#"
+ @article{foo:2019,
+ author = {Foo Bar},
+ title = {Baz Qux},
+ year = {2019},
+ }
+
+ @article{bar:2005,}
+ "#,
+ "bibtex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, tex_uri, 3, 7)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_citation_open_brace() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let tex_uri = server.open(
"main.tex",
- indoc!(
- r#"
- \documentclass{book}
- \usepackage{amsmath}
- \chap
- \varDel
- \begin{theind}
- \end{alig}
- \begin{doc}
- \vareps
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_item = run_item(&test_bed, "main.tex", 7, 7, "varepsilon").await;
-
- test_bed.shutdown().await;
-
- verify_detail(&actual_item, "ε, built-in");
- verify_text_edit(&actual_item, 7, 1, 7, 7, "varepsilon");
-}
-
-#[tokio::test]
-async fn latex_component_kernel_environment() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ \documentclass{article}
+ \bibliography{main}
+ \begin{document}
+ \cite{Foo
+ \end{document}
+ "#,
+ "latex",
+ false,
+ )?;
+ server.open(
+ "main.bib",
+ r#"
+ @article{FooBar,
+ author = {Foo Bar},
+ title = {Baz Qux},
+ year = {2019},
+ }
+ "#,
+ "bibtex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, tex_uri, 3, 9)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_color_name() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.tex",
- indoc!(
- r#"
- \documentclass{book}
- \usepackage{amsmath}
- \chap
- \varDel
- \begin{theind}
- \end{alig}
- \begin{doc}
- \vareps
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_item = run_item(&test_bed, "main.tex", 6, 10, "document").await;
-
- test_bed.shutdown().await;
-
- verify_detail(&actual_item, "built-in");
- verify_text_edit(&actual_item, 6, 7, 6, 10, "document");
-}
-
-#[tokio::test]
-async fn latex_component_class_command() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ \color{re}
+ \definecolor{foo}{
+ \definecolorset{R}
+ "#,
+ "latex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 0, 9)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_color_model_define_color() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.tex",
- indoc!(
- r#"
- \documentclass{book}
- \usepackage{amsmath}
- \chap
- \varDel
- \begin{theind}
- \end{alig}
- \begin{doc}
- \vareps
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_item = run_item(&test_bed, "main.tex", 2, 5, "chapter").await;
-
- test_bed.shutdown().await;
-
- verify_detail(&actual_item, "book.cls");
- verify_text_edit(&actual_item, 2, 1, 2, 5, "chapter");
-}
-
-#[tokio::test]
-async fn latex_component_class_environment() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ \color{re}
+ \definecolor{foo}{
+ \definecolorset{R}
+ "#,
+ "latex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 1, 18)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_color_model_define_color_set() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.tex",
- indoc!(
- r#"
- \documentclass{book}
- \usepackage{amsmath}
- \chap
- \varDel
- \begin{theind}
- \end{alig}
- \begin{doc}
- \vareps
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_item = run_item(&test_bed, "main.tex", 4, 13, "theindex").await;
-
- test_bed.shutdown().await;
-
- verify_detail(&actual_item, "book.cls");
- verify_text_edit(&actual_item, 4, 7, 4, 13, "theindex");
-}
-
-#[tokio::test]
-async fn latex_component_package_command() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ \color{re}
+ \definecolor{foo}{
+ \definecolorset{R}
+ "#,
+ "latex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 2, 17)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_kernel_command() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.tex",
- indoc!(
- r#"
- \documentclass{book}
- \usepackage{amsmath}
- \chap
- \varDel
- \begin{theind}
- \end{alig}
- \begin{doc}
- \vareps
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_item = run_item(&test_bed, "main.tex", 3, 7, "varDelta").await;
-
- test_bed.shutdown().await;
-
- verify_detail(&actual_item, "amsmath.sty");
- verify_text_edit(&actual_item, 3, 1, 3, 7, "varDelta");
-}
-
-#[tokio::test]
-async fn latex_component_package_environment() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ \documentclass{book}
+ \usepackage{amsmath}
+ \chap
+ \varDel
+ \begin{theind}
+ \end{alig}
+ \begin{doc}
+ \vareps
+ "#,
+ "latex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 0, 1)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_kernel_command_glyph() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.tex",
- indoc!(
- r#"
- \documentclass{book}
- \usepackage{amsmath}
- \chap
- \varDel
- \begin{theind}
- \end{alig}
- \begin{doc}
- \vareps
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_item = run_item(&test_bed, "main.tex", 5, 5, "align").await;
-
- test_bed.shutdown().await;
-
- verify_detail(&actual_item, "amsmath.sty");
- verify_text_edit(&actual_item, 5, 5, 5, 9, "align");
-}
-
-#[tokio::test]
-async fn latex_import_class() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ \documentclass{book}
+ \usepackage{amsmath}
+ \chap
+ \varDel
+ \begin{theind}
+ \end{alig}
+ \begin{doc}
+ \vareps
+ "#,
+ "latex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 7, 7)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_kernel_command_environment() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.tex",
- indoc!(
- r#"
- \documentclass{book}
- \usepackage{amsmath}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_item = run_item(&test_bed, "main.tex", 0, 19, "book").await;
-
- test_bed.shutdown().await;
-
- assert!(actual_item.documentation.is_some());
- verify_text_edit(&actual_item, 0, 15, 0, 19, "book");
-}
-
-#[tokio::test]
-async fn latex_import_package() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ \documentclass{book}
+ \usepackage{amsmath}
+ \chap
+ \varDel
+ \begin{theind}
+ \end{alig}
+ \begin{doc}
+ \vareps
+ "#,
+ "latex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 6, 10)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_class_command() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.tex",
- indoc!(
- r#"
- \documentclass{book}
- \usepackage{amsmath}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_item = run_item(&test_bed, "main.tex", 1, 15, "amsmath").await;
-
- test_bed.shutdown().await;
-
- assert!(actual_item.documentation.is_some());
- verify_text_edit(&actual_item, 1, 12, 1, 19, "amsmath");
-}
-
-#[tokio::test]
-async fn latex_include_relative_root_no_extension() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ \documentclass{book}
+ \usepackage{amsmath}
+ \chap
+ \varDel
+ \begin{theind}
+ \end{alig}
+ \begin{doc}
+ \vareps
+ "#,
+ "latex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 2, 5)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_class_environment() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.tex",
- indoc!(
- r#"
- \documentclass{article}
- \include{}
- \input{}
- \input{qux/}
- \addbibresource{}
- "#
- ),
- )
- .file("foo.bib", "")
- .file("bar.tex", "")
- .file("qux/baz.tex", "")
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_items = run_list(&test_bed, "main.tex", 1, 9).await;
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_items, vec!["bar", "main", "qux"]);
-}
-
-#[tokio::test]
-async fn latex_include_relative_root_with_extension() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ \documentclass{book}
+ \usepackage{amsmath}
+ \chap
+ \varDel
+ \begin{theind}
+ \end{alig}
+ \begin{doc}
+ \vareps
+ "#,
+ "latex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 4, 13)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_package_command() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.tex",
- indoc!(
- r#"
- \documentclass{article}
- \include{}
- \input{}
- \input{qux/}
- \addbibresource{}
- "#
- ),
- )
- .file("foo.bib", "")
- .file("bar.tex", "")
- .file("qux/baz.tex", "")
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_items = run_list(&test_bed, "main.tex", 2, 7).await;
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_items, vec!["bar.tex", "main.tex", "qux"]);
-}
-
-#[tokio::test]
-async fn latex_include_relative_subdir() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ \documentclass{book}
+ \usepackage{amsmath}
+ \chap
+ \varDel
+ \begin{theind}
+ \end{alig}
+ \begin{doc}
+ \vareps
+ "#,
+ "latex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 3, 7)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_package_environment() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.tex",
- indoc!(
- r#"
- \documentclass{article}
- \include{}
- \input{}
- \input{qux/}
- \addbibresource{}
- "#
- ),
- )
- .file("foo.bib", "")
- .file("bar.tex", "")
- .file("qux/baz.tex", "")
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_items = run_list(&test_bed, "main.tex", 3, 11).await;
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_items, vec!["baz.tex"]);
-}
-
-#[tokio::test]
-async fn latex_include_relative_parent_dir() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ \documentclass{book}
+ \usepackage{amsmath}
+ \chap
+ \varDel
+ \begin{theind}
+ \end{alig}
+ \begin{doc}
+ \vareps
+ "#,
+ "latex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 5, 5)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_class_import() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.tex",
- indoc!(
- r#"
- \documentclass{article}
- \include{}
- \input{}
- \input{qux/}
- \addbibresource{}
- "#
- ),
- )
- .file("foo.bib", "")
- .file("bar.tex", "")
- .file("qux/baz.tex", r#"\input{../}"#)
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("qux/baz.tex").await;
-
- let actual_items = run_list(&test_bed, "qux/baz.tex", 0, 10).await;
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_items, vec!["bar.tex", "main.tex", "qux"]);
-}
-
-#[tokio::test]
-async fn latex_include_relative_bibliography() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ \documentclass{book}
+ \usepackage{amsmath}
+ "#,
+ "latex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 0, 19)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_package_import() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.tex",
- indoc!(
- r#"
- \documentclass{article}
- \include{}
- \input{}
- \input{qux/}
- \addbibresource{}
- "#
- ),
- )
- .file("foo.bib", "")
- .file("bar.tex", "")
- .file("qux/baz.tex", "")
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_items = run_list(&test_bed, "main.tex", 4, 16).await;
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_items, vec!["foo.bib", "qux"]);
-}
-
-#[tokio::test]
-async fn latex_include_root_dir() {
- let mut test_bed = TestBedBuilder::new()
- .file("src/main.tex", r#"\input{}"#)
- .root_dir(".")
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("src/main.tex").await;
-
- let actual_items = run_list(&test_bed, "src/main.tex", 0, 7).await;
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_items, vec!["src"]);
-}
-
-#[tokio::test]
-async fn latex_label() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ \documentclass{book}
+ \usepackage{amsmath}
+ "#,
+ "latex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 1, 15)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_label() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+
+ server.open(
"foo.tex",
- indoc!(
- r#"
- \documentclass{article}
- %
- \usepackage{amsmath}
- \usepackage{caption}
- \usepackage{amsthm}
- \newtheorem{lemma}{Lemma}
- %
- \begin{document}
- %
- \section{Foo}%
- \label{sec:foo}
- %
- \begin{equation}%
- \label{eq:foo}
- 1 + 1 = 2
- \end{equation}
- %
- \begin{equation}%
- \label{eq:bar}
- 1 + 1 = 2
- \end{equation}
- %
- \begin{figure}%
- \LaTeX{}
- \caption{Baz}%
- \label{fig:baz}
- \end{figure}
- %
- \begin{lemma}%
- \label{thm:foo}
- 1 + 1 = 2
- \end{lemma}
- %
- \include{bar}
- %
- \end{document}
- "#
- ),
- )
- .file(
- "foo.aux",
- indoc!(
- r#"
- \relax
- \@writefile{lof}{\contentsline {figure}{\numberline {1}{\ignorespaces Baz\relax }}{1}\protected@file@percent }
- \providecommand*\caption@xref[2]{\@setref\relax\@undefined{#1}}
- \newlabel{fig:baz}{{1}{1}}
- \@writefile{toc}{\contentsline {section}{\numberline {1}Foo}{1}\protected@file@percent }
- \newlabel{sec:foo}{{1}{1}}
- \newlabel{eq:foo}{{1}{1}}
- \newlabel{eq:bar}{{2}{1}}
- \newlabel{thm:foo}{{1}{1}}
- \@input{bar.aux}
- "#
- ),
- )
- .file(
- "bar.tex",
- indoc!(
- r#"
- \section{Bar}%
- \label{sec:bar}
- %
- Lorem ipsum dolor sit amet.
- \ref{}
- \eqref{}
- "#
- ),
- )
- .file(
- "bar.aux",
- indoc!(
- r#"
- \relax
- \@writefile{toc}{\contentsline {section}{\numberline {2}Bar}{2}\protected@file@percent }
- \newlabel{sec:bar}{{2}{2}}
- \@setckpt{bar}{
- \setcounter{page}{3}
- \setcounter{equation}{2}
- \setcounter{enumi}{0}
- \setcounter{enumii}{0}
- \setcounter{enumiii}{0}
- \setcounter{enumiv}{0}
- \setcounter{footnote}{0}
- \setcounter{mpfootnote}{0}
- \setcounter{part}{0}
- \setcounter{section}{2}
- \setcounter{subsection}{0}
- \setcounter{subsubsection}{0}
- \setcounter{paragraph}{0}
- \setcounter{subparagraph}{0}
- \setcounter{figure}{1}
- \setcounter{table}{0}
- \setcounter{parentequation}{0}
- \setcounter{caption@flags}{0}
- \setcounter{ContinuedFloat}{0}
- \setcounter{lemma}{1}
- }
- "#),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("foo.tex").await;
- test_bed.open("foo.aux").await;
- test_bed.open("bar.tex").await;
- test_bed.open("bar.aux").await;
-
- let actual_items = test_bed.completion("bar.tex", 4, 5).await.unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_items.len(), 6);
- verify_text_edit(&actual_items[0], 4, 5, 4, 5, "sec:bar");
- verify_text_edit(&actual_items[1], 4, 5, 4, 5, "sec:foo");
- verify_text_edit(&actual_items[2], 4, 5, 4, 5, "eq:foo");
- verify_text_edit(&actual_items[3], 4, 5, 4, 5, "eq:bar");
- verify_text_edit(&actual_items[4], 4, 5, 4, 5, "fig:baz");
- verify_text_edit(&actual_items[5], 4, 5, 4, 5, "thm:foo");
- verify_detail(&actual_items[0], "Section 2 (Bar)");
- verify_detail(&actual_items[1], "Section 1 (Foo)");
- verify_detail(&actual_items[2], "Equation (1)");
- verify_detail(&actual_items[3], "Equation (2)");
- verify_detail(&actual_items[4], "Figure 1");
- verify_detail(&actual_items[5], "Lemma 1");
- assert_eq!(
- *actual_items[4].documentation.as_ref().unwrap(),
- Documentation::String("Baz".into())
- );
-}
-
-#[tokio::test]
-async fn latex_preselect_environment() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \begin{document}
- \end{
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_item = run_item(&test_bed, "main.tex", 1, 5, "document").await;
-
- test_bed.shutdown().await;
-
- assert!(actual_item.preselect.unwrap());
-}
-
-#[tokio::test]
-async fn latex_theorem() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ \documentclass{article}
+
+ \usepackage{amsmath}
+ \usepackage{caption}
+ \usepackage{amsthm}
+ \newtheorem{lemma}{Lemma}
+
+ \begin{document}
+
+ \section{Foo}%
+ \label{sec:foo}
+
+ \begin{equation}%
+ \label{eq:foo}
+ 1 + 1 = 2
+ \end{equation}
+
+ \begin{equation}%
+ \label{eq:bar}
+ 1 + 1 = 2
+ \end{equation}
+
+ \begin{figure}%
+ \LaTeX{}
+ \caption{Baz}%
+ \label{fig:baz}
+ \end{figure}
+
+ \begin{lemma}%
+ \label{thm:foo}
+ 1 + 1 = 2
+ \end{lemma}
+
+ \include{bar}
+
+ \end{document}
+ "#,
+ "latex",
+ true,
+ )?;
+ server.open(
+ "foo.aux",
+ r#"
+ \relax
+ \@writefile{lof}{\contentsline {figure}{\numberline {1}{\ignorespaces Baz\relax }}{1}\protected@file@percent }
+ \providecommand*\caption@xref[2]{\@setref\relax\@undefined{#1}}
+ \newlabel{fig:baz}{{1}{1}}
+ \@writefile{toc}{\contentsline {section}{\numberline {1}Foo}{1}\protected@file@percent }
+ \newlabel{sec:foo}{{1}{1}}
+ \newlabel{eq:foo}{{1}{1}}
+ \newlabel{eq:bar}{{2}{1}}
+ \newlabel{thm:foo}{{1}{1}}
+ \@input{bar.aux}
+ "#,
+ "latex",
+ true,
+ )?;
+ let uri = server.open(
+ "bar.tex",
+ r#"
+ \section{Bar}%
+ \label{sec:bar}
+
+ Lorem ipsum dolor sit amet.
+ \ref{}
+ \eqref{}
+ "#,
+ "latex",
+ true,
+ )?;
+ server.open(
+ "bar.aux",
+ r#"
+ \relax
+ \@writefile{toc}{\contentsline {section}{\numberline {2}Bar}{2}\protected@file@percent }
+ \newlabel{sec:bar}{{2}{2}}
+ \@setckpt{bar}{
+ \setcounter{page}{3}
+ \setcounter{equation}{2}
+ \setcounter{enumi}{0}
+ \setcounter{enumii}{0}
+ \setcounter{enumiii}{0}
+ \setcounter{enumiv}{0}
+ \setcounter{footnote}{0}
+ \setcounter{mpfootnote}{0}
+ \setcounter{part}{0}
+ \setcounter{section}{2}
+ \setcounter{subsection}{0}
+ \setcounter{subsubsection}{0}
+ \setcounter{paragraph}{0}
+ \setcounter{subparagraph}{0}
+ \setcounter{figure}{1}
+ \setcounter{table}{0}
+ \setcounter{parentequation}{0}
+ \setcounter{caption@flags}{0}
+ \setcounter{ContinuedFloat}{0}
+ \setcounter{lemma}{1}
+ "#,
+ "latex",
+ true,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 4, 5)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_preselect_environment() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.tex",
- indoc!(
- r#"
- \documentclass{article}
- \usepackage{amsthm}
- \newtheorem{foo}{Foo}
- \begin{f}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_item = run_item(&test_bed, "main.tex", 3, 7, "foo").await;
-
- test_bed.shutdown().await;
-
- verify_text_edit(&actual_item, 3, 7, 3, 8, "foo");
- verify_detail(&actual_item, "user-defined");
-}
-
-#[tokio::test]
-async fn latex_pgf_library() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ \begin{document}
+ \end{
+ "#,
+ "latex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 1, 5)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_theorem_environment() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.tex",
- indoc!(
- r#"
- \usepackage{tikz}
- \usepgflibrary{}
- \usetikzlibrary{}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_item = run_item(&test_bed, "main.tex", 1, 15, "arrows").await;
-
- test_bed.shutdown().await;
-
- verify_text_edit(&actual_item, 1, 15, 1, 15, "arrows");
-}
-
-#[tokio::test]
-async fn latex_tikz_library() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ \documentclass{article}
+ \usepackage{amsthm}
+ \newtheorem{foo}{Foo}
+ \begin{f}
+ "#,
+ "latex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 3, 7)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_pgf_library() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.tex",
- indoc!(
- r#"
- \usepackage{tikz}
- \usepgflibrary{}
- \usetikzlibrary{}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_item = run_item(&test_bed, "main.tex", 2, 16, "arrows").await;
-
- test_bed.shutdown().await;
-
- verify_text_edit(&actual_item, 2, 16, 2, 16, "arrows");
-}
-
-#[tokio::test]
-async fn latex_user_command() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ \usepackage{tikz}
+ \usepgflibrary{}
+ \usetikzlibrary{}
+ "#,
+ "latex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 1, 15)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_user_command() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.tex",
- indoc!(
- r#"
- \foo
- \fo
- \begin{foo}
- \end{foo}
- \begin{fo}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_item = run_item(&test_bed, "main.tex", 1, 3, "foo").await;
-
- test_bed.shutdown().await;
-
- verify_detail(&actual_item, "user-defined");
- verify_text_edit(&actual_item, 1, 1, 1, 3, "foo");
-}
-
-#[tokio::test]
-async fn latex_user_environment() {
- let mut test_bed = TestBedBuilder::new()
- .file(
+ r#"
+ \foobar
+ \fooba
+ \begin{foo}
+ \end{foo}
+ \begin{fo}
+ "#,
+ "latex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 1, 3)?);
+ Ok(())
+ }
+
+ #[test]
+ fn test_user_environment() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
"main.tex",
- indoc!(
- r#"
- \foo
- \fo
- \begin{foo}
- \end{foo}
- \begin{fo}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_item = run_item(&test_bed, "main.tex", 4, 7, "foo").await;
-
- test_bed.shutdown().await;
-
- verify_detail(&actual_item, "user-defined");
- verify_text_edit(&actual_item, 4, 7, 4, 9, "foo");
+ r#"
+ \foobar
+ \fooba
+ \begin{foo}
+ \end{foo}
+ \begin{fo}
+ "#,
+ "latex",
+ false,
+ )?;
+ assert_json_snapshot!(complete_and_resolve(&server, uri, 4, 7)?);
+ Ok(())
+ }
}
diff --git a/support/texlab/tests/integration/definition.rs b/support/texlab/tests/integration/definition.rs
deleted file mode 100644
index 685b121377..0000000000
--- a/support/texlab/tests/integration/definition.rs
+++ /dev/null
@@ -1,482 +0,0 @@
-use indoc::indoc;
-use texlab::{
- protocol::{LocationLink, Range, RangeExt},
- test::{TestBedBuilder, LOCATION_LINK_CAPABILITIES, PULL_CAPABILITIES},
-};
-
-fn verify_origin_selection_range(
- link: &LocationLink,
- start_line: u64,
- start_character: u64,
- end_line: u64,
- end_character: u64,
-) {
- assert_eq!(
- link.origin_selection_range,
- Some(Range::new_simple(
- start_line,
- start_character,
- end_line,
- end_character
- ))
- );
-}
-
-#[tokio::test]
-async fn empty_latex_document() {
- let mut test_bed = TestBedBuilder::new().file("main.tex", "").build().await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_locations = test_bed
- .definition_location("main.tex", 0, 0)
- .await
- .unwrap();
-
- test_bed.shutdown().await;
-
- assert!(actual_locations.is_empty());
-}
-
-#[tokio::test]
-async fn empty_bibtex_document() {
- let mut test_bed = TestBedBuilder::new().file("main.bib", "").build().await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.bib").await;
-
- let actual_locations = test_bed
- .definition_location("main.bib", 0, 0)
- .await
- .unwrap();
-
- test_bed.shutdown().await;
-
- assert!(actual_locations.is_empty());
-}
-
-#[tokio::test]
-async fn bibtex_string() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.bib",
- indoc!(
- r#"
- @string{foo = "Foo"}
- %
- @string{bar = "Bar"}
- %
- @article{baz,
- author = bar
- }
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed
- .initialize(LOCATION_LINK_CAPABILITIES.clone())
- .await;
- test_bed.open("main.bib").await;
-
- let mut actual_links = test_bed.definition_link("main.bib", 5, 14).await.unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_links.len(), 1);
- let link = actual_links.pop().unwrap();
- verify_origin_selection_range(&link, 5, 13, 5, 16);
- assert_eq!(link.target_uri, test_bed.uri("main.bib").into());
- assert_eq!(link.target_range, Range::new_simple(2, 0, 2, 20));
- assert_eq!(link.target_selection_range, Range::new_simple(2, 8, 2, 11));
-}
-
-#[tokio::test]
-async fn latex_citation_link() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "foo.tex",
- indoc!(
- r#"
- \bibliography{bar}
- \cite{bar}
- "#
- ),
- )
- .file(
- "bar.bib",
- indoc!(
- r#"
- @article{foo,}
- %
- @article{bar,}
- "#
- ),
- )
- .file(
- "baz.bib",
- indoc!(
- r#"
- @article{foo,}
- %
- @article{bar,}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed
- .initialize(LOCATION_LINK_CAPABILITIES.clone())
- .await;
- test_bed.open("foo.tex").await;
- test_bed.open("bar.bib").await;
- test_bed.open("baz.bib").await;
-
- let mut actual_links = test_bed.definition_link("foo.tex", 1, 7).await.unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_links.len(), 1);
- let link = actual_links.pop().unwrap();
- verify_origin_selection_range(&link, 1, 6, 1, 9);
- assert_eq!(link.target_uri, test_bed.uri("bar.bib").into());
- assert_eq!(link.target_range, Range::new_simple(2, 0, 2, 14));
- assert_eq!(link.target_selection_range, Range::new_simple(2, 9, 2, 12));
-}
-
-#[tokio::test]
-async fn latex_citation_location() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "foo.tex",
- indoc!(
- r#"
- \bibliography{bar}
- \cite{bar}
- "#
- ),
- )
- .file(
- "bar.bib",
- indoc!(
- r#"
- @article{foo,}
- %
- @article{bar,}
- "#
- ),
- )
- .file(
- "baz.bib",
- indoc!(
- r#"
- @article{foo,}
- %
- @article{bar,}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("foo.tex").await;
- test_bed.open("bar.bib").await;
- test_bed.open("baz.bib").await;
-
- let mut actual_locations = test_bed.definition_location("foo.tex", 1, 7).await.unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_locations.len(), 1);
- let location = actual_locations.pop().unwrap();
- assert_eq!(location.uri, test_bed.uri("bar.bib").into());
- assert_eq!(location.range, Range::new_simple(2, 9, 2, 12));
-}
-
-#[tokio::test]
-async fn latex_command_link() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \newcommand{\foo}{Foo}
- %
- \foo
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed
- .initialize(LOCATION_LINK_CAPABILITIES.clone())
- .await;
- test_bed.open("main.tex").await;
-
- let mut actual_links = test_bed.definition_link("main.tex", 2, 2).await.unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_links.len(), 1);
- let link = actual_links.pop().unwrap();
- verify_origin_selection_range(&link, 2, 0, 2, 4);
- assert_eq!(link.target_uri, test_bed.uri("main.tex").into());
- assert_eq!(link.target_range, Range::new_simple(0, 0, 0, 22));
- assert_eq!(link.target_selection_range, Range::new_simple(0, 0, 0, 22));
-}
-
-#[tokio::test]
-async fn latex_math_operator_link() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \DeclareMathOperator{\foo}{foo}
- %
- \foo
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed
- .initialize(LOCATION_LINK_CAPABILITIES.clone())
- .await;
- test_bed.open("main.tex").await;
-
- let mut actual_links = test_bed.definition_link("main.tex", 2, 2).await.unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_links.len(), 1);
- let link = actual_links.pop().unwrap();
- verify_origin_selection_range(&link, 2, 0, 2, 4);
- assert_eq!(link.target_uri, test_bed.uri("main.tex").into());
- assert_eq!(link.target_range, Range::new_simple(0, 0, 0, 31));
- assert_eq!(link.target_selection_range, Range::new_simple(0, 0, 0, 31));
-}
-
-#[tokio::test]
-async fn latex_label_unknown_link() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \label{foo}
- \ref{foo}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed
- .initialize(LOCATION_LINK_CAPABILITIES.clone())
- .await;
- test_bed.open("main.tex").await;
-
- let mut actual_links = test_bed.definition_link("main.tex", 1, 7).await.unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_links.len(), 1);
- let link = actual_links.pop().unwrap();
- verify_origin_selection_range(&link, 1, 5, 1, 8);
- assert_eq!(link.target_uri, test_bed.uri("main.tex").into());
- assert_eq!(link.target_range, Range::new_simple(0, 0, 0, 11));
- assert_eq!(link.target_selection_range, Range::new_simple(0, 0, 0, 11));
-}
-
-#[tokio::test]
-async fn latex_label_equation_link() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \begin{equation}%
- \label{eq:foo}
- Foo
- \end{equation}
- %
- \ref{eq:foo}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed
- .initialize(LOCATION_LINK_CAPABILITIES.clone())
- .await;
- test_bed.open("main.tex").await;
-
- let mut actual_links = test_bed.definition_link("main.tex", 5, 8).await.unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_links.len(), 1);
- let link = actual_links.pop().unwrap();
- verify_origin_selection_range(&link, 5, 5, 5, 11);
- assert_eq!(link.target_uri, test_bed.uri("main.tex").into());
- assert_eq!(link.target_range, Range::new_simple(0, 0, 3, 14));
- assert_eq!(link.target_selection_range, Range::new_simple(1, 0, 1, 14));
-}
-
-#[tokio::test]
-async fn latex_label_float_link() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \begin{figure}
- Foo
- \caption{Bar}
- \label{fig}
- \end{figure}
- %
- \ref{fig}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed
- .initialize(LOCATION_LINK_CAPABILITIES.clone())
- .await;
- test_bed.open("main.tex").await;
-
- let mut actual_links = test_bed.definition_link("main.tex", 6, 6).await.unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_links.len(), 1);
- let link = actual_links.pop().unwrap();
- verify_origin_selection_range(&link, 6, 5, 6, 8);
- assert_eq!(link.target_uri, test_bed.uri("main.tex").into());
- assert_eq!(link.target_range, Range::new_simple(0, 0, 4, 12));
- assert_eq!(link.target_selection_range, Range::new_simple(3, 0, 3, 11));
-}
-
-#[tokio::test]
-async fn latex_label_item_link() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \begin{enumerate}
- \item Foo
- \item\label{bar} Bar
- \item Baz
- \end{enumerate}
- %
- \ref{bar}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed
- .initialize(LOCATION_LINK_CAPABILITIES.clone())
- .await;
- test_bed.open("main.tex").await;
-
- let mut actual_links = test_bed.definition_link("main.tex", 6, 6).await.unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_links.len(), 1);
- let link = actual_links.pop().unwrap();
- verify_origin_selection_range(&link, 6, 5, 6, 8);
- assert_eq!(link.target_uri, test_bed.uri("main.tex").into());
- assert_eq!(link.target_range, Range::new_simple(0, 0, 4, 15));
- assert_eq!(link.target_selection_range, Range::new_simple(2, 9, 2, 20));
-}
-
-#[tokio::test]
-async fn latex_label_section_link() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \section{Foo}
- \label{sec:foo}
- %
- \section{Bar}
- \label{sec:bar}
- %
- \ref{sec:foo}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed
- .initialize(LOCATION_LINK_CAPABILITIES.clone())
- .await;
- test_bed.open("main.tex").await;
-
- let mut actual_links = test_bed.definition_link("main.tex", 6, 6).await.unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_links.len(), 1);
- let link = actual_links.pop().unwrap();
- verify_origin_selection_range(&link, 6, 5, 6, 12);
- assert_eq!(link.target_uri, test_bed.uri("main.tex").into());
- assert_eq!(link.target_range, Range::new_simple(0, 0, 3, 0));
- assert_eq!(link.target_selection_range, Range::new_simple(1, 0, 1, 15));
-}
-
-#[tokio::test]
-async fn latex_label_theorem_link() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \usepackage{amsthm}
- \newtheorem{lemma}{Lemma}
- %
- \begin{lemma}%
- \label{thm:foo}
- Foo
- \end{lemma}
- %
- \ref{thm:foo}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed
- .initialize(LOCATION_LINK_CAPABILITIES.clone())
- .await;
- test_bed.open("main.tex").await;
-
- let mut actual_links = test_bed.definition_link("main.tex", 8, 7).await.unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_links.len(), 1);
- let link = actual_links.pop().unwrap();
- verify_origin_selection_range(&link, 8, 5, 8, 12);
- assert_eq!(link.target_uri, test_bed.uri("main.tex").into());
- assert_eq!(link.target_range, Range::new_simple(3, 0, 6, 11));
- assert_eq!(link.target_selection_range, Range::new_simple(4, 0, 4, 15));
-}
diff --git a/support/texlab/tests/integration/document_symbol.rs b/support/texlab/tests/integration/document_symbol.rs
index afc3d54831..dbdbe64972 100644
--- a/support/texlab/tests/integration/document_symbol.rs
+++ b/support/texlab/tests/integration/document_symbol.rs
@@ -1,424 +1,438 @@
-use indoc::indoc;
-use texlab::{
- protocol::{DocumentSymbol, Range, RangeExt},
- test::{TestBedBuilder, NESTED_SYMBOL_CAPABILITIES},
+use anyhow::Result;
+use insta::assert_json_snapshot;
+use lsp_types::{
+ ClientCapabilities, DocumentSymbolClientCapabilities, TextDocumentClientCapabilities, Url,
};
-fn verify_symbol(
- symbol: &DocumentSymbol,
- name: &str,
- detail: Option<&str>,
- selection_range: Range,
- range: Range,
-) {
- assert_eq!(symbol.name, name);
- assert_eq!(symbol.detail.as_ref().map(AsRef::as_ref), detail);
- assert_eq!(symbol.selection_range, selection_range);
- assert_eq!(symbol.range, range);
+use crate::common::ServerTester;
+
+fn nested_symbol_capabilities() -> ClientCapabilities {
+ ClientCapabilities {
+ text_document: Some(TextDocumentClientCapabilities {
+ document_symbol: Some(DocumentSymbolClientCapabilities {
+ hierarchical_document_symbol_support: Some(true),
+ ..DocumentSymbolClientCapabilities::default()
+ }),
+ ..TextDocumentClientCapabilities::default()
+ }),
+ ..ClientCapabilities::default()
+ }
}
-#[tokio::test]
-async fn enumerate() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \documentclass{article}
- %
- \begin{document}
- %
- \begin{enumerate}
- \item\label{it:foo} Foo
- \item\label{it:bar} Bar
- \item[Baz] Baz
- \item[Qux]\label{it:qux} Qux
- \end{enumerate}
- %
- \end{document}
- "#
- ),
- )
- .file(
- "main.aux",
- indoc!(
- r#"
- \relax
- \newlabel{it:foo}{{1}{1}}
- \newlabel{it:qux}{{2}{1}}
- "#
- ),
- )
- .build()
- .await;
-
- test_bed.spawn();
- test_bed
- .initialize(NESTED_SYMBOL_CAPABILITIES.clone())
- .await;
- test_bed.open("main.tex").await;
- test_bed.open("main.aux").await;
-
- let mut actual_symbols = test_bed.document_symbol_nested("main.tex").await.unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_symbols.len(), 1);
- verify_symbol(
- &actual_symbols[0],
- "Enumerate",
- None,
- Range::new_simple(4, 0, 9, 15),
- Range::new_simple(4, 0, 9, 15),
- );
-
- let children = actual_symbols[0].children.take().unwrap();
- assert_eq!(children.len(), 4);
- verify_symbol(
- &children[0],
- "1",
- Some("it:foo"),
- Range::new_simple(5, 9, 5, 23),
- Range::new_simple(5, 4, 6, 4),
- );
- verify_symbol(
- &children[1],
- "Item",
- Some("it:bar"),
- Range::new_simple(6, 9, 6, 23),
- Range::new_simple(6, 4, 7, 4),
- );
- verify_symbol(
- &children[2],
- "Baz",
- None,
- Range::new_simple(7, 4, 7, 14),
- Range::new_simple(7, 4, 8, 4),
- );
- verify_symbol(
- &children[3],
- "Qux",
- Some("it:qux"),
- Range::new_simple(8, 14, 8, 28),
- Range::new_simple(8, 4, 9, 0),
- );
+#[test]
+fn test_enumerate_nested() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(nested_symbol_capabilities(), None)?;
+ let uri = Url::parse("http://www.example.com/main.tex")?;
+ server.open_memory(
+ uri.clone(),
+ r#"
+ \documentclass{article}
+
+ \begin{document}
+
+ \begin{enumerate}
+ \item\label{it:foo} Foo
+ \item\label{it:bar} Bar
+ \item[Baz] Baz
+ \item[Qux]\label{it:qux} Qux
+ \end{enumerate}
+
+ \end{document}
+ "#,
+ "latex",
+ )?;
+ server.open_memory(
+ Url::parse("http://www.example.com/main.aux")?,
+ r#"
+ \relax
+ \newlabel{it:foo}{{1}{1}}
+ \newlabel{it:qux}{{2}{1}}
+ "#,
+ "latex",
+ )?;
+ assert_json_snapshot!(server.find_document_symbols(uri)?);
+ Ok(())
}
-#[tokio::test]
-async fn equation() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \documentclass{article}
- %
- \begin{document}
- %
- \begin{equation}\label{eq:foo}
- Foo
- \end{equation}
- %
- \begin{equation}\label{eq:bar}
- Bar
- \end{equation}
- %
- \begin{equation}
- Baz
- \end{equation}
- %
- \end{document}
- "#
- ),
- )
- .file(
- "main.aux",
- indoc!(
- r#"
- \relax
- \newlabel{eq:foo}{{1}{1}}
- "#
- ),
- )
- .build()
- .await;
-
- test_bed.spawn();
- test_bed
- .initialize(NESTED_SYMBOL_CAPABILITIES.clone())
- .await;
- test_bed.open("main.tex").await;
- test_bed.open("main.aux").await;
-
- let actual_symbols = test_bed.document_symbol_nested("main.tex").await.unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_symbols.len(), 3);
- verify_symbol(
- &actual_symbols[0],
- "Equation (1)",
- Some("eq:foo"),
- Range::new_simple(4, 16, 4, 30),
- Range::new_simple(4, 0, 6, 14),
- );
- verify_symbol(
- &actual_symbols[1],
- "Equation",
- Some("eq:bar"),
- Range::new_simple(8, 16, 8, 30),
- Range::new_simple(8, 0, 10, 14),
- );
- verify_symbol(
- &actual_symbols[2],
- "Equation",
- None,
- Range::new_simple(12, 0, 14, 14),
- Range::new_simple(12, 0, 14, 14),
- );
+#[test]
+fn test_enumerate_flat() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = Url::parse("http://www.example.com/main.tex")?;
+ server.open_memory(
+ uri.clone(),
+ r#"
+ \documentclass{article}
+
+ \begin{document}
+
+ \begin{enumerate}
+ \item\label{it:foo} Foo
+ \item\label{it:bar} Bar
+ \item[Baz] Baz
+ \item[Qux]\label{it:qux} Qux
+ \end{enumerate}
+
+ \end{document}
+ "#,
+ "latex",
+ )?;
+ server.open_memory(
+ Url::parse("http://www.example.com/main.aux")?,
+ r#"
+ \relax
+ \newlabel{it:foo}{{1}{1}}
+ \newlabel{it:qux}{{2}{1}}
+ "#,
+ "latex",
+ )?;
+ assert_json_snapshot!(server.find_document_symbols(uri)?);
+ Ok(())
}
-#[tokio::test]
-async fn float() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \documentclass{article}
- %
- \begin{document}
- %
- \begin{figure}
- Foo
- \caption{Foo}\label{fig:foo}
- \end{figure}
- %
- \begin{figure}
- Bar
- \caption{Bar}\label{fig:bar}
- \end{figure}
- %
- \begin{figure}
- Baz
- \caption{Baz}
- \end{figure}
- %
- \begin{figure}
- Qux
- \end{figure}
- %
- \end{document}
- "#
- ),
- )
- .file(
- "main.aux",
- indoc!(
- r#"
- \relax
- \@writefile{lof}{\contentsline {figure}{\numberline {1}{\ignorespaces Foo}}{1}\protected@file@percent }
- \newlabel{fig:foo}{{1}{1}}
- \@writefile{lof}{\contentsline {figure}{\numberline {2}{\ignorespaces Bar}}{1}\protected@file@percent }
- \@writefile{lof}{\contentsline {figure}{\numberline {3}{\ignorespaces Baz}}{1}\protected@file@percent }
- "#
- ),
- )
- .build()
- .await;
-
- test_bed.spawn();
- test_bed
- .initialize(NESTED_SYMBOL_CAPABILITIES.clone())
- .await;
- test_bed.open("main.tex").await;
- test_bed.open("main.aux").await;
-
- let actual_symbols = test_bed.document_symbol_nested("main.tex").await.unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_symbols.len(), 3);
- verify_symbol(
- &actual_symbols[0],
- "Figure 1: Foo",
- Some("fig:foo"),
- Range::new_simple(6, 17, 6, 32),
- Range::new_simple(4, 0, 7, 12),
- );
- verify_symbol(
- &actual_symbols[1],
- "Figure: Bar",
- Some("fig:bar"),
- Range::new_simple(11, 17, 11, 32),
- Range::new_simple(9, 0, 12, 12),
- );
- verify_symbol(
- &actual_symbols[2],
- "Figure: Baz",
- None,
- Range::new_simple(14, 0, 17, 12),
- Range::new_simple(14, 0, 17, 12),
- );
+#[test]
+fn test_equation_nested() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(nested_symbol_capabilities(), None)?;
+ let uri = Url::parse("http://www.example.com/main.tex")?;
+ server.open_memory(
+ uri.clone(),
+ r#"
+ \documentclass{article}
+
+ \begin{document}
+
+ \begin{equation}\label{eq:foo}
+ Foo
+ \end{equation}
+
+ \begin{equation}\label{eq:bar}
+ Bar
+ \end{equation}
+
+ \begin{equation}
+ Baz
+ \end{equation}
+
+ \end{document}
+ "#,
+ "latex",
+ )?;
+ server.open_memory(
+ Url::parse("http://www.example.com/main.aux")?,
+ r#"
+ \relax
+ \newlabel{eq:foo}{{1}{1}}
+ "#,
+ "latex",
+ )?;
+ assert_json_snapshot!(server.find_document_symbols(uri)?);
+ Ok(())
}
-#[tokio::test]
-async fn section() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \documentclass{article}
- %
- \begin{document}
- %
- \section{Foo}
- %
- \section{Bar}\label{sec:bar}
- %
- \subsection{Baz}\label{sec:baz}
- %
- \end{document}
- "#
- ),
- )
- .file(
- "main.aux",
- indoc!(
- r#"
- \relax
- \@writefile{toc}{\contentsline {section}{\numberline {1}Foo}{1}\protected@file@percent }
- \@writefile{toc}{\contentsline {section}{\numberline {2}Bar}{1}\protected@file@percent }
- \newlabel{sec:bar}{{2}{1}}
- "#
- ),
- )
- .build()
- .await;
-
- test_bed.spawn();
- test_bed
- .initialize(NESTED_SYMBOL_CAPABILITIES.clone())
- .await;
- test_bed.open("main.tex").await;
- test_bed.open("main.aux").await;
-
- let mut actual_symbols = test_bed.document_symbol_nested("main.tex").await.unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_symbols.len(), 2);
- verify_symbol(
- &actual_symbols[0],
- "Foo",
- None,
- Range::new_simple(4, 0, 4, 13),
- Range::new_simple(4, 0, 6, 0),
- );
- verify_symbol(
- &actual_symbols[1],
- "2 Bar",
- Some("sec:bar"),
- Range::new_simple(6, 0, 6, 13),
- Range::new_simple(6, 0, 10, 0),
- );
-
- let children = actual_symbols[1].children.take().unwrap();
- assert_eq!(children.len(), 1);
- verify_symbol(
- &children[0],
- "Baz",
- Some("sec:baz"),
- Range::new_simple(8, 0, 8, 16),
- Range::new_simple(8, 0, 10, 0),
- );
+#[test]
+fn test_equation_flat() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = Url::parse("http://www.example.com/main.tex")?;
+ server.open_memory(
+ uri.clone(),
+ r#"
+ \documentclass{article}
+
+ \begin{document}
+
+ \begin{equation}\label{eq:foo}
+ Foo
+ \end{equation}
+
+ \begin{equation}\label{eq:bar}
+ Bar
+ \end{equation}
+
+ \begin{equation}
+ Baz
+ \end{equation}
+
+ \end{document}
+ "#,
+ "latex",
+ )?;
+ server.open_memory(
+ Url::parse("http://www.example.com/main.aux")?,
+ r#"
+ \relax
+ \newlabel{eq:foo}{{1}{1}}
+ "#,
+ "latex",
+ )?;
+ assert_json_snapshot!(server.find_document_symbols(uri)?);
+ Ok(())
}
-#[tokio::test]
-async fn theorem() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \documentclass{article}
- \usepackage{amsthm}
- \newtheorem{lemma}{Lemma}
- %
- \begin{document}
- %
- \begin{lemma}[Foo]\label{thm:foo}
- Foo
- \end{lemma}
- %
- \begin{lemma}\label{thm:bar}
- Bar
- \end{lemma}
- %
- \begin{lemma}\label{thm:baz}
- Baz
- \end{lemma}
- %
- \begin{lemma}[Qux]
- Qux
- \end{lemma}
- %
- \end{document}
- "#
- ),
- )
- .file(
- "main.aux",
- indoc!(
- r#"
- \relax
- \newlabel{thm:foo}{{1}{1}}
- \newlabel{thm:bar}{{2}{1}}
- "#
- ),
- )
- .build()
- .await;
-
- test_bed.spawn();
- test_bed
- .initialize(NESTED_SYMBOL_CAPABILITIES.clone())
- .await;
- test_bed.open("main.tex").await;
- test_bed.open("main.aux").await;
-
- let actual_symbols = test_bed.document_symbol_nested("main.tex").await.unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_symbols.len(), 4);
- verify_symbol(
- &actual_symbols[0],
- "Lemma 1 (Foo)",
- Some("thm:foo"),
- Range::new_simple(6, 18, 6, 33),
- Range::new_simple(6, 0, 8, 11),
- );
- verify_symbol(
- &actual_symbols[1],
- "Lemma 2",
- Some("thm:bar"),
- Range::new_simple(10, 13, 10, 28),
- Range::new_simple(10, 0, 12, 11),
- );
- verify_symbol(
- &actual_symbols[2],
- "Lemma",
- Some("thm:baz"),
- Range::new_simple(14, 13, 14, 28),
- Range::new_simple(14, 0, 16, 11),
- );
- verify_symbol(
- &actual_symbols[3],
- "Lemma (Qux)",
- None,
- Range::new_simple(18, 0, 20, 11),
- Range::new_simple(18, 0, 20, 11),
- );
+#[test]
+fn test_float_nested() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(nested_symbol_capabilities(), None)?;
+ let uri = Url::parse("http://www.example.com/main.tex")?;
+ server.open_memory(
+ uri.clone(),
+ r#"
+ \documentclass{article}
+
+ \begin{document}
+
+ \begin{figure}
+ Foo
+ \caption{Foo}\label{fig:foo}
+ \end{figure}
+
+ \begin{figure}
+ Bar
+ \caption{Bar}\label{fig:bar}
+ \end{figure}
+
+ \begin{figure}
+ Baz
+ \caption{Baz}
+ \end{figure}
+
+ \begin{figure}
+ Qux
+ \end{figure}
+
+ \end{document}
+ "#,
+ "latex",
+ )?;
+ server.open_memory(
+ Url::parse("http://www.example.com/main.aux")?,
+ r#"
+ \relax
+ \@writefile{lof}{\contentsline {figure}{\numberline {1}{\ignorespaces Foo}}{1}\protected@file@percent }
+ \newlabel{fig:foo}{{1}{1}}
+ \@writefile{lof}{\contentsline {figure}{\numberline {2}{\ignorespaces Bar}}{1}\protected@file@percent }
+ \@writefile{lof}{\contentsline {figure}{\numberline {3}{\ignorespaces Baz}}{1}\protected@file@percent }
+ "#,
+ "latex",
+ )?;
+ assert_json_snapshot!(server.find_document_symbols(uri)?);
+ Ok(())
+}
+
+#[test]
+fn test_float_flat() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = Url::parse("http://www.example.com/main.tex")?;
+ server.open_memory(
+ uri.clone(),
+ r#"
+ \documentclass{article}
+
+ \begin{document}
+
+ \begin{figure}
+ Foo
+ \caption{Foo}\label{fig:foo}
+ \end{figure}
+
+ \begin{figure}
+ Bar
+ \caption{Bar}\label{fig:bar}
+ \end{figure}
+
+ \begin{figure}
+ Baz
+ \caption{Baz}
+ \end{figure}
+
+ \begin{figure}
+ Qux
+ \end{figure}
+
+ \end{document}
+ "#,
+ "latex",
+ )?;
+ server.open_memory(
+ Url::parse("http://www.example.com/main.aux")?,
+ r#"
+ \relax
+ \@writefile{lof}{\contentsline {figure}{\numberline {1}{\ignorespaces Foo}}{1}\protected@file@percent }
+ \newlabel{fig:foo}{{1}{1}}
+ \@writefile{lof}{\contentsline {figure}{\numberline {2}{\ignorespaces Bar}}{1}\protected@file@percent }
+ \@writefile{lof}{\contentsline {figure}{\numberline {3}{\ignorespaces Baz}}{1}\protected@file@percent }
+ "#,
+ "latex",
+ )?;
+ assert_json_snapshot!(server.find_document_symbols(uri)?);
+ Ok(())
+}
+
+#[test]
+fn test_section_nested() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(nested_symbol_capabilities(), None)?;
+ let uri = Url::parse("http://www.example.com/main.tex")?;
+ server.open_memory(
+ uri.clone(),
+ r#"
+ \documentclass{article}
+
+ \begin{document}
+
+ \section{Foo}
+
+ \section{Bar}\label{sec:bar}
+
+ \subsection{Baz}\label{sec:baz}
+
+ \end{document}
+ "#,
+ "latex",
+ )?;
+ server.open_memory(
+ Url::parse("http://www.example.com/main.aux")?,
+ r#"
+ \relax
+ \@writefile{toc}{\contentsline {section}{\numberline {1}Foo}{1}\protected@file@percent }
+ \@writefile{toc}{\contentsline {section}{\numberline {2}Bar}{1}\protected@file@percent }
+ \newlabel{sec:bar}{{2}{1}}
+ "#,
+ "latex",
+ )?;
+ assert_json_snapshot!(server.find_document_symbols(uri)?);
+ Ok(())
+}
+
+#[test]
+fn test_section_flat() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = Url::parse("http://www.example.com/main.tex")?;
+ server.open_memory(
+ uri.clone(),
+ r#"
+ \documentclass{article}
+
+ \begin{document}
+
+ \section{Foo}
+
+ \section{Bar}\label{sec:bar}
+
+ \subsection{Baz}\label{sec:baz}
+
+ \end{document}
+ "#,
+ "latex",
+ )?;
+ server.open_memory(
+ Url::parse("http://www.example.com/main.aux")?,
+ r#"
+ \relax
+ \@writefile{toc}{\contentsline {section}{\numberline {1}Foo}{1}\protected@file@percent }
+ \@writefile{toc}{\contentsline {section}{\numberline {2}Bar}{1}\protected@file@percent }
+ \newlabel{sec:bar}{{2}{1}}
+ "#,
+ "latex",
+ )?;
+ assert_json_snapshot!(server.find_document_symbols(uri)?);
+ Ok(())
+}
+
+#[test]
+fn test_theorem_nested() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(nested_symbol_capabilities(), None)?;
+ let uri = Url::parse("http://www.example.com/main.tex")?;
+ server.open_memory(
+ uri.clone(),
+ r#"
+ \documentclass{article}
+ \usepackage{amsthm}
+ \newtheorem{lemma}{Lemma}
+
+ \begin{document}
+
+ \begin{lemma}[Foo]\label{thm:foo}
+ Foo
+ \end{lemma}
+
+ \begin{lemma}\label{thm:bar}
+ Bar
+ \end{lemma}
+
+ \begin{lemma}\label{thm:baz}
+ Baz
+ \end{lemma}
+
+ \begin{lemma}[Qux]
+ Qux
+ \end{lemma}
+
+ \end{document}
+ "#,
+ "latex",
+ )?;
+ server.open_memory(
+ Url::parse("http://www.example.com/main.aux")?,
+ r#"
+ \relax
+ \newlabel{thm:foo}{{1}{1}}
+ \newlabel{thm:bar}{{2}{1}}
+ "#,
+ "latex",
+ )?;
+ assert_json_snapshot!(server.find_document_symbols(uri)?);
+ Ok(())
+}
+
+#[test]
+fn test_theorem_flat() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = Url::parse("http://www.example.com/main.tex")?;
+ server.open_memory(
+ uri.clone(),
+ r#"
+ \documentclass{article}
+ \usepackage{amsthm}
+ \newtheorem{lemma}{Lemma}
+
+ \begin{document}
+
+ \begin{lemma}[Foo]\label{thm:foo}
+ Foo
+ \end{lemma}
+
+ \begin{lemma}\label{thm:bar}
+ Bar
+ \end{lemma}
+
+ \begin{lemma}\label{thm:baz}
+ Baz
+ \end{lemma}
+
+ \begin{lemma}[Qux]
+ Qux
+ \end{lemma}
+
+ \end{document}
+ "#,
+ "latex",
+ )?;
+ server.open_memory(
+ Url::parse("http://www.example.com/main.aux")?,
+ r#"
+ \relax
+ \newlabel{thm:foo}{{1}{1}}
+ \newlabel{thm:bar}{{2}{1}}
+ "#,
+ "latex",
+ )?;
+ assert_json_snapshot!(server.find_document_symbols(uri)?);
+ Ok(())
}
diff --git a/support/texlab/tests/integration/folding.rs b/support/texlab/tests/integration/folding.rs
deleted file mode 100644
index 3540e2c680..0000000000
--- a/support/texlab/tests/integration/folding.rs
+++ /dev/null
@@ -1,126 +0,0 @@
-use indoc::indoc;
-use texlab::{
- protocol::{FoldingRange, FoldingRangeKind},
- test::{TestBedBuilder, PULL_CAPABILITIES},
-};
-
-#[tokio::test]
-async fn empty_latex_document() {
- let mut test_bed = TestBedBuilder::new().file("main.tex", "").build().await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_foldings = test_bed.folding_range("main.tex").await.unwrap();
-
- test_bed.shutdown().await;
-
- assert!(actual_foldings.is_empty());
-}
-
-#[tokio::test]
-async fn empty_bibtex_document() {
- let mut test_bed = TestBedBuilder::new().file("main.bib", "").build().await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.bib").await;
-
- let actual_foldings = test_bed.folding_range("main.bib").await.unwrap();
-
- test_bed.shutdown().await;
-
- assert!(actual_foldings.is_empty());
-}
-
-#[tokio::test]
-async fn latex_sections_with_env() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \begin{document}
- \section{Foo}
- Foo
- \section{Bar}
- Bar
- \end{document}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_foldings = test_bed.folding_range("main.tex").await.unwrap();
-
- test_bed.shutdown().await;
-
- let expected_foldings = vec![
- FoldingRange {
- start_line: 0,
- start_character: Some(16),
- end_line: 5,
- end_character: Some(0),
- kind: Some(FoldingRangeKind::Region),
- },
- FoldingRange {
- start_line: 1,
- start_character: Some(13),
- end_line: 2,
- end_character: Some(0),
- kind: Some(FoldingRangeKind::Region),
- },
- ];
- assert_eq!(actual_foldings, expected_foldings);
-}
-
-#[tokio::test]
-async fn bibtex_entry() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.bib",
- indoc!(
- r#"
- @article{foo,
- author = {Foo Bar},
- title = {Baz Qux},
- }
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.bib").await;
-
- let actual_foldings = test_bed.folding_range("main.bib").await.unwrap();
-
- test_bed.shutdown().await;
-
- let expected_foldings = vec![FoldingRange {
- start_line: 0,
- start_character: Some(0),
- end_line: 3,
- end_character: Some(1),
- kind: Some(FoldingRangeKind::Region),
- }];
- assert_eq!(actual_foldings, expected_foldings);
-}
-
-#[tokio::test]
-async fn unknown_file() {
- let mut test_bed = TestBedBuilder::new().build().await;
-
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
-
- let actual_foldings = test_bed.folding_range("main.tex").await;
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_foldings, None);
-}
diff --git a/support/texlab/tests/integration/highlight.rs b/support/texlab/tests/integration/highlight.rs
deleted file mode 100644
index ba77a7b8e3..0000000000
--- a/support/texlab/tests/integration/highlight.rs
+++ /dev/null
@@ -1,83 +0,0 @@
-use indoc::indoc;
-use texlab::{
- protocol::{DocumentHighlight, DocumentHighlightKind, Range, RangeExt},
- test::{TestBedBuilder, PULL_CAPABILITIES},
-};
-
-#[tokio::test]
-async fn empty_latex_document() {
- let mut test_bed = TestBedBuilder::new().file("main.tex", "").build().await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_highlights = test_bed.document_highlight("main.tex", 0, 0).await.unwrap();
-
- test_bed.shutdown().await;
-
- assert!(actual_highlights.is_empty());
-}
-
-#[tokio::test]
-async fn empty_bibtex_document() {
- let mut test_bed = TestBedBuilder::new().file("main.bib", "").build().await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.bib").await;
-
- let actual_highlights = test_bed.document_highlight("main.bib", 0, 0).await.unwrap();
-
- test_bed.shutdown().await;
-
- assert!(actual_highlights.is_empty());
-}
-
-#[tokio::test]
-async fn label() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \label{foo}
- \ref{foo}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_highlights = test_bed.document_highlight("main.tex", 0, 7).await.unwrap();
-
- let expected_highlights = vec![
- DocumentHighlight {
- range: Range::new_simple(0, 7, 0, 10),
- kind: Some(DocumentHighlightKind::Write),
- },
- DocumentHighlight {
- range: Range::new_simple(1, 5, 1, 8),
- kind: Some(DocumentHighlightKind::Read),
- },
- ];
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_highlights, expected_highlights);
-}
-
-#[tokio::test]
-async fn unknown_file() {
- let mut test_bed = TestBedBuilder::new().build().await;
-
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
-
- let actual_highlights = test_bed.document_highlight("main.tex", 0, 0).await;
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_highlights, None);
-}
diff --git a/support/texlab/tests/integration/hover.rs b/support/texlab/tests/integration/hover.rs
index 319ebcc5e9..70b14cccd0 100644
--- a/support/texlab/tests/integration/hover.rs
+++ b/support/texlab/tests/integration/hover.rs
@@ -1,86 +1,89 @@
-use indoc::indoc;
-use texlab::{
- protocol::{HoverContents, MarkupContent, MarkupKind},
- test::{TestBedBuilder, PULL_CAPABILITIES},
-};
+use anyhow::Result;
+use insta::assert_json_snapshot;
+use lsp_types::ClientCapabilities;
-#[tokio::test]
-async fn label_theorem_child_file() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \documentclass{article}
- \newtheorem{lemma}{Lemma}
- \include{child}
- \ref{thm:foo}
- "#
- ),
- )
- .file(
- "child.tex",
- indoc!(
- r#"
- \begin{lemma}\label{thm:foo}
- 1 + 1 = 2
- \end{lemma}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
+use crate::common::ServerTester;
- let actual_hover = test_bed.hover("main.tex", 3, 8).await.unwrap().unwrap();
- assert_eq!(
- actual_hover.contents,
- HoverContents::Markup(MarkupContent {
- kind: MarkupKind::PlainText,
- value: "Lemma".into()
- })
- );
+#[test]
+fn test_empty_bibtex_document() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open("main.bib", "", "bibtex", false)?;
+ assert_json_snapshot!(server.hover(uri, 0, 0)?);
+ Ok(())
}
-#[tokio::test]
-async fn label_theorem_child_file_number() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \documentclass{article}
- \newtheorem{lemma}{Lemma}
- \include{child}
- \ref{thm:foo}
- "#
- ),
- )
- .file(
- "child.tex",
- indoc!(
- r#"
- \begin{lemma}[Foo]\label{thm:foo}
- 1 + 1 = 2
- \end{lemma}
- "#
- ),
- )
- .file("child.aux", r#"\newlabel{thm:foo}{{1}{1}{Foo}{lemma.1}{}}"#)
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
+#[test]
+fn test_empty_labtex_document() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open("main.tex", "", "latex", false)?;
+ assert_json_snapshot!(server.hover(uri, 0, 0)?);
+ Ok(())
+}
+
+#[test]
+fn test_label_theorem_child_file() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
+ "main.tex",
+ r#"
+ \documentclass{article}
+ \newtheorem{lemma}{Lemma}
+ \include{child}
+ \ref{thm:foo}
+ "#,
+ "latex",
+ false,
+ )?;
+ server.open(
+ "child.tex",
+ r#"
+ \begin{lemma}\label{thm:foo}
+ 1 + 1 = 2
+ \end{lemma}
+ "#,
+ "latex",
+ false,
+ )?;
+
+ assert_json_snapshot!(server.hover(uri, 3, 8)?);
+ Ok(())
+}
+
+#[test]
+fn test_label_theorem_child_file_mumber() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = server.open(
+ "main.tex",
+ r#"
+ \documentclass{article}
+ \newtheorem{lemma}{Lemma}
+ \include{child}
+ \ref{thm:foo}
+ "#,
+ "latex",
+ false,
+ )?;
+ server.open(
+ "child.tex",
+ r#"
+ \begin{lemma}[Foo]\label{thm:foo}
+ 1 + 1 = 2
+ \end{lemma}
+ "#,
+ "latex",
+ false,
+ )?;
+ server.open(
+ "child.aux",
+ r#"\newlabel{thm:foo}{{1}{1}{Foo}{lemma.1}{}}"#,
+ "latex",
+ false,
+ )?;
- let actual_hover = test_bed.hover("main.tex", 3, 8).await.unwrap().unwrap();
- assert_eq!(
- actual_hover.contents,
- HoverContents::Markup(MarkupContent {
- kind: MarkupKind::PlainText,
- value: "Lemma 1 (Foo)".into()
- })
- );
+ assert_json_snapshot!(server.hover(uri, 3, 8)?);
+ Ok(())
}
diff --git a/support/texlab/tests/integration/issues.rs b/support/texlab/tests/integration/issues.rs
index 59a1bfeacb..5d6caeb79d 100644
--- a/support/texlab/tests/integration/issues.rs
+++ b/support/texlab/tests/integration/issues.rs
@@ -1,350 +1,41 @@
-use indoc::indoc;
-use texlab::{
- protocol::{HoverContents, Location, LocationLink, MarkupContent, MarkupKind, Range, RangeExt},
- test::{TestBedBuilder, LOCATION_LINK_CAPABILITIES, PULL_CAPABILITIES},
-};
-
-#[tokio::test]
-async fn issue_14() {
- let mut test_bed = TestBedBuilder::new()
- .file("main.tex", r#"\(\be\)"#)
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_items = test_bed.completion("main.tex", 0, 5).await.unwrap();
-
- test_bed.shutdown().await;
- assert!(actual_items.iter().any(|item| item.label == "beta"));
-}
-
-#[tokio::test]
-async fn issue_15_link() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \documentclass{article}
- \begin{document}
- \newcommand{\test}{test}
- hello \test{}
- \end{document}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed
- .initialize(LOCATION_LINK_CAPABILITIES.clone())
- .await;
- test_bed.open("main.tex").await;
-
- let actual_links = test_bed.definition_link("main.tex", 3, 9).await.unwrap();
-
- test_bed.shutdown().await;
- let expected_links = vec![LocationLink {
- origin_selection_range: Some(Range::new_simple(3, 6, 3, 13)),
- target_range: Range::new_simple(2, 0, 2, 24),
- target_selection_range: Range::new_simple(2, 0, 2, 24),
- target_uri: test_bed.uri("main.tex").into(),
- }];
- assert_eq!(actual_links, expected_links);
-}
-
-#[tokio::test]
-async fn issue_15_location() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \documentclass{article}
- \begin{document}
- \newcommand{\test}{test}
- hello \test{}
- \end{document}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_locations = test_bed
- .definition_location("main.tex", 3, 9)
- .await
- .unwrap();
-
- test_bed.shutdown().await;
- let expected_locations = vec![Location {
- range: Range::new_simple(2, 0, 2, 24),
- uri: test_bed.uri("main.tex").into(),
- }];
- assert_eq!(actual_locations, expected_locations);
-}
-
-#[tokio::test]
-async fn issue_17() {
- let mut test_bed = TestBedBuilder::new()
- .file("main.bib", r#"@ARTICLE{foo,}"#)
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.bib").await;
-
- let actual_hover = test_bed.hover("main.bib", 0, 3).await.unwrap().unwrap();
-
- test_bed.shutdown().await;
- assert_eq!(actual_hover.range.unwrap(), Range::new_simple(0, 0, 0, 8));
-}
-
-#[tokio::test]
-async fn issue_21() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \label{foo}
- \cref{}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_items = test_bed.completion("main.tex", 1, 6).await.unwrap();
-
- test_bed.shutdown().await;
- assert!(actual_items.iter().any(|item| item.label == "foo"));
-}
-
-#[tokio::test]
-async fn issue_22_include() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \bibliography{bibfile}
- \cite{}
- "#
- ),
- )
- .file("bibfile.bib", r#"@article{foo,}"#)
- .build()
- .await;
-
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_items = test_bed.completion("main.tex", 1, 6).await.unwrap();
-
- test_bed.shutdown().await;
- assert!(actual_items.iter().any(|item| item.label == "foo"));
-}
-
-#[tokio::test]
-async fn issue_22_definition() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \bibliography{bibfile}
- \cite{A,B}
- "#
- ),
- )
- .file(
- "bibfile.bib",
- indoc!(
- r#"
- @article{A,}
- @article{B,}
- "#
- ),
- )
- .build()
- .await;
-
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_locations = test_bed
- .definition_location("main.tex", 1, 8)
- .await
- .unwrap();
-
- test_bed.shutdown().await;
- let expected_locations = vec![Location {
- range: Range::new_simple(1, 9, 1, 10),
- uri: test_bed.uri("bibfile.bib").into(),
- }];
- assert_eq!(actual_locations, expected_locations);
-}
-
-#[tokio::test]
-async fn issue_23_completion() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \documentclass{article}
- \begin{document}
- \include{test1}
- \include{test2}
- \end{document}
- "#
- ),
- )
- .file("test1.tex", r#"\section{Section 1}\label{sec:1}"#)
- .file(
- "test2.tex",
- indoc!(
- r#"
- \section{Section 2}\label{sec:2}
- %
- This section continues from Section \ref{sec}"#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("test2.tex").await;
- test_bed.detect_root("test2.tex").await;
-
- let actual_items = test_bed.completion("test2.tex", 2, 42).await.unwrap();
-
- test_bed.shutdown().await;
- assert_eq!(actual_items.len(), 2);
-}
-
-#[tokio::test]
-async fn issue_23_rename() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \documentclass{article}
- \begin{document}
- \include{test1}
- \include{test2}
- \end{document}
- "#
- ),
- )
- .file("test1.tex", r#"\section{Section 1}\label{sec:1}"#)
- .file(
- "test2.tex",
- indoc!(
- r#"
- \section{Section 2}\label{sec:2}
- %
- This section continues from Section \ref{sec:1}"#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("test1.tex").await;
- test_bed.detect_root("test1.tex").await;
-
- let workspace_edit = test_bed
- .rename("test1.tex", 0, 27, "foo")
- .await
- .unwrap()
- .unwrap();
-
- test_bed.shutdown().await;
- let changes = workspace_edit.changes.unwrap();
- assert!(changes.contains_key(&test_bed.uri("test1.tex")));
- assert!(changes.contains_key(&test_bed.uri("test2.tex")));
-}
-
-#[tokio::test]
-async fn issue_23_hover() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \documentclass{article}
- \begin{document}
- \include{test1}
- \include{test2}
- \end{document}
- "#
- ),
- )
- .file("test1.tex", r#"\section{Section 1}\label{sec:1}"#)
- .file(
- "test2.tex",
- indoc!(
- r#"
- \section{Section 2}\label{sec:2}
- %
- This section continues from Section \ref{sec:1}"#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("test2.tex").await;
- test_bed.detect_root("test2.tex").await;
-
- let actual_hover = test_bed.hover("test2.tex", 2, 42).await.unwrap().unwrap();
-
- test_bed.shutdown().await;
- assert_eq!(
- actual_hover.contents,
- HoverContents::Markup(MarkupContent {
- kind: MarkupKind::PlainText,
- value: "Section (Section 1)".into()
- })
- );
-}
-
-#[tokio::test]
-async fn issue_26() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \section{Foo}\label{sec:foo}
- \begin{equation}\label{eq:bar}
- \end{equation}
- \eqref{}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_items = test_bed.completion("main.tex", 3, 7).await.unwrap();
-
- test_bed.shutdown().await;
- assert!(actual_items.iter().any(|item| item.label == "eq:bar"));
- assert!(actual_items.iter().all(|item| item.label != "sec:foo"));
+use std::{fs, thread, time::Duration};
+
+use anyhow::Result;
+use lsp_types::ClientCapabilities;
+
+use crate::common::ServerTester;
+
+#[test]
+#[cfg(feature = "completion")]
+fn test_408_parent_expansion() -> Result<()> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let root = server.directory.path();
+
+ let level1 = root.join("level1");
+ let level2 = level1.join("level2");
+ let level3 = level2.join("level3");
+ fs::create_dir_all(&level3)?;
+ fs::write(level3.join("d.tex"), "d")?;
+ fs::write(
+ level2.join("c.tex"),
+ r#"\documentclass{subfiles}\begin{document}\include{level3/d}\end{document}"#,
+ )?;
+ fs::write(
+ level1.join("b.tex"),
+ r#"\documentclass{article}\begin{document}\include{level2/c}\end{document}"#,
+ )?;
+ fs::write(root.join("a.tex"), "}")?;
+
+ thread::sleep(Duration::from_millis(300));
+
+ let uri = server.open("level1/level2/level3/d.tex", "d", "latex", false)?;
+ server.complete(uri, 0, 0)?;
+
+ let diagnostics_by_uri = server.diagnostics_by_uri.lock().unwrap();
+ assert!(diagnostics_by_uri
+ .iter()
+ .all(|(uri, _)| !uri.as_str().ends_with("a.tex")));
+
+ Ok(())
}
diff --git a/support/texlab/tests/integration/link.rs b/support/texlab/tests/integration/link.rs
deleted file mode 100644
index 8db718bd26..0000000000
--- a/support/texlab/tests/integration/link.rs
+++ /dev/null
@@ -1,173 +0,0 @@
-use indoc::indoc;
-use texlab::{
- protocol::{DocumentLink, Range, RangeExt},
- test::{TestBedBuilder, PULL_CAPABILITIES, PUSH_CAPABILITIES},
-};
-
-#[tokio::test]
-async fn empty_latex_document() {
- let mut test_bed = TestBedBuilder::new().file("main.tex", "").build().await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_links = test_bed.document_link("main.tex").await.unwrap();
-
- test_bed.shutdown().await;
-
- assert!(actual_links.is_empty());
-}
-
-#[tokio::test]
-async fn default_settings() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \include{foo/bar}
- \input{qux.tex}
- "#
- ),
- )
- .file("foo/bar.tex", "")
- .file("qux.tex", "")
- .build()
- .await;
-
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_links = test_bed.document_link("main.tex").await.unwrap();
-
- test_bed.shutdown().await;
-
- let expected_links = vec![
- DocumentLink {
- range: Range::new_simple(0, 9, 0, 16),
- target: test_bed.uri("foo/bar.tex").into(),
- tooltip: None,
- },
- DocumentLink {
- range: Range::new_simple(1, 7, 1, 14),
- target: test_bed.uri("qux.tex").into(),
- tooltip: None,
- },
- ];
- assert_eq!(actual_links, expected_links);
-}
-
-#[tokio::test]
-async fn root_directory() {
- let mut test_bed = TestBedBuilder::new()
- .file("src/main.tex", r#"\include{src/foo}"#)
- .file("src/foo.tex", "")
- .root_dir(".")
- .build()
- .await;
-
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("src/main.tex").await;
-
- let actual_links = test_bed.document_link("src/main.tex").await.unwrap();
-
- test_bed.shutdown().await;
-
- let expected_links = vec![DocumentLink {
- range: Range::new_simple(0, 9, 0, 16),
- target: test_bed.uri("src/foo.tex").into(),
- tooltip: None,
- }];
- assert_eq!(actual_links, expected_links);
-}
-
-#[tokio::test]
-async fn parent_directory() {
- let mut test_bed = TestBedBuilder::new()
- .file("src/main.tex", r#"\addbibresource{../foo.bib}"#)
- .file("foo.bib", "")
- .build()
- .await;
-
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("src/main.tex").await;
-
- let actual_links = test_bed.document_link("src/main.tex").await.unwrap();
-
- test_bed.shutdown().await;
-
- let expected_links = vec![DocumentLink {
- range: Range::new_simple(0, 16, 0, 26),
- target: test_bed.uri("foo.bib").into(),
- tooltip: None,
- }];
- assert_eq!(actual_links, expected_links);
-}
-
-#[tokio::test]
-async fn unknown_file() {
- let mut test_bed = TestBedBuilder::new().build().await;
-
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
-
- let actual_links = test_bed.document_link("main.tex").await;
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_links, None);
-}
-
-#[tokio::test]
-async fn edit_file() {
- let mut test_bed = TestBedBuilder::new()
- .file("main.tex", "")
- .file("foo.tex", "")
- .build()
- .await;
-
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
- test_bed.edit("main.tex", r#"\include{foo}"#).await;
-
- let actual_links = test_bed.document_link("main.tex").await.unwrap();
-
- test_bed.shutdown().await;
-
- let expected_links = vec![DocumentLink {
- range: Range::new_simple(0, 9, 0, 12),
- target: test_bed.uri("foo.tex").into(),
- tooltip: None,
- }];
- assert_eq!(actual_links, expected_links);
-}
-
-#[tokio::test]
-async fn did_change_configuration() {
- let mut test_bed = TestBedBuilder::new()
- .file("src/main.tex", r#"\include{src/foo}"#)
- .file("src/foo.tex", "")
- .root_dir(".")
- .build()
- .await;
-
- test_bed.spawn();
- test_bed.initialize(PUSH_CAPABILITIES.clone()).await;
- test_bed.open("src/main.tex").await;
- test_bed.push_options().await;
-
- let actual_links = test_bed.document_link("src/main.tex").await.unwrap();
-
- test_bed.shutdown().await;
-
- let expected_links = vec![DocumentLink {
- range: Range::new_simple(0, 9, 0, 16),
- target: test_bed.uri("src/foo.tex").into(),
- tooltip: None,
- }];
- assert_eq!(actual_links, expected_links);
-}
diff --git a/support/texlab/tests/integration/main.rs b/support/texlab/tests/integration/main.rs
index 99c0b23a5c..ebccca7396 100644
--- a/support/texlab/tests/integration/main.rs
+++ b/support/texlab/tests/integration/main.rs
@@ -1,12 +1,7 @@
+mod common;
+#[cfg(feature = "completion")]
mod completion;
-mod definition;
mod document_symbol;
-mod folding;
-mod highlight;
mod hover;
mod issues;
-mod link;
-mod prepare_rename;
-mod reference;
-mod rename;
mod workspace_symbol;
diff --git a/support/texlab/tests/integration/prepare_rename.rs b/support/texlab/tests/integration/prepare_rename.rs
deleted file mode 100644
index 0efaa712b2..0000000000
--- a/support/texlab/tests/integration/prepare_rename.rs
+++ /dev/null
@@ -1,151 +0,0 @@
-use texlab::{
- protocol::{Range, RangeExt},
- test::{TestBedBuilder, PULL_CAPABILITIES},
-};
-
-#[tokio::test]
-async fn empty_latex_document() {
- let mut test_bed = TestBedBuilder::new().file("main.tex", "").build().await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_range = test_bed.prepare_rename("main.tex", 0, 0).await.unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_range, None);
-}
-
-#[tokio::test]
-async fn empty_bibtex_document() {
- let mut test_bed = TestBedBuilder::new().file("main.bib", "").build().await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.bib").await;
-
- let actual_range = test_bed.prepare_rename("main.bib", 0, 0).await.unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_range, None);
-}
-
-#[tokio::test]
-async fn bibtex_entry() {
- let mut test_bed = TestBedBuilder::new()
- .file("main.bib", r#"@article{foo,}"#)
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.bib").await;
-
- let actual_range = test_bed
- .prepare_rename("main.bib", 0, 10)
- .await
- .unwrap()
- .unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_range, Range::new_simple(0, 9, 0, 12));
-}
-
-#[tokio::test]
-async fn latex_citation() {
- let mut test_bed = TestBedBuilder::new()
- .file("main.tex", r#"\cite{foo,bar}"#)
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_range = test_bed
- .prepare_rename("main.tex", 0, 11)
- .await
- .unwrap()
- .unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_range, Range::new_simple(0, 10, 0, 13));
-}
-
-#[tokio::test]
-async fn latex_command() {
- let mut test_bed = TestBedBuilder::new()
- .file("main.tex", r#"\foo"#)
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_range = test_bed
- .prepare_rename("main.tex", 0, 1)
- .await
- .unwrap()
- .unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_range, Range::new_simple(0, 0, 0, 4));
-}
-
-#[tokio::test]
-async fn latex_environment() {
- let mut test_bed = TestBedBuilder::new()
- .file("main.tex", r#"\begin{foo}\end{bar}"#)
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_range = test_bed
- .prepare_rename("main.tex", 0, 7)
- .await
- .unwrap()
- .unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_range, Range::new_simple(0, 7, 0, 10));
-}
-
-#[tokio::test]
-async fn latex_label() {
- let mut test_bed = TestBedBuilder::new()
- .file("main.tex", r#"\ref{foo,bar}"#)
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_range = test_bed
- .prepare_rename("main.tex", 0, 9)
- .await
- .unwrap()
- .unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_range, Range::new_simple(0, 9, 0, 12));
-}
-
-#[tokio::test]
-async fn unknown_file() {
- let mut test_bed = TestBedBuilder::new().build().await;
-
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
-
- let actual_range = test_bed.prepare_rename("main.tex", 0, 0).await;
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_range, None);
-}
diff --git a/support/texlab/tests/integration/reference.rs b/support/texlab/tests/integration/reference.rs
deleted file mode 100644
index 224f85fbfc..0000000000
--- a/support/texlab/tests/integration/reference.rs
+++ /dev/null
@@ -1,150 +0,0 @@
-use indoc::indoc;
-use texlab::{
- protocol::{Location, Range, RangeExt},
- test::{TestBedBuilder, PULL_CAPABILITIES},
-};
-
-#[tokio::test]
-async fn empty_latex_document() {
- let mut test_bed = TestBedBuilder::new().file("main.tex", "").build().await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_refs = test_bed.references("main.tex", 0, 0, false).await.unwrap();
-
- test_bed.shutdown().await;
-
- assert!(actual_refs.is_empty());
-}
-
-#[tokio::test]
-async fn empty_bibtex_document() {
- let mut test_bed = TestBedBuilder::new().file("main.bib", "").build().await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.bib").await;
-
- let actual_refs = test_bed.references("main.bib", 0, 0, false).await.unwrap();
-
- test_bed.shutdown().await;
-
- assert!(actual_refs.is_empty());
-}
-
-#[tokio::test]
-async fn bibtex_entry() {
- let mut test_bed = TestBedBuilder::new()
- .file("main.bib", r#"@article{foo,}"#)
- .file(
- "main.tex",
- indoc!(
- r#"
- \addbibresource{main.bib}
- \cite{foo}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.bib").await;
- test_bed.open("main.tex").await;
-
- let actual_refs = test_bed.references("main.tex", 1, 8, true).await.unwrap();
-
- test_bed.shutdown().await;
-
- let expected_refs = vec![
- Location {
- uri: test_bed.uri("main.tex").into(),
- range: Range::new_simple(1, 6, 1, 9),
- },
- Location {
- uri: test_bed.uri("main.bib").into(),
- range: Range::new_simple(0, 9, 0, 12),
- },
- ];
-
- assert_eq!(actual_refs, expected_refs);
-}
-
-#[tokio::test]
-async fn bibtex_string() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.bib",
- indoc!(
- r#"
- @string{foo = "foo"}
- @article{bar, author = foo # foo}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.bib").await;
-
- let actual_refs = test_bed.references("main.bib", 1, 31, false).await.unwrap();
-
- test_bed.shutdown().await;
-
- let expected_refs = vec![
- Location {
- uri: test_bed.uri("main.bib").into(),
- range: Range::new_simple(1, 23, 1, 26),
- },
- Location {
- uri: test_bed.uri("main.bib").into(),
- range: Range::new_simple(1, 29, 1, 32),
- },
- ];
-
- assert_eq!(actual_refs, expected_refs);
-}
-
-#[tokio::test]
-async fn latex_label() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \label{foo}
- \ref{foo}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_refs = test_bed.references("main.tex", 0, 7, false).await.unwrap();
-
- test_bed.shutdown().await;
-
- let expected_refs = vec![Location {
- uri: test_bed.uri("main.tex").into(),
- range: Range::new_simple(1, 5, 1, 8),
- }];
-
- assert_eq!(actual_refs, expected_refs);
-}
-
-#[tokio::test]
-async fn unknown_file() {
- let mut test_bed = TestBedBuilder::new().build().await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
-
- let actual_refs = test_bed.references("main.tex", 0, 0, false).await;
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_refs, None);
-}
diff --git a/support/texlab/tests/integration/rename.rs b/support/texlab/tests/integration/rename.rs
deleted file mode 100644
index d8522f1ec6..0000000000
--- a/support/texlab/tests/integration/rename.rs
+++ /dev/null
@@ -1,236 +0,0 @@
-use indoc::indoc;
-use std::collections::HashMap;
-use texlab::{
- protocol::{Range, RangeExt, TextEdit, WorkspaceEdit},
- test::{TestBedBuilder, PULL_CAPABILITIES},
-};
-
-#[tokio::test]
-async fn empty_latex_document() {
- let mut test_bed = TestBedBuilder::new().file("main.tex", "").build().await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_edit = test_bed.rename("main.tex", 0, 0, "").await.unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_edit, None);
-}
-
-#[tokio::test]
-async fn empty_bibtex_document() {
- let mut test_bed = TestBedBuilder::new().file("main.bib", "").build().await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.bib").await;
-
- let actual_edit = test_bed.rename("main.bib", 0, 0, "").await.unwrap();
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_edit, None);
-}
-
-#[tokio::test]
-async fn bibtex_entry() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.bib",
- indoc!(
- r#"
- @article{foo,}
- @article{bar,}
- "#
- ),
- )
- .file(
- "main.tex",
- indoc!(
- r#"
- \addbibresource{main.bib}
- \cite{foo,bar}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.bib").await;
- test_bed.open("main.tex").await;
-
- let actual_edit = test_bed
- .rename("main.bib", 1, 10, "baz")
- .await
- .unwrap()
- .unwrap();
-
- test_bed.shutdown().await;
-
- let mut expected_changes = HashMap::new();
- expected_changes.insert(
- test_bed.uri("main.bib").into(),
- vec![TextEdit::new(Range::new_simple(1, 9, 1, 12), "baz".into())],
- );
- expected_changes.insert(
- test_bed.uri("main.tex").into(),
- vec![TextEdit::new(Range::new_simple(1, 10, 1, 13), "baz".into())],
- );
-
- assert_eq!(actual_edit, WorkspaceEdit::new(expected_changes));
-}
-
-#[tokio::test]
-async fn latex_command() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "foo.tex",
- indoc!(
- r#"
- \input{bar.tex}
- \foo
- "#
- ),
- )
- .file(
- "bar.tex",
- indoc!(
- r#"
- \foo
- \bar
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("foo.tex").await;
- test_bed.open("bar.tex").await;
-
- let actual_edit = test_bed
- .rename("foo.tex", 1, 2, "baz")
- .await
- .unwrap()
- .unwrap();
-
- test_bed.shutdown().await;
-
- let mut expected_changes = HashMap::new();
- expected_changes.insert(
- test_bed.uri("foo.tex").into(),
- vec![TextEdit::new(Range::new_simple(1, 0, 1, 4), "\\baz".into())],
- );
- expected_changes.insert(
- test_bed.uri("bar.tex").into(),
- vec![TextEdit::new(Range::new_simple(0, 0, 0, 4), "\\baz".into())],
- );
-
- assert_eq!(actual_edit, WorkspaceEdit::new(expected_changes));
-}
-
-#[tokio::test]
-async fn latex_environment() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "main.tex",
- indoc!(
- r#"
- \begin{foo}
- \begin{bar}
- Baz
- \end{bar}
- \end{foo}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("main.tex").await;
-
- let actual_edit = test_bed
- .rename("main.tex", 3, 11, "baz")
- .await
- .unwrap()
- .unwrap();
-
- test_bed.shutdown().await;
-
- let mut expected_changes = HashMap::new();
- expected_changes.insert(
- test_bed.uri("main.tex").into(),
- vec![
- TextEdit::new(Range::new_simple(1, 11, 1, 14), "baz".into()),
- TextEdit::new(Range::new_simple(3, 9, 3, 12), "baz".into()),
- ],
- );
-
- assert_eq!(actual_edit, WorkspaceEdit::new(expected_changes));
-}
-
-#[tokio::test]
-async fn latex_label() {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "foo.tex",
- indoc!(
- r#"
- \input{bar.tex}
- \ref{foo,bar}
- "#
- ),
- )
- .file(
- "bar.tex",
- indoc!(
- r#"
- \label{foo}
- \label{bar}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("foo.tex").await;
- test_bed.open("bar.tex").await;
-
- let actual_edit = test_bed
- .rename("foo.tex", 1, 10, "baz")
- .await
- .unwrap()
- .unwrap();
-
- test_bed.shutdown().await;
-
- let mut expected_changes = HashMap::new();
- expected_changes.insert(
- test_bed.uri("foo.tex").into(),
- vec![TextEdit::new(Range::new_simple(1, 9, 1, 12), "baz".into())],
- );
- expected_changes.insert(
- test_bed.uri("bar.tex").into(),
- vec![TextEdit::new(Range::new_simple(1, 7, 1, 10), "baz".into())],
- );
-
- assert_eq!(actual_edit, WorkspaceEdit::new(expected_changes));
-}
-
-#[tokio::test]
-async fn unknown_file() {
- let mut test_bed = TestBedBuilder::new().build().await;
-
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
-
- let actual_edit = test_bed.rename("main.tex", 0, 0, "").await;
-
- test_bed.shutdown().await;
-
- assert_eq!(actual_edit, None);
-}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__bibtex__command_complete_entry.snap b/support/texlab/tests/integration/snapshots/integration__completion__bibtex__command_complete_entry.snap
new file mode 100644
index 0000000000..acd3389f49
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__bibtex__command_complete_entry.snap
@@ -0,0 +1,150 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 1, 18)?"
+
+---
+{
+ "isIncomplete": true,
+ "items": [
+ {
+ "label": "LaTeX",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 15
+ },
+ "end": {
+ "line": 1,
+ "character": 18
+ }
+ },
+ "newText": "LaTeX"
+ }
+ },
+ {
+ "label": "LaTeXe",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "01",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 15
+ },
+ "end": {
+ "line": 1,
+ "character": 18
+ }
+ },
+ "newText": "LaTeXe"
+ }
+ },
+ {
+ "label": "latexreleaseversion",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "02",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 15
+ },
+ "end": {
+ "line": 1,
+ "character": 18
+ }
+ },
+ "newText": "latexreleaseversion"
+ }
+ },
+ {
+ "label": "LastDeclaredEncoding",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "03",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 15
+ },
+ "end": {
+ "line": 1,
+ "character": 18
+ }
+ },
+ "newText": "LastDeclaredEncoding"
+ }
+ },
+ {
+ "label": "last",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "04",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 15
+ },
+ "end": {
+ "line": 1,
+ "character": 18
+ }
+ },
+ "newText": "last"
+ }
+ },
+ {
+ "label": "lastbox",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "05",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 15
+ },
+ "end": {
+ "line": 1,
+ "character": 18
+ }
+ },
+ "newText": "lastbox"
+ }
+ },
+ {
+ "label": "lastkern",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "06",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 15
+ },
+ "end": {
+ "line": 1,
+ "character": 18
+ }
+ },
+ "newText": "lastkern"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__bibtex__command_incomplete_entry.snap b/support/texlab/tests/integration/snapshots/integration__completion__bibtex__command_incomplete_entry.snap
new file mode 100644
index 0000000000..acd3389f49
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__bibtex__command_incomplete_entry.snap
@@ -0,0 +1,150 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 1, 18)?"
+
+---
+{
+ "isIncomplete": true,
+ "items": [
+ {
+ "label": "LaTeX",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 15
+ },
+ "end": {
+ "line": 1,
+ "character": 18
+ }
+ },
+ "newText": "LaTeX"
+ }
+ },
+ {
+ "label": "LaTeXe",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "01",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 15
+ },
+ "end": {
+ "line": 1,
+ "character": 18
+ }
+ },
+ "newText": "LaTeXe"
+ }
+ },
+ {
+ "label": "latexreleaseversion",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "02",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 15
+ },
+ "end": {
+ "line": 1,
+ "character": 18
+ }
+ },
+ "newText": "latexreleaseversion"
+ }
+ },
+ {
+ "label": "LastDeclaredEncoding",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "03",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 15
+ },
+ "end": {
+ "line": 1,
+ "character": 18
+ }
+ },
+ "newText": "LastDeclaredEncoding"
+ }
+ },
+ {
+ "label": "last",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "04",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 15
+ },
+ "end": {
+ "line": 1,
+ "character": 18
+ }
+ },
+ "newText": "last"
+ }
+ },
+ {
+ "label": "lastbox",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "05",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 15
+ },
+ "end": {
+ "line": 1,
+ "character": 18
+ }
+ },
+ "newText": "lastbox"
+ }
+ },
+ {
+ "label": "lastkern",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "06",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 15
+ },
+ "end": {
+ "line": 1,
+ "character": 18
+ }
+ },
+ "newText": "lastkern"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__bibtex__empty_document.snap b/support/texlab/tests/integration/snapshots/integration__completion__bibtex__empty_document.snap
new file mode 100644
index 0000000000..7f8638455c
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__bibtex__empty_document.snap
@@ -0,0 +1,9 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 0, 0)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": []
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__bibtex__entry_type_complete.snap b/support/texlab/tests/integration/snapshots/integration__completion__bibtex__entry_type_complete.snap
new file mode 100644
index 0000000000..fdc29dacb4
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__bibtex__entry_type_complete.snap
@@ -0,0 +1,33 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 0, 1)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "article",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "An article in a journal, magazine, newspaper, or other periodical which forms a \n self-contained unit with its own title. The title of the periodical is given in the \n journaltitle field. If the issue has its own title in addition to the main title of \n the periodical, it goes in the issuetitle field. Note that editor and related \n fields refer to the journal while translator and related fields refer to the article.\n\nRequired fields: `author`, `title`, `journaltitle`, `year/date`"
+ },
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 1
+ },
+ "end": {
+ "line": 0,
+ "character": 8
+ }
+ },
+ "newText": "article"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__bibtex__entry_type_empty_name.snap b/support/texlab/tests/integration/snapshots/integration__completion__bibtex__entry_type_empty_name.snap
new file mode 100644
index 0000000000..ad571951ed
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__bibtex__entry_type_empty_name.snap
@@ -0,0 +1,159 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 0, 1)?"
+
+---
+{
+ "isIncomplete": true,
+ "items": [
+ {
+ "label": "preamble",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 1
+ },
+ "end": {
+ "line": 0,
+ "character": 1
+ }
+ },
+ "newText": "preamble"
+ }
+ },
+ {
+ "label": "string",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "01",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 1
+ },
+ "end": {
+ "line": 0,
+ "character": 1
+ }
+ },
+ "newText": "string"
+ }
+ },
+ {
+ "label": "comment",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "02",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 1
+ },
+ "end": {
+ "line": 0,
+ "character": 1
+ }
+ },
+ "newText": "comment"
+ }
+ },
+ {
+ "label": "article",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "An article in a journal, magazine, newspaper, or other periodical which forms a \n self-contained unit with its own title. The title of the periodical is given in the \n journaltitle field. If the issue has its own title in addition to the main title of \n the periodical, it goes in the issuetitle field. Note that editor and related \n fields refer to the journal while translator and related fields refer to the article.\n\nRequired fields: `author`, `title`, `journaltitle`, `year/date`"
+ },
+ "preselect": false,
+ "sortText": "03",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 1
+ },
+ "end": {
+ "line": 0,
+ "character": 1
+ }
+ },
+ "newText": "article"
+ }
+ },
+ {
+ "label": "book",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "A single-volume book with one or more authors where the authors share credit for\n the work as a whole. This entry type also covers the function of the `@inbook` type\n of traditional BibTeX.\n\nRequired fields: `author`, `title`, `year/date`"
+ },
+ "preselect": false,
+ "sortText": "04",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 1
+ },
+ "end": {
+ "line": 0,
+ "character": 1
+ }
+ },
+ "newText": "book"
+ }
+ },
+ {
+ "label": "mvbook",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "A multi-volume `@book`. For backwards compatibility, multi-volume books are also\n supported by the entry type `@book`. However, it is advisable to make use of the\n dedicated entry type `@mvbook`.\n\nRequired fields: `author`, `title`, `year/date`"
+ },
+ "preselect": false,
+ "sortText": "05",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 1
+ },
+ "end": {
+ "line": 0,
+ "character": 1
+ }
+ },
+ "newText": "mvbook"
+ }
+ },
+ {
+ "label": "inbook",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "A part of a book which forms a self-contained unit with its own title. Note that the\n profile of this entry type is different from standard BibTeX.\n\nRequired fields: `author`, `title`, `booktitle`, `year/date`"
+ },
+ "preselect": false,
+ "sortText": "06",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 1
+ },
+ "end": {
+ "line": 0,
+ "character": 1
+ }
+ },
+ "newText": "inbook"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__bibtex__entry_type_empty_name_before.snap b/support/texlab/tests/integration/snapshots/integration__completion__bibtex__entry_type_empty_name_before.snap
new file mode 100644
index 0000000000..7f8638455c
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__bibtex__entry_type_empty_name_before.snap
@@ -0,0 +1,9 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 0, 0)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": []
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__bibtex__entry_type_incomplete.snap b/support/texlab/tests/integration/snapshots/integration__completion__bibtex__entry_type_incomplete.snap
new file mode 100644
index 0000000000..2f3236bfc4
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__bibtex__entry_type_incomplete.snap
@@ -0,0 +1,79 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 0, 1)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "article",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "An article in a journal, magazine, newspaper, or other periodical which forms a \n self-contained unit with its own title. The title of the periodical is given in the \n journaltitle field. If the issue has its own title in addition to the main title of \n the periodical, it goes in the issuetitle field. Note that editor and related \n fields refer to the journal while translator and related fields refer to the article.\n\nRequired fields: `author`, `title`, `journaltitle`, `year/date`"
+ },
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 1
+ },
+ "end": {
+ "line": 0,
+ "character": 4
+ }
+ },
+ "newText": "article"
+ }
+ },
+ {
+ "label": "artwork",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "Works of the visual arts such as paintings, sculpture, and installations."
+ },
+ "preselect": false,
+ "sortText": "01",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 1
+ },
+ "end": {
+ "line": 0,
+ "character": 4
+ }
+ },
+ "newText": "artwork"
+ }
+ },
+ {
+ "label": "mastersthesis",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "Similar to `@thesis` except that the `type` field is optional and defaults to the\n localised term ‘Master’s thesis’. You may still use the `type` field to override that."
+ },
+ "preselect": false,
+ "sortText": "02",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 1
+ },
+ "end": {
+ "line": 0,
+ "character": 4
+ }
+ },
+ "newText": "mastersthesis"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__bibtex__field_complete_entry.snap b/support/texlab/tests/integration/snapshots/integration__completion__bibtex__field_complete_entry.snap
new file mode 100644
index 0000000000..8c6f330647
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__bibtex__field_complete_entry.snap
@@ -0,0 +1,171 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 1, 6)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "title",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "The title of the work."
+ },
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 4
+ },
+ "end": {
+ "line": 1,
+ "character": 9
+ }
+ },
+ "newText": "title"
+ }
+ },
+ {
+ "label": "titleaddon",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "An annex to the `title`, to be printed in a different font."
+ },
+ "preselect": false,
+ "sortText": "01",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 4
+ },
+ "end": {
+ "line": 1,
+ "character": 9
+ }
+ },
+ "newText": "titleaddon"
+ }
+ },
+ {
+ "label": "booksubtitle",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "The subtitle related to the `booktitle`. If the subtitle field refers to a work which is part of a larger publication, a possible subtitle of the main work is given in this field. See also `subtitle`."
+ },
+ "preselect": false,
+ "sortText": "02",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 4
+ },
+ "end": {
+ "line": 1,
+ "character": 9
+ }
+ },
+ "newText": "booksubtitle"
+ }
+ },
+ {
+ "label": "booktitle",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "If the `title` field indicates the title of a work which is part of a larger publication, the title of the main work is given in this field. See also `title`."
+ },
+ "preselect": false,
+ "sortText": "03",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 4
+ },
+ "end": {
+ "line": 1,
+ "character": 9
+ }
+ },
+ "newText": "booktitle"
+ }
+ },
+ {
+ "label": "booktitleaddon",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "An annex to the `booktitle`, to be printed in a different font."
+ },
+ "preselect": false,
+ "sortText": "04",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 4
+ },
+ "end": {
+ "line": 1,
+ "character": 9
+ }
+ },
+ "newText": "booktitleaddon"
+ }
+ },
+ {
+ "label": "eventtitle",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "The title of a conference, a symposium, or some other event in `@proceedings` and `@inproceedings` entries. Note that this field holds the plain title of the event. Things like \"Proceedings of the Fifth XYZ Conference\" go into the `titleaddon` or `booktitleaddon` field, respectively. See also `eventdate` and `venue`."
+ },
+ "preselect": false,
+ "sortText": "05",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 4
+ },
+ "end": {
+ "line": 1,
+ "character": 9
+ }
+ },
+ "newText": "eventtitle"
+ }
+ },
+ {
+ "label": "eventtitleaddon",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "An annex to the `eventtitle` field. Can be used for known event acronyms, for example."
+ },
+ "preselect": false,
+ "sortText": "06",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 4
+ },
+ "end": {
+ "line": 1,
+ "character": 9
+ }
+ },
+ "newText": "eventtitleaddon"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__bibtex__field_incomplete_entry.snap b/support/texlab/tests/integration/snapshots/integration__completion__bibtex__field_incomplete_entry.snap
new file mode 100644
index 0000000000..72e803cd36
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__bibtex__field_incomplete_entry.snap
@@ -0,0 +1,171 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 1, 6)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "title",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "The title of the work."
+ },
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 4
+ },
+ "end": {
+ "line": 1,
+ "character": 8
+ }
+ },
+ "newText": "title"
+ }
+ },
+ {
+ "label": "titleaddon",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "An annex to the `title`, to be printed in a different font."
+ },
+ "preselect": false,
+ "sortText": "01",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 4
+ },
+ "end": {
+ "line": 1,
+ "character": 8
+ }
+ },
+ "newText": "titleaddon"
+ }
+ },
+ {
+ "label": "booksubtitle",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "The subtitle related to the `booktitle`. If the subtitle field refers to a work which is part of a larger publication, a possible subtitle of the main work is given in this field. See also `subtitle`."
+ },
+ "preselect": false,
+ "sortText": "02",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 4
+ },
+ "end": {
+ "line": 1,
+ "character": 8
+ }
+ },
+ "newText": "booksubtitle"
+ }
+ },
+ {
+ "label": "booktitle",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "If the `title` field indicates the title of a work which is part of a larger publication, the title of the main work is given in this field. See also `title`."
+ },
+ "preselect": false,
+ "sortText": "03",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 4
+ },
+ "end": {
+ "line": 1,
+ "character": 8
+ }
+ },
+ "newText": "booktitle"
+ }
+ },
+ {
+ "label": "booktitleaddon",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "An annex to the `booktitle`, to be printed in a different font."
+ },
+ "preselect": false,
+ "sortText": "04",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 4
+ },
+ "end": {
+ "line": 1,
+ "character": 8
+ }
+ },
+ "newText": "booktitleaddon"
+ }
+ },
+ {
+ "label": "eventtitle",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "The title of a conference, a symposium, or some other event in `@proceedings` and `@inproceedings` entries. Note that this field holds the plain title of the event. Things like \"Proceedings of the Fifth XYZ Conference\" go into the `titleaddon` or `booktitleaddon` field, respectively. See also `eventdate` and `venue`."
+ },
+ "preselect": false,
+ "sortText": "05",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 4
+ },
+ "end": {
+ "line": 1,
+ "character": 8
+ }
+ },
+ "newText": "eventtitle"
+ }
+ },
+ {
+ "label": "eventtitleaddon",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "An annex to the `eventtitle` field. Can be used for known event acronyms, for example."
+ },
+ "preselect": false,
+ "sortText": "06",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 4
+ },
+ "end": {
+ "line": 1,
+ "character": 8
+ }
+ },
+ "newText": "eventtitleaddon"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__bibtex__junk.snap b/support/texlab/tests/integration/snapshots/integration__completion__bibtex__junk.snap
new file mode 100644
index 0000000000..7f8638455c
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__bibtex__junk.snap
@@ -0,0 +1,9 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 0, 0)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": []
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__latex__begin_command.snap b/support/texlab/tests/integration/snapshots/integration__completion__latex__begin_command.snap
new file mode 100644
index 0000000000..ab5ea7765b
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__latex__begin_command.snap
@@ -0,0 +1,162 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 0, 1)?"
+
+---
+{
+ "isIncomplete": true,
+ "items": [
+ {
+ "label": "begin",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 1
+ },
+ "end": {
+ "line": 0,
+ "character": 2
+ }
+ },
+ "newText": "begin"
+ }
+ },
+ {
+ "label": "b",
+ "kind": 1,
+ "detail": "built-in",
+ "documentation": {
+ "kind": "markdown",
+ "value": "![b](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAA6CAIAAAB5+022AAAHLklEQVR4nO2ZX0hTbRzHz9nZ5jb2V8O5mqaOWrrUCJZgLqmkhBDroghSCYr+UDdGXRQFRnQTBUU3o4tjRGgJQZg4Jkgs5pIkM4uZtmzqnJa5s7X/5+w8570477v3cJzbMe19veh7+dvzfM/nnOd3fs/vPIMpioLWknj/NwBbf4Ayac0B8TmOi8Vi4+PjdrvdZrN9/Phxbm4uFAoBAMRicU5Ojl6vr66u3r17d3l5uUwmWwkQnPEti0ajdrv90aNHNpttbm4OhmGVSqVSqSQSCUmSGIbNz89Ho1Eej5ednW00GhsbG+vq6rKzs3+RiFpaAACXy3Xq1CmVSoUgiE6nO336dGdn58jIiNfrXVhYmJ+fd7lcXV1dzc3NSQKxWLx///6+vj6CINKYL6UlgQAAb9++3blzJ4/HU6vVV65c+fTp01LXiEajz58/NxgMyfvUaDT37t0Lh8OrBjQxMVFdXQ3DsMFg6OnpwXE8vREAwG636/X6JJNMJrtx40YkElkFIBzHL1y4gCBIVlZWS0vL0NBQNBrN6EWSZHt7u1wuTzLJ5fIHDx4kEomVAo2Ojm7cuJE2FQgEGo3m/Pnzs7OzGe1CodDhw4eZOarT6YaGhlYK9PjxY4FAwPTl8/ktLS3xeDyjY3d3t1QqTU6EYfjMmTNcJtJKXRg/f/5MEAQzkkgkurq6PB5PxtfWaDRu3ryZ+RZbLBaXy5VxIq3UQNFodHHQ7/f7/f6Mjjk5Odu3b2dGvF7vwMDAioB0Oh2fzy7iCoVCoVBkdEQQpLy8nMf715kgiHfv3gEAfh1o7969JSUlrMvU1dVptVoupgUFBawUnJqaYuXAkkqZWSRJ9vb2VlZWisViBEEUCsXRo0fdbjfHxHz58iUzryEIqq2tDYVCXOam3lx5PF5tbe3WrVuHh4cxDNNqtdu2bWMWmPQSCoXMJYMgCIZhGIa5zF1yt4dhWKPRaDQajhCsuayIQCBgIS4biIsAALFYLBgMBgIBv9//8+fPUCgUi8XoXe8/AiII4sePHy6X68OHDyMjIy6Xa25uDsOwcDiM4zhJkuAfMWdxXK9lAJEk6fV6+/v7rVbr4ODg9PQ03aBBECQUClUqVXFxcX5+vlarzcvL8/l8ZrM5Fost61a5AhEE4XQ629vbu7u7v3z5Eo/H6bhIJNLpdCaTqaamxmAwrF+/XiqVCoVCGIZfv36NoujqA1EUNTk5aTabOzo6PB5PchWkUqnJZGpsbKypqVGr1YtL6Iq0VD3AcfzFixdGoxFBkORgBEF27Njx5MkTv9+fppY4HA5WjTh48CDH/TU1UDweN5vNarWaaSoSiU6ePDkxMQEASG+6ykCJRAJFUVaXLhKJLl26hGEYF9NVBnI4HMnujBaPx2tubvb5fFwcVwjEzsdgMHjnzp2pqSlmUK/XX758WaVSpU/HZenv5wFBrILJBrLb7X19fRTjYw1BkKamJmbPtSr6+vXr/fv3KYq6e/cuM84u58+ePWN1YXl5efX19RwLPy0cxzN2P9++fUNRFEVRVpx9mf7+flZEr9cXFhZyp4EgCMMwrt3PIrGBZmZmWJHi4mKxWLws07GxMRzHVwcoEomwIjKZjPvWSDs4HA7qVw/m2ECs1hOCIAzDOLbDtEZHR9+8ecMK0t+KzAhBECRJZgbatGkTK+J0Orl8bNDCcfzhw4c+n2/Lli3M5xoMBlmLSOfZ4n2QDXTkyBHWoNHR0cWZnlIURVmt1o6OjsLCwrNnzzIzz+v1+nw+5sixsTGCIIqKilK4MDU5OVlVVcUaU1NTMz09nb7CAgAGBgYMBoNIJLp9+7bb7WYehkgkks7OzuTgQCCwb98+BEFaW1tZPmwgAIDNZmOVQQRBjh07loYJx3GLxVJWViYQCE6cOIFhWCKRaG1tZT5sk8nkdDoJggiHw2azWSqVlpWVjY+PZwCiKIokyVevXlVXVzPt+Hz+nj17LBZLIBBg7vaRSGR4ePjixYu5ublSqfTcuXPfv3+nf3K73VVVVclM4vF4JSUlTU1N9fX1SqVy3bp1T58+Xdw4pG4/AAAej+fmzZsGg0EoFCaxlEplbW3ttWvXUBRta2u7fv16Q0PDhg0bsrKyKioqUBRlfnzRi1hZWcnsqCAIgmG4qKiora0t5ZlTujNGkiQ9Ho/NZuvt7X3//v3s7Gw4HCYIAgBA7yQIgiiVytLS0kOHDjU0NBQUFLAqFkVRExMTKIparVb6NCc3N3fXrl3Hjx+vqKhggf6NmwYoKRzHFxYWZmZmvF6v1+sNBAKxWEwsFufn5+v1ep1OJ5fL0xRPkiR9Ph/dvahUqpycnDRdLyeg/1Jr7uD8D1AmrTmg1NkeiUR+uaFJL6FQKJFIlgcEALh161ZPT8/vADpw4MDVq1dTVqAlgehqNjg4+DuASktL0xea1EuWnZ2dn5//O4Ay/kv0pzBm0h+gTFpzQH8Bp4gu0MowthIAAAAASUVORK5CYII=|width=48,height=48)"
+ },
+ "preselect": false,
+ "sortText": "01",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 1
+ },
+ "end": {
+ "line": 0,
+ "character": 2
+ }
+ },
+ "newText": "b"
+ }
+ },
+ {
+ "label": "backslash",
+ "kind": 1,
+ "detail": "\\, built-in",
+ "documentation": {
+ "kind": "markdown",
+ "value": "![backslash](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAA6CAIAAAAsmq7oAAADD0lEQVR4nO2Y3Uv6UBzGv5stsyhCiUTRytJIDTMkRUEqhmnbVTf+o9sygqjAi4RIVqlJKWWl1FB6oyLt/C68K9tb5+YHncudw4fnczjbcxiBEALcg8RO/IN+ha6srKTT6ZeXF5xQSZJ2d3fPzs5wQjc2Nlqt1vb29m+O2lcoy7IWiyWTyTw+PmKDer3eUCgkiuLp6Sk26NDQEMMwT09PW1tb+ncAfRuFQsHhcEQiEUmSvs+qGT3O6dTUVDgcLhQKoijqC9oDOjAwwLLs29vb5ubm5+enHmrP/Ofn5y6XKxgMNhoNPPoA4HA4YrFYuVw+PDzUEbQ3tL+/n2GYTqcjCEK73dZM/Unh8vJydnbW5/PVajU8+gBgs9ni8XilUsnlclqD/gjt6+tjWZYgCI7jPj4+tFFlLG5ububn52dmZiqVCh59ABgfH19dXa3VatlsVlNQOajBYFhfX6coiuf59/d3DVR5kbu7u1AoNDExUSqV8OgDgMVioWn69vZ2f39ffVAFKEmSyWRycHCQ5/nX11e1VEWXZrMZjUZtNpsoinj0AWB0dHRtbe3+/n5nZwep+2wrQwmCSCQSIyMjgiCorG5Vlwmv1xsIBI6OjkqlEjbo8PBwMplUX92qoARB0DTdre6Hhwc8UADweDyLi4vHx8dqqlsttFvdz8/PmUxGubjUv3wnJyd2uz0cDitWt4arpMvlWlpaKhaL+XxefqUGqMlk6la3IAidTkduqXp9hFC5XJ6cnFxYWKjX63j0AcDpdEajUcXq1gY1Go3d6uY4Tq66NekjhKrVqtvtnpubu7q6wqMPAHa7PR6PV6vVg4ODn9ZohlIUxTCMQnVr1UcIXV9f+3y+6enpi4sLPPoAYLVal5eXZapbD9RgMDAMQ1EUx3G9q1uHPkKo0WgEg0Gn01ksFvHoA8DY2BhN0/V6fW9v7/usTihJkqlUymQy8Tzfo7j06SOEJEmKRCJWqzWXy+HRBwCz2ZxIJCRJ4nkeW1KEUDabNZvNfr8fW1IA8Pv9sVjMaDR+eU6gvx8z/wX0H6vA4+khaPAcAAAAAElFTkSuQmCC|width=48,height=48)"
+ },
+ "preselect": false,
+ "sortText": "02",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 1
+ },
+ "end": {
+ "line": 0,
+ "character": 2
+ }
+ },
+ "newText": "backslash"
+ }
+ },
+ {
+ "label": "badness",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "03",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 1
+ },
+ "end": {
+ "line": 0,
+ "character": 2
+ }
+ },
+ "newText": "badness"
+ }
+ },
+ {
+ "label": "bar",
+ "kind": 1,
+ "detail": " ̄, built-in",
+ "documentation": {
+ "kind": "markdown",
+ "value": "![bar](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC8AAAA6CAIAAACvXpfTAAAHHklEQVR4nO2ZX0xSbxjHORxAkH8FEShCpU7DRI3RNHEul1uzrdRqtpazumrzprV1QReuedFFrbY229ja6kJbK7fanEsrK1saWGoFREA0Y0mioAiNA8HhnPO7YDudH3Dg6K/t14Xfu/fhfZ7zed/3Oc/7vgcAwzDaXyP6/w3wL23QkOvvoqFhJLp16xaLxQL/tM6cOZNIJMgeyshCiSAIgiB/dvAoimb5lZRGq9X29vb+8fe/qqqKTidND2Cj3pBqg4ZcGzTk2qAhF2n1oyIURSEI8vv9gUAgFovl5+fLZDKJRMJgrDPsOt0ikYjFYhkdHZ2YmHC73aFQCEEQJpMpk8n2799/+vRptVoNguBaw665FsMwbDKZDAbDixcvVlZWQBDcsWNHVVWVRCIJBoNWq9Xlcsnl8kuXLp04cYLJZGIY9vXr16GhoaWlpfr6+paWFjabTRqdbDvNKI/Ho9frt27dSqPR6HS6SqXq6+tzu92xWAxFURiGPR7PtWvXZDKZWCweGBhIJBIfP37UarXJvUkoFN6/fz9LfKo0CIKYTKampqbk/DMYjPb2dpvNhiBISk8Yhu/duycWi4uLiycnJ7u6ugAAwAd//vx5FEX/E00ikRgeHi4rK0vGZTAYXV1dCwsLZP1jsZher2cwGI2NjYWFhb/TAgCuXr2a5UG5aRAEefjwoVKpxCO2trZ6vd7sXk6ns6SkhE6nEyeGy+WOjY2tnwZF0adPn27fvh2PqFarP3/+nHMMMAx3d3en5Ghpaanb7V4/jdlsrqysxMMJBILBwcEsC0/Uo0eP8vLyiDStra3RaHSdND6f79ChQ/hUAwDQ1dUFQRAVFAzDZmdnxWIxMWmuXLmS3YWUBobhy5cvs1gsPJxUKjUajRRRMAwbGxvjcrm4O5/PHx8fz+5Cuk9NT08bDIZ4PI5bmpqaampqyPqnCMOw9+/fRyIR3FJUVFRWVpbdKzMNBEF9fX0/fvzALRwO5+jRoxwOhyJNLBZLTiRuUavVW7ZsWQ/N69evR0dHibFKSkr27t1LEYVGo/l8PpvN9vsxdLpOpyOuO1WaaDTa398fDAaJxsbGRqlUSp3G4XAsLCzgTYFAoNVqc3ploLFarS9fviRaOBxOc3Pzms4Jb9++jUajeFOpVJaUlKyZJlnulpeXiUa5XL57927qKNFodGpqirjQNTU1IpFozTSrq6vPnj1LuZ9WV1fLZDLqNF6v1263400QBHU6HZPJXDONy+VyOp3/6kGnNzQ0ZDuUpMluty8uLuLNTZs2UZzaVJqZmZnV1VWihcfjUUlAXBiGmUymX79+4RalUrlt27b10ExPTycSCaJFLpcXFxdTp4Eg6N27d8Skqays3Lx583povnz5kmIpLy8nbjc5tbCwQFxrEAT37NlDJWky0Ph8vhSLWq1O2Yqzy2azEYPw+XyNRkPRN5UGgiBik8lkqtVq6igoihqNxlgshlsyVhoURefm5lJeF1rOGwyXy6WYgEmFw+GZmRli0lRXV6cv9NLS0qlTpwKBAHH3oKXPTcqbLBQKJRIJdRqPx+NyufAmCIJ1dXXpSWO3281mc/qFK5UmeTvBJRAIeDwedRqLxeL3+4nu6ZUGw7CpqSkIgnQ6XQ4alUpFbHI4nJwbL654PP78+XPikUipVKZXh0gkMjExkZeXt2/fvhw0DQ0NxIml/lkUwzCj0Tg6Oko0qlSq9ErjdrutVqtCoUgvqqk09fX1crkcb4ZCoZS3jEwOh0Ov13u9XtwCAEBFRUXK1GIYNjk56fP5tFptUVFRDprS0tLm5mb8ZL68vOzxeHKieDyeCxcuzM7OEucVAADiwJKCIGh4eBgAgAMHDmQoY+lH5Tdv3uA7Np1Ov379evYry/z8/LFjx1gsVltbW0dHBx4ZBMG7d++mdH716pVIJNq5c+e3b9/SQ2WgicfjPT09+AzX1dUtLi5m5EBR1GKxtLS0sFisw4cPu93ux48fCwQCfCQGg4HYPxwOd3Z2giB48eLFjN/3M99gfD7f8ePHk/WAxWL19PSkX6OCwWB/f/+uXbvYbPbJkyfn5+cxDAuFQh0dHfhCd3d3wzCc7A/D8J07d/h8vlqtdjqdGZ9Lep+an5/v7OxMFkM+n3/u3LkPHz74/f7l5WWHwzEwMHDw4EEejyeVSnt7e1dWVnDHT58+1dbWJoEUCsXQ0JDf7//+/fvNmzcLCgokEsmDBw/Sv2zkoMEwLBAI3Lhxo6KigslkgiBYUFCg0Wg0Go1CoWCz2SKR6MiRI+Pj47FYjOiFoqjVam1vb8/Pz6fRaCKRSKPRlJeXczgcpVJ5+/btlP5E5fi2hSDI3NzcyMjIkydPHA7Hz58/QRAsLCzU6XRtbW21tbV4lqQoEAiMjIwMDg6azeZwOCwUChsaGs6ePVtbW5vltL/xrwe5NmjI9XfR/ANs1iTDg+2GBQAAAABJRU5ErkJggg==|width=48,height=48)"
+ },
+ "preselect": false,
+ "sortText": "04",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 1
+ },
+ "end": {
+ "line": 0,
+ "character": 2
+ }
+ },
+ "newText": "bar"
+ }
+ },
+ {
+ "label": "baselineskip",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "05",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 1
+ },
+ "end": {
+ "line": 0,
+ "character": 2
+ }
+ },
+ "newText": "baselineskip"
+ }
+ },
+ {
+ "label": "baselinestretch",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "06",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 1
+ },
+ "end": {
+ "line": 0,
+ "character": 2
+ }
+ },
+ "newText": "baselinestretch"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__latex__citation.snap b/support/texlab/tests/integration/snapshots/integration__completion__latex__citation.snap
new file mode 100644
index 0000000000..1ccf0206c8
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__latex__citation.snap
@@ -0,0 +1,54 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, tex_uri, 3, 6)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "foo:2019",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "Bar, F. (2019). *Baz Qux*."
+ },
+ "preselect": false,
+ "sortText": "00 foo:2019 @articlefoo:2019 author Foo Bar title Baz Qux year 2019",
+ "filterText": "foo:2019 @articlefoo:2019 author Foo Bar title Baz Qux year 2019",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 3,
+ "character": 6
+ },
+ "end": {
+ "line": 3,
+ "character": 6
+ }
+ },
+ "newText": "foo:2019"
+ }
+ },
+ {
+ "label": "bar:2005",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "01 bar:2005 @articlebar:2005",
+ "filterText": "bar:2005 @articlebar:2005",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 3,
+ "character": 6
+ },
+ "end": {
+ "line": 3,
+ "character": 6
+ }
+ },
+ "newText": "bar:2005"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__latex__citation_after.snap b/support/texlab/tests/integration/snapshots/integration__completion__latex__citation_after.snap
new file mode 100644
index 0000000000..156b28dc02
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__latex__citation_after.snap
@@ -0,0 +1,9 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, tex_uri, 3, 7)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": []
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__latex__citation_open_brace.snap b/support/texlab/tests/integration/snapshots/integration__completion__latex__citation_open_brace.snap
new file mode 100644
index 0000000000..ba2a1c7ce3
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__latex__citation_open_brace.snap
@@ -0,0 +1,34 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, tex_uri, 3, 9)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "FooBar",
+ "kind": 1,
+ "documentation": {
+ "kind": "markdown",
+ "value": "Bar, F. (2019). *Baz Qux*."
+ },
+ "preselect": false,
+ "sortText": "00 FooBar @articleFooBar author Foo Bar title Baz Qux year 2019",
+ "filterText": "FooBar @articleFooBar author Foo Bar title Baz Qux year 2019",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 3,
+ "character": 6
+ },
+ "end": {
+ "line": 3,
+ "character": 9
+ }
+ },
+ "newText": "FooBar"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__latex__class_command.snap b/support/texlab/tests/integration/snapshots/integration__completion__latex__class_command.snap
new file mode 100644
index 0000000000..ff214e764a
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__latex__class_command.snap
@@ -0,0 +1,150 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 2, 5)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "chaptername",
+ "kind": 1,
+ "detail": "book.cls",
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 2,
+ "character": 1
+ },
+ "end": {
+ "line": 2,
+ "character": 5
+ }
+ },
+ "newText": "chaptername"
+ }
+ },
+ {
+ "label": "chaptermark",
+ "kind": 1,
+ "detail": "book.cls",
+ "preselect": false,
+ "sortText": "01",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 2,
+ "character": 1
+ },
+ "end": {
+ "line": 2,
+ "character": 5
+ }
+ },
+ "newText": "chaptermark"
+ }
+ },
+ {
+ "label": "chapter",
+ "kind": 1,
+ "detail": "book.cls",
+ "preselect": false,
+ "sortText": "02",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 2,
+ "character": 1
+ },
+ "end": {
+ "line": 2,
+ "character": 5
+ }
+ },
+ "newText": "chapter"
+ }
+ },
+ {
+ "label": "thechapter",
+ "kind": 1,
+ "detail": "book.cls",
+ "preselect": false,
+ "sortText": "03",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 2,
+ "character": 1
+ },
+ "end": {
+ "line": 2,
+ "character": 5
+ }
+ },
+ "newText": "thechapter"
+ }
+ },
+ {
+ "label": "scshape",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "04",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 2,
+ "character": 1
+ },
+ "end": {
+ "line": 2,
+ "character": 5
+ }
+ },
+ "newText": "scshape"
+ }
+ },
+ {
+ "label": "fontchardp",
+ "kind": 1,
+ "detail": "amsmath.sty",
+ "preselect": false,
+ "sortText": "05",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 2,
+ "character": 1
+ },
+ "end": {
+ "line": 2,
+ "character": 5
+ }
+ },
+ "newText": "fontchardp"
+ }
+ },
+ {
+ "label": "XeTeXcharglyph",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "06",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 2,
+ "character": 1
+ },
+ "end": {
+ "line": 2,
+ "character": 5
+ }
+ },
+ "newText": "XeTeXcharglyph"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__latex__class_environment.snap b/support/texlab/tests/integration/snapshots/integration__completion__latex__class_environment.snap
new file mode 100644
index 0000000000..c893498077
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__latex__class_environment.snap
@@ -0,0 +1,30 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 4, 13)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "theindex",
+ "kind": 1,
+ "detail": "book.cls",
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 4,
+ "character": 7
+ },
+ "end": {
+ "line": 4,
+ "character": 13
+ }
+ },
+ "newText": "theindex"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__latex__class_import.snap b/support/texlab/tests/integration/snapshots/integration__completion__latex__class_import.snap
new file mode 100644
index 0000000000..baf1218114
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__latex__class_import.snap
@@ -0,0 +1,171 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 0, 19)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "book",
+ "kind": 1,
+ "documentation": {
+ "kind": "plaintext",
+ "value": "LaTeX is a widely-used macro package for TeX, providing many basic document formating commands extended by a wide range of packages. It is a development of Leslie Lamport's LaTeX 2.09, and superseded the older system in June 1994. The basic distribution is catalogued separately, at latex-base; apart from a large set of contributed packages and third-party documentation (elsewhere on the archive), the distribution includes: - a bunch of required packages, which LaTeX authors are \"entitled to assume\" will be present on any system running LaTeX; and - a minimal set of documentation detailing differences from the 'old' version of LaTeX in the areas of user commands, font selection and control, class and package writing, font encodings, configuration options and modification of LaTeX. For downloading details, see the linked catalogue entries above."
+ },
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 15
+ },
+ "end": {
+ "line": 0,
+ "character": 19
+ }
+ },
+ "newText": "book"
+ }
+ },
+ {
+ "label": "bookcover",
+ "kind": 1,
+ "documentation": {
+ "kind": "plaintext",
+ "value": "This class helps typesetting book covers and dust jackets."
+ },
+ "preselect": false,
+ "sortText": "01",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 15
+ },
+ "end": {
+ "line": 0,
+ "character": 19
+ }
+ },
+ "newText": "bookcover"
+ }
+ },
+ {
+ "label": "bookest",
+ "kind": 1,
+ "documentation": {
+ "kind": "plaintext",
+ "value": "The class extends the standard book class, in the areas of colour scheme management, document layout, headings and footers, front page layout, and other minor items."
+ },
+ "preselect": false,
+ "sortText": "02",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 15
+ },
+ "end": {
+ "line": 0,
+ "character": 19
+ }
+ },
+ "newText": "bookest"
+ }
+ },
+ {
+ "label": "bbook",
+ "kind": 1,
+ "documentation": {
+ "kind": "plaintext",
+ "value": "The bundle provides class files for writing Bangla and Assamese with LaTeX, and Metafont sources for fonts."
+ },
+ "preselect": false,
+ "sortText": "03",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 15
+ },
+ "end": {
+ "line": 0,
+ "character": 19
+ }
+ },
+ "newText": "bbook"
+ }
+ },
+ {
+ "label": "basque-book",
+ "kind": 1,
+ "documentation": {
+ "kind": "plaintext",
+ "value": "The class is derived from the LaTeX book class. The extensions solve grammatical and numeration issues that occur when book-type documents are written in Basque. The class is useful for writing books, PhD and Master Theses, etc., in Basque."
+ },
+ "preselect": false,
+ "sortText": "04",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 15
+ },
+ "end": {
+ "line": 0,
+ "character": 19
+ }
+ },
+ "newText": "basque-book"
+ }
+ },
+ {
+ "label": "caesar_book",
+ "kind": 1,
+ "documentation": {
+ "kind": "plaintext",
+ "value": "The package allows typesetting of texts with notes, figures, citations, captions and tables in the margin. This is common (for example) in science text books."
+ },
+ "preselect": false,
+ "sortText": "05",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 15
+ },
+ "end": {
+ "line": 0,
+ "character": 19
+ }
+ },
+ "newText": "caesar_book"
+ }
+ },
+ {
+ "label": "tufte-book",
+ "kind": 1,
+ "documentation": {
+ "kind": "plaintext",
+ "value": "Provided are two classes inspired, respectively, by handouts and books created by Edward Tufte."
+ },
+ "preselect": false,
+ "sortText": "06",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 15
+ },
+ "end": {
+ "line": 0,
+ "character": 19
+ }
+ },
+ "newText": "tufte-book"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__latex__color_model_define_color.snap b/support/texlab/tests/integration/snapshots/integration__completion__latex__color_model_define_color.snap
new file mode 100644
index 0000000000..5385f5c646
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__latex__color_model_define_color.snap
@@ -0,0 +1,105 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 1, 18)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "gray",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 18
+ },
+ "end": {
+ "line": 1,
+ "character": 18
+ }
+ },
+ "newText": "gray"
+ }
+ },
+ {
+ "label": "rgb",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "01",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 18
+ },
+ "end": {
+ "line": 1,
+ "character": 18
+ }
+ },
+ "newText": "rgb"
+ }
+ },
+ {
+ "label": "RGB",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "02",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 18
+ },
+ "end": {
+ "line": 1,
+ "character": 18
+ }
+ },
+ "newText": "RGB"
+ }
+ },
+ {
+ "label": "HTML",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "03",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 18
+ },
+ "end": {
+ "line": 1,
+ "character": 18
+ }
+ },
+ "newText": "HTML"
+ }
+ },
+ {
+ "label": "cmyk",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "04",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 18
+ },
+ "end": {
+ "line": 1,
+ "character": 18
+ }
+ },
+ "newText": "cmyk"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__latex__color_model_define_color_set.snap b/support/texlab/tests/integration/snapshots/integration__completion__latex__color_model_define_color_set.snap
new file mode 100644
index 0000000000..7e6f4a1de6
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__latex__color_model_define_color_set.snap
@@ -0,0 +1,67 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 2, 17)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "RGB",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 2,
+ "character": 16
+ },
+ "end": {
+ "line": 2,
+ "character": 17
+ }
+ },
+ "newText": "RGB"
+ }
+ },
+ {
+ "label": "rgb",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "01",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 2,
+ "character": 16
+ },
+ "end": {
+ "line": 2,
+ "character": 17
+ }
+ },
+ "newText": "rgb"
+ }
+ },
+ {
+ "label": "gray",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "02",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 2,
+ "character": 16
+ },
+ "end": {
+ "line": 2,
+ "character": 17
+ }
+ },
+ "newText": "gray"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__latex__color_name.snap b/support/texlab/tests/integration/snapshots/integration__completion__latex__color_name.snap
new file mode 100644
index 0000000000..b34b32f5ac
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__latex__color_name.snap
@@ -0,0 +1,143 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 0, 9)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "red",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 7
+ },
+ "end": {
+ "line": 0,
+ "character": 9
+ }
+ },
+ "newText": "red"
+ }
+ },
+ {
+ "label": "RedOrange",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "01",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 7
+ },
+ "end": {
+ "line": 0,
+ "character": 9
+ }
+ },
+ "newText": "RedOrange"
+ }
+ },
+ {
+ "label": "Red",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "02",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 7
+ },
+ "end": {
+ "line": 0,
+ "character": 9
+ }
+ },
+ "newText": "Red"
+ }
+ },
+ {
+ "label": "RedViolet",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "03",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 7
+ },
+ "end": {
+ "line": 0,
+ "character": 9
+ }
+ },
+ "newText": "RedViolet"
+ }
+ },
+ {
+ "label": "OrangeRed",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "04",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 7
+ },
+ "end": {
+ "line": 0,
+ "character": 9
+ }
+ },
+ "newText": "OrangeRed"
+ }
+ },
+ {
+ "label": "VioletRed",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "05",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 7
+ },
+ "end": {
+ "line": 0,
+ "character": 9
+ }
+ },
+ "newText": "VioletRed"
+ }
+ },
+ {
+ "label": "BrickRed",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "06",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 7
+ },
+ "end": {
+ "line": 0,
+ "character": 9
+ }
+ },
+ "newText": "BrickRed"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__latex__empty_document.snap b/support/texlab/tests/integration/snapshots/integration__completion__latex__empty_document.snap
new file mode 100644
index 0000000000..7f8638455c
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__latex__empty_document.snap
@@ -0,0 +1,9 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 0, 0)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": []
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__latex__kernel_command.snap b/support/texlab/tests/integration/snapshots/integration__completion__latex__kernel_command.snap
new file mode 100644
index 0000000000..49f65f06ca
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__latex__kernel_command.snap
@@ -0,0 +1,30 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 0, 1)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "documentclass",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 1
+ },
+ "end": {
+ "line": 0,
+ "character": 14
+ }
+ },
+ "newText": "documentclass"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__latex__kernel_command_environment.snap b/support/texlab/tests/integration/snapshots/integration__completion__latex__kernel_command_environment.snap
new file mode 100644
index 0000000000..8d01ea7b8d
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__latex__kernel_command_environment.snap
@@ -0,0 +1,30 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 6, 10)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "document",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 6,
+ "character": 7
+ },
+ "end": {
+ "line": 6,
+ "character": 10
+ }
+ },
+ "newText": "document"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__latex__kernel_command_glyph.snap b/support/texlab/tests/integration/snapshots/integration__completion__latex__kernel_command_glyph.snap
new file mode 100644
index 0000000000..293b190176
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__latex__kernel_command_glyph.snap
@@ -0,0 +1,34 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 7, 7)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "varepsilon",
+ "kind": 1,
+ "detail": "ε, built-in",
+ "documentation": {
+ "kind": "markdown",
+ "value": "![varepsilon](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADMAAAA6CAIAAACSzPa1AAAHu0lEQVR4nO2ZX0hTXxzA77n37p933rmmzmQxYaZmKlOzNKQQJEyJMqgQKn2Q/gjmQ3+ECLEXIxRJkx4CQYJ6SSoffFGaYEgoTW0G/kPMNd1ya5v7d2/bvef+Hm6YLHV/Muvh93nbzrnnfHZ2zvd87zmA4zjknwT92wJb8r9Z5Py7ZvhONRQIBOx2++Li4vz8/PT0tNFoNJvNbrebpmkMw+Li4vbt25eZmZmbm5uZmZmUlCQQCP6sWSAQMJlMo6OjOp1uYmLCaDSura0FAgEI4a+VMQyTSqVqtbq4uLiysvLw4cMkSW7ZNBctHo9Hp9Ndv359//79IpEoot8DAJDJZGVlZX19fV6vd9P2ozGjKGpgYODs2bNyuRwAEOkwb4QkyZqamrm5OQhhUC+AiyTSchw3NzfX3t7+8uVLh8MRVIphmEgkIghCKpUSBCEWizEMgxD6/X6n02m3271e76//MoqiRUVFjx8/zs3N3fh9BGZ+v7+/v//+/ftTU1PrHeA4rlAoUlNTs7Ozs7KyUlNTlUplXFycRCIRCAQYhnEc5/f73W73ly9fxsbGBgcH9Xq90+nc2C8A4MyZM69evQoehnDw+XyPHj1KTExcbys+Pr6ioqKrq2t8fNxutzMME7IRCKHT6RwYGLhw4ULQ3C8rKwuqHJYZRVGtra0ymYx3SkpKunbt2rt379xu96/zIxxcLteLFy9yc3NRFAUAaDSa/v7+iM1Yln327NmePXsQBBEKhRUVFcPDwzRNRyG0EQjh3NxcU1PT7du3379//+uQhzYzGAxpaWkIghAE0djYaLVaf9NpIyzLsiy7aVEIM5qmr169CgAQiUT37t3zeDw7qLU9IcwMBoNKpUIQpKKiwmaz7Y4TT4gdfXR01Gw2EwRRW1urUCjCjC87wnZmEMLJyUmWZZOTk7Va7W4p/SDEmPGBniRJqVS6Kz4/CWHGxzCfz0fT9K74/GQ7MxRFs7OzMQxbWVmZnZ3dNacfvW9ffOzYMZVK5XK5enp63G73NjWDVtbvm4XIHNPS0qqqqtra2l6/fq3Vauvq6iQSCV/EMIzD4TCZTCaTyWKxrK6u+nw+lmVFIpFIJJLL5SqVKj09Xa1WR5q9/SBkXFlaWiopKeFzvbt375pMpuXl5d7e3itXruTl5SUkJIjFYhQNHnsURSUSSUpKyo0bNywWSxTxLHQWxHHc5ORkXV3d2NgYhmE5OTkQwpmZGYqiUBSVyWSJiYl85kMQBIqiDMO43W6z2by0tORwODAMa2tra2hoiHTIQr8HAAC0Wu2DBw9qamqWlpb0ev36qGi12qqqqry8PKVSKZFIUBTlOA5C6PV6p6enu7u7dTodwzA2my1SrbDMGIYZHh5uaWkxm80oiu7du5emabvdDiE0GAyLi4tSqZRPXwEAEEIIIUVRLpeL32TT09NPnToVhVmIf9Pr9XZ3dz98+NBsNickJFy+fLm6utrr9ep0upGRkYWFBbvdTtM0wzB8lgsAAAAIBAKpVKpSqYqLiy9evJiTk/PrRPwtM5fL1dLS0tXVRdP0kSNHmpubjx8/LhQKEQThOO779+8Oh8Nut7vdbo/HEwgEOI7DMIzXksvl8fHxMpksCqcfbLU0PB5PY2OjWCwWCATnz59fWFiIYn39DpubBQKBjo4OgiBwHL906ZLZbN5lLW4rs6GhoeTkZABAeXm5yWTaZSeeTcxsNltZWRmCIBqNRq/X774TT7AZhLCnp0cikQiFwvb29q2S9F0g2Ozbt2+lpaUIghw6dOivTK91gpf0hw8f9Ho9iqLnzp1TKpVRLvidINhsaGhobW1NLpfzu/hfceIJNtPr9RDC5ORktVr9V4TWCTZbXl5GECQ2NjbKpGrnCDZjWRZBEJ/PFwgE/obPT4LN5HI5giBfv361WCx/qMvZ2dnOzs6FhYXIzA4ePAgAsFqtOp2O+wPXKzab7datWzdv3nzz5k2I9oOiyPPnz2NiYhAEycrKmpmZ2dkQRVFUc3OzSCRSKBRDQ0PbVw42MxqN+fn5CIKgKHr69OnPnz/vlJbX6+3o6JDL5TiONzQ0UBQVmRnLsp2dnfzCxDCspKREp9OFbGV7GIaZnZ2tr68nSRIAcOLECaPRGPKpTXb01dXV8vJyPswCAJRKZW1t7eDgoNVqDefIcx2WZZ1O59jYWFNT04EDB/h0vLi42GAwhPP45jntxMRETU2NwWDgPwIASJLMyMgoKCjIz8/XaDTx8fGxsbESiQTHcT5rZVnW7/fTNO3xeKxW6+LiosFgGB8fn56ettlsEEIcx0tLS1tbW/lFFnKtbG7GcdzIyEh9ff3Hjx+DKuA4HhMTQxAESZIEQQgEAhzHIYQsy9I0TVGU2+32er0URfF5Cv8USZLV1dV37tzhT+PCYqvBhBBOTU1VVlaKxeJw29oMoVBYVFTU29vr8/nCnwlcyDNHh8Px9OnTgoKCSP0AAFKp9OjRo0+ePDGbzVEcgYf1jm6xWN6+fdvX1zc+Pm6xWGia3vTCCwCA4zhJkmq1urCw8OTJk4WFhQqFIrqcJYI7FJ/PZzQaP336NDk5OT8/v7Ky4nK5AoGAQCCIiYlJSEhISUnJyMjIzs7WaDSJiYk4/lsXgZHdO+0m/+6d8P9mkfPvmv0HfL2v3IpRQCcAAAAASUVORK5CYII=|width=48,height=48)"
+ },
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 7,
+ "character": 1
+ },
+ "end": {
+ "line": 7,
+ "character": 7
+ }
+ },
+ "newText": "varepsilon"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__latex__label.snap b/support/texlab/tests/integration/snapshots/integration__completion__latex__label.snap
new file mode 100644
index 0000000000..efcaa02ce1
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__latex__label.snap
@@ -0,0 +1,137 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 4, 5)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "sec:bar",
+ "kind": 1,
+ "detail": "Section 2 (Bar)",
+ "preselect": false,
+ "sortText": "00 sec:bar Section 2 (Bar)",
+ "filterText": "sec:bar Section 2 (Bar)",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 4,
+ "character": 5
+ },
+ "end": {
+ "line": 4,
+ "character": 5
+ }
+ },
+ "newText": "sec:bar"
+ }
+ },
+ {
+ "label": "sec:foo",
+ "kind": 1,
+ "detail": "Section 1 (Foo)",
+ "preselect": false,
+ "sortText": "01 sec:foo Section 1 (Foo)",
+ "filterText": "sec:foo Section 1 (Foo)",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 4,
+ "character": 5
+ },
+ "end": {
+ "line": 4,
+ "character": 5
+ }
+ },
+ "newText": "sec:foo"
+ }
+ },
+ {
+ "label": "eq:foo",
+ "kind": 1,
+ "detail": "Equation (1)",
+ "preselect": false,
+ "sortText": "02 eq:foo Equation (1)",
+ "filterText": "eq:foo Equation (1)",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 4,
+ "character": 5
+ },
+ "end": {
+ "line": 4,
+ "character": 5
+ }
+ },
+ "newText": "eq:foo"
+ }
+ },
+ {
+ "label": "eq:bar",
+ "kind": 1,
+ "detail": "Equation (2)",
+ "preselect": false,
+ "sortText": "03 eq:bar Equation (2)",
+ "filterText": "eq:bar Equation (2)",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 4,
+ "character": 5
+ },
+ "end": {
+ "line": 4,
+ "character": 5
+ }
+ },
+ "newText": "eq:bar"
+ }
+ },
+ {
+ "label": "fig:baz",
+ "kind": 1,
+ "detail": "Figure 1",
+ "documentation": "Baz",
+ "preselect": false,
+ "sortText": "04 fig:baz Figure 1: Baz",
+ "filterText": "fig:baz Figure 1: Baz",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 4,
+ "character": 5
+ },
+ "end": {
+ "line": 4,
+ "character": 5
+ }
+ },
+ "newText": "fig:baz"
+ }
+ },
+ {
+ "label": "thm:foo",
+ "kind": 1,
+ "detail": "Lemma 1",
+ "preselect": false,
+ "sortText": "05 thm:foo Lemma 1",
+ "filterText": "thm:foo Lemma 1",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 4,
+ "character": 5
+ },
+ "end": {
+ "line": 4,
+ "character": 5
+ }
+ },
+ "newText": "thm:foo"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__latex__package_command.snap b/support/texlab/tests/integration/snapshots/integration__completion__latex__package_command.snap
new file mode 100644
index 0000000000..6a260fe387
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__latex__package_command.snap
@@ -0,0 +1,34 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 3, 7)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "varDelta",
+ "kind": 1,
+ "detail": "amsmath.sty",
+ "documentation": {
+ "kind": "markdown",
+ "value": "![varDelta](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADoAAAA4CAIAAAAjEXx0AAAF60lEQVR4nO2ab0gTfxzHb0PvNjfERqXOqWyIjaRCUUzc0FUoJaWooEUQPSiIIaFIiaKGYE96IIlECyrBCltqjSwVI9AHJiJapmu1NGkYkXP/0bndnx4Y8u1st+/Nbf5+4Ovh3efzude+9952dxuHoijk/wN3pwXYsasbSnZ1/TE/P3/69GmxWHz9+nWv18uumQovFovl3LlzXC4XQZC0tLSlpSVW7WFdXa/Xe/fu3d7eXpIkEQQxmUxGo5HVhPDpUhQ1ODjY1ta2vr6+scXlck1OTrKeEh7m5uaOHDlCO3pZWZnb7YYfEibdlZWV8vJyLpfL4XBAXblcbjKZ4OeEIwwej6ejo+Ply5cKheLAgQPgrqWlpS9fvsCPCrkuRVH9/f3t7e0ymaytrS0vLw/cyzq+ITv/f5iZmUlLSxOJRFqtliCIzs7OyMhIUKCkpAQ+vqHV/fXrV3FxMYZhTU1NG07T09P79+8HdVNTU79//w45MIS6bre7oaEBRdHS0lKz2byx0WKxHD16FNQVCATDw8OQM0OlSxBEd3f3nj17Dh8+PDs7u7kdx/ErV66AuhwO5+bNm5BjQ6U7NTUll8v37dun0+lIkgR3dXV1RUREgMZnzpxZW1uDGRsS3Z8/f546dYrH47W2tno8HtreDx8+xMbGgropKSmLi4swk4Ovu7a2du3aNRRFKysrLRbL1gKr1ZqTk0OL79DQEMzwIOsSBNHV1RUTE5ORkWEwGHzVqNVqWnxbWlpomQmH7sTEREpKSlxc3MDAAMPhHz16RPv0LSoqWl1d9Ts/mLo/fvwoKCjg8/m3bt3aGlmQmZmZuLg4UFcmky0sLPg9RNB0V1dXq6urURS9cOGCzWZjLrZarbm5uaBuVFTU69ev/R4lOLoEQTx48CA6Ojo7O3t+fh6mvqqqihbfGzdu+I1vcHTHxsakUqlYLH7z5g3MO4aiqCdPnqAoChqfPHnSb3yDoGsymVQqlUAgaG9v93q9kF2zs7Px8fGgrlQq9XtmtqvrcrnUajWKopcuXXI4HPCNNptNoVCAunw+v7+/n7lrW7o4jms0GqFQqFAoIL+WNiEI4urVq7T4NjY2MmdpW7qjo6NJSUlJSUkjIyMBtHd3d9PiW1hY6HK5GFoC111cXFQqlUKhUKPR4DgewIS5uTlafJOTk41GI0NLgLpOp/Py5csoiqrVaub1YMButyuVSlCXx+PpdDqGlkB0cRzv6OgQCAQqlYrVfSwNgiCqq6uRv6mvr2eIL2tdkiTfvn0rkUhkMtnY2FjArhtotVpafE+cOOF0On3Vs9ZdWFjIycmJjo5++PAhQRDbs6X0en1CQgKom5iY+PnzZ1/17HQdDsfFixcxDKupqYG5gPKL3W7Pz88HdTEMe/78ua96Fs8ZcBy/f//+06dP8/Pza2tr+Xw+fK8vhEJhZmYmuGV9fX18fJzy9YsJ5DKQJDk0NBQfH5+amjo5Obn9dd2kp6cHwzBQ6dixY77iC6trNBqzsrJiYmIeP368/ciCfPr0SSKRgLoSicTXnQiUrs1mO3/+PIZhdXV1kLes8DgcDpVKBepiGNbT0/PPYv/Z9Xq9Go2mt7e3oKCgpqaGx+NB5hISX/HdeGRNh/mlkyT56tWr2NjYgwcPvn//PrjruklfXx8tvnl5eXa7fWulH12DwZCeni4SiZ49ewZ53R0ABoMhMTER1BWLxXq9fmslk67FYqmoqODxeM3NzayecbPF6XQeP34c1EVRVKvVbq30mV2Px3Pnzh2dTldUVFRVVUU7WcElKioqKysLfLDu8XjevXv3j/j+8+WSJPnixYu9e/ceOnTo48ePROjp6+ujvYmVSqXVaqWJRdD1EQRBEL1e39jYaDabExISbt++TftBIRQsLy9Tf3+Tff369du3b+np6X/VbV1as9lcWloaBkVmUBS9d+8ezY2u63a7m5qaaBd1O8XZs2f96HZ2dopEop32/INUKvWjK5fLdzwGG3A4HIlEQtPjUL4u1f6T7P6fIZTs6oaS3/oaUpPRFnIuAAAAAElFTkSuQmCC|width=48,height=48)"
+ },
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 3,
+ "character": 1
+ },
+ "end": {
+ "line": 3,
+ "character": 7
+ }
+ },
+ "newText": "varDelta"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__latex__package_environment.snap b/support/texlab/tests/integration/snapshots/integration__completion__latex__package_environment.snap
new file mode 100644
index 0000000000..7dbe95a81b
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__latex__package_environment.snap
@@ -0,0 +1,150 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 5, 5)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "alignedat",
+ "kind": 1,
+ "detail": "amsmath.sty",
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 5,
+ "character": 5
+ },
+ "end": {
+ "line": 5,
+ "character": 9
+ }
+ },
+ "newText": "alignedat"
+ }
+ },
+ {
+ "label": "aligned",
+ "kind": 1,
+ "detail": "amsmath.sty",
+ "preselect": false,
+ "sortText": "01",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 5,
+ "character": 5
+ },
+ "end": {
+ "line": 5,
+ "character": 9
+ }
+ },
+ "newText": "aligned"
+ }
+ },
+ {
+ "label": "alignat",
+ "kind": 1,
+ "detail": "amsmath.sty",
+ "preselect": false,
+ "sortText": "02",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 5,
+ "character": 5
+ },
+ "end": {
+ "line": 5,
+ "character": 9
+ }
+ },
+ "newText": "alignat"
+ }
+ },
+ {
+ "label": "align",
+ "kind": 1,
+ "detail": "amsmath.sty",
+ "preselect": false,
+ "sortText": "03",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 5,
+ "character": 5
+ },
+ "end": {
+ "line": 5,
+ "character": 9
+ }
+ },
+ "newText": "align"
+ }
+ },
+ {
+ "label": "align*",
+ "kind": 1,
+ "detail": "amsmath.sty",
+ "preselect": false,
+ "sortText": "04",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 5,
+ "character": 5
+ },
+ "end": {
+ "line": 5,
+ "character": 9
+ }
+ },
+ "newText": "align*"
+ }
+ },
+ {
+ "label": "alignat*",
+ "kind": 1,
+ "detail": "amsmath.sty",
+ "preselect": false,
+ "sortText": "05",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 5,
+ "character": 5
+ },
+ "end": {
+ "line": 5,
+ "character": 9
+ }
+ },
+ "newText": "alignat*"
+ }
+ },
+ {
+ "label": "flalign",
+ "kind": 1,
+ "detail": "amsmath.sty",
+ "preselect": false,
+ "sortText": "06",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 5,
+ "character": 5
+ },
+ "end": {
+ "line": 5,
+ "character": 9
+ }
+ },
+ "newText": "flalign"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__latex__package_import.snap b/support/texlab/tests/integration/snapshots/integration__completion__latex__package_import.snap
new file mode 100644
index 0000000000..992ea91692
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__latex__package_import.snap
@@ -0,0 +1,102 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 1, 15)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "amsmath",
+ "kind": 1,
+ "documentation": {
+ "kind": "plaintext",
+ "value": "The package provides the principal packages in the AMS-LaTeX distribution. It adapts for use in LaTeX most of the mathematical features found in AMS-TeX; it is highly recommended as an adjunct to serious mathematical typesetting in LaTeX. When amsmath is loaded, AMS-LaTeX packages amsbsy (for bold symbols), amsopn (for operator names) and amstext (for text embedded in mathematics) are also loaded. amsmath is part of the LaTeX required distribution; however, several contributed packages add still further to its appeal; examples are empheq, which provides functions for decorating and highlighting mathematics, and ntheorem, for specifying theorem (and similar) definitions."
+ },
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 12
+ },
+ "end": {
+ "line": 1,
+ "character": 19
+ }
+ },
+ "newText": "amsmath"
+ }
+ },
+ {
+ "label": "lwarp-amsmath",
+ "kind": 1,
+ "documentation": {
+ "kind": "plaintext",
+ "value": "This package converts LaTeX to HTML by using LaTeX to process the user's document and generate HTML tags. External utility programs are only used for the final conversion of text and images. Math may be represented by SVG files or MathJax. Hundreds of LaTeX packages are supported, and their load order is automatically verified. Documents may be produced by LaTeX, LuaLaTeX, XeLaTeX, and by several CJK engines, classes, and packages. A texlua script automates compilation, index, glossary, and batch image processing, and also supports latexmk. Configuration is semi-automatic at the first manual compile. Support files are self-generated. Print and HTML versions of each document may coexist. Assistance is provided for HTML import into EPUB conversion software and word processors. Requirements include the commonly-available Poppler utilities, and Perl. Detailed installation instructions are included for each of the major operating systems and TeX distributions. A quick-start tutorial is provided."
+ },
+ "preselect": false,
+ "sortText": "01",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 12
+ },
+ "end": {
+ "line": 1,
+ "character": 19
+ }
+ },
+ "newText": "lwarp-amsmath"
+ }
+ },
+ {
+ "label": "simurgh-amsmath",
+ "kind": 1,
+ "documentation": {
+ "kind": "plaintext",
+ "value": "The package provides an automatic and unified interface for Parsi typesetting in LaTeX, using the LuaTeX engine. The project to produce this system is dedicated to Ferdowsi The Great."
+ },
+ "preselect": false,
+ "sortText": "02",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 12
+ },
+ "end": {
+ "line": 1,
+ "character": 19
+ }
+ },
+ "newText": "simurgh-amsmath"
+ }
+ },
+ {
+ "label": "onlyamsmath",
+ "kind": 1,
+ "documentation": {
+ "kind": "plaintext",
+ "value": "This package inhibits the usage of plain TeX and (on demand) of standard LaTeX mathematics environments. This is useful for class writers who want to encourage their users to use the environments provided by the amsmath package."
+ },
+ "preselect": false,
+ "sortText": "03",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 12
+ },
+ "end": {
+ "line": 1,
+ "character": 19
+ }
+ },
+ "newText": "onlyamsmath"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__latex__pgf_library.snap b/support/texlab/tests/integration/snapshots/integration__completion__latex__pgf_library.snap
new file mode 100644
index 0000000000..da34563c8d
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__latex__pgf_library.snap
@@ -0,0 +1,143 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 1, 15)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "arrows",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 15
+ },
+ "end": {
+ "line": 1,
+ "character": 15
+ }
+ },
+ "newText": "arrows"
+ }
+ },
+ {
+ "label": "arrows.meta",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "01",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 15
+ },
+ "end": {
+ "line": 1,
+ "character": 15
+ }
+ },
+ "newText": "arrows.meta"
+ }
+ },
+ {
+ "label": "arrows.spaced",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "02",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 15
+ },
+ "end": {
+ "line": 1,
+ "character": 15
+ }
+ },
+ "newText": "arrows.spaced"
+ }
+ },
+ {
+ "label": "curvilinear",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "03",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 15
+ },
+ "end": {
+ "line": 1,
+ "character": 15
+ }
+ },
+ "newText": "curvilinear"
+ }
+ },
+ {
+ "label": "datavisualization.barcharts",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "04",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 15
+ },
+ "end": {
+ "line": 1,
+ "character": 15
+ }
+ },
+ "newText": "datavisualization.barcharts"
+ }
+ },
+ {
+ "label": "datavisualization.formats.functions",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "05",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 15
+ },
+ "end": {
+ "line": 1,
+ "character": 15
+ }
+ },
+ "newText": "datavisualization.formats.functions"
+ }
+ },
+ {
+ "label": "datavisualization.polar",
+ "kind": 1,
+ "preselect": false,
+ "sortText": "06",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 15
+ },
+ "end": {
+ "line": 1,
+ "character": 15
+ }
+ },
+ "newText": "datavisualization.polar"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__latex__preselect_environment.snap b/support/texlab/tests/integration/snapshots/integration__completion__latex__preselect_environment.snap
new file mode 100644
index 0000000000..791f045476
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__latex__preselect_environment.snap
@@ -0,0 +1,150 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 1, 5)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "document",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": true,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 5
+ },
+ "end": {
+ "line": 1,
+ "character": 5
+ }
+ },
+ "newText": "document"
+ }
+ },
+ {
+ "label": "abstract",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "01",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 5
+ },
+ "end": {
+ "line": 1,
+ "character": 5
+ }
+ },
+ "newText": "abstract"
+ }
+ },
+ {
+ "label": "array",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "02",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 5
+ },
+ "end": {
+ "line": 1,
+ "character": 5
+ }
+ },
+ "newText": "array"
+ }
+ },
+ {
+ "label": "center",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "03",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 5
+ },
+ "end": {
+ "line": 1,
+ "character": 5
+ }
+ },
+ "newText": "center"
+ }
+ },
+ {
+ "label": "csname",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "04",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 5
+ },
+ "end": {
+ "line": 1,
+ "character": 5
+ }
+ },
+ "newText": "csname"
+ }
+ },
+ {
+ "label": "description",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "05",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 5
+ },
+ "end": {
+ "line": 1,
+ "character": 5
+ }
+ },
+ "newText": "description"
+ }
+ },
+ {
+ "label": "displaymath",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "06",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 5
+ },
+ "end": {
+ "line": 1,
+ "character": 5
+ }
+ },
+ "newText": "displaymath"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__latex__theorem_environment.snap b/support/texlab/tests/integration/snapshots/integration__completion__latex__theorem_environment.snap
new file mode 100644
index 0000000000..cc0b336e9f
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__latex__theorem_environment.snap
@@ -0,0 +1,130 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 3, 7)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "figure",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 3,
+ "character": 7
+ },
+ "end": {
+ "line": 3,
+ "character": 8
+ }
+ },
+ "newText": "figure"
+ }
+ },
+ {
+ "label": "filecontents",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "01",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 3,
+ "character": 7
+ },
+ "end": {
+ "line": 3,
+ "character": 8
+ }
+ },
+ "newText": "filecontents"
+ }
+ },
+ {
+ "label": "flushleft",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "02",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 3,
+ "character": 7
+ },
+ "end": {
+ "line": 3,
+ "character": 8
+ }
+ },
+ "newText": "flushleft"
+ }
+ },
+ {
+ "label": "flushright",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "03",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 3,
+ "character": 7
+ },
+ "end": {
+ "line": 3,
+ "character": 8
+ }
+ },
+ "newText": "flushright"
+ }
+ },
+ {
+ "label": "foo",
+ "kind": 1,
+ "detail": "user-defined",
+ "preselect": false,
+ "sortText": "04",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 3,
+ "character": 7
+ },
+ "end": {
+ "line": 3,
+ "character": 8
+ }
+ },
+ "newText": "foo"
+ }
+ },
+ {
+ "label": "proof",
+ "kind": 1,
+ "detail": "amsthm.sty",
+ "preselect": false,
+ "sortText": "05",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 3,
+ "character": 7
+ },
+ "end": {
+ "line": 3,
+ "character": 8
+ }
+ },
+ "newText": "proof"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__latex__user_command.snap b/support/texlab/tests/integration/snapshots/integration__completion__latex__user_command.snap
new file mode 100644
index 0000000000..f2f2eef4a5
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__latex__user_command.snap
@@ -0,0 +1,30 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 1, 3)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "foobar",
+ "kind": 1,
+ "detail": "user-defined",
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 1
+ },
+ "end": {
+ "line": 1,
+ "character": 6
+ }
+ },
+ "newText": "foobar"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__completion__latex__user_environment.snap b/support/texlab/tests/integration/snapshots/integration__completion__latex__user_environment.snap
new file mode 100644
index 0000000000..0f85b3fa56
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__completion__latex__user_environment.snap
@@ -0,0 +1,50 @@
+---
+source: tests/integration/completion.rs
+expression: "complete_and_resolve(&server, uri, 4, 7)?"
+
+---
+{
+ "isIncomplete": false,
+ "items": [
+ {
+ "label": "foo",
+ "kind": 1,
+ "detail": "user-defined",
+ "preselect": false,
+ "sortText": "00",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 4,
+ "character": 7
+ },
+ "end": {
+ "line": 4,
+ "character": 9
+ }
+ },
+ "newText": "foo"
+ }
+ },
+ {
+ "label": "filecontents",
+ "kind": 1,
+ "detail": "built-in",
+ "preselect": false,
+ "sortText": "01",
+ "textEdit": {
+ "range": {
+ "start": {
+ "line": 4,
+ "character": 7
+ },
+ "end": {
+ "line": 4,
+ "character": 9
+ }
+ },
+ "newText": "filecontents"
+ }
+ }
+ ]
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__document_symbol__enumerate_flat.snap b/support/texlab/tests/integration/snapshots/integration__document_symbol__enumerate_flat.snap
new file mode 100644
index 0000000000..1f042106a2
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__document_symbol__enumerate_flat.snap
@@ -0,0 +1,97 @@
+---
+source: tests/integration/document_symbol.rs
+expression: server.find_document_symbols(uri)?
+
+---
+[
+ {
+ "name": "Enumerate",
+ "kind": 10,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 4,
+ "character": 0
+ },
+ "end": {
+ "line": 9,
+ "character": 15
+ }
+ }
+ }
+ },
+ {
+ "name": "1",
+ "kind": 22,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 5,
+ "character": 4
+ },
+ "end": {
+ "line": 5,
+ "character": 27
+ }
+ }
+ }
+ },
+ {
+ "name": "Item",
+ "kind": 22,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 6,
+ "character": 4
+ },
+ "end": {
+ "line": 6,
+ "character": 27
+ }
+ }
+ }
+ },
+ {
+ "name": "Baz",
+ "kind": 22,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 7,
+ "character": 4
+ },
+ "end": {
+ "line": 7,
+ "character": 18
+ }
+ }
+ }
+ },
+ {
+ "name": "2",
+ "kind": 22,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 8,
+ "character": 4
+ },
+ "end": {
+ "line": 8,
+ "character": 32
+ }
+ }
+ }
+ }
+]
diff --git a/support/texlab/tests/integration/snapshots/integration__document_symbol__enumerate_nested.snap b/support/texlab/tests/integration/snapshots/integration__document_symbol__enumerate_nested.snap
new file mode 100644
index 0000000000..2584b7ef50
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__document_symbol__enumerate_nested.snap
@@ -0,0 +1,141 @@
+---
+source: tests/integration/document_symbol.rs
+expression: server.find_document_symbols(uri)?
+
+---
+[
+ {
+ "name": "Enumerate",
+ "kind": 10,
+ "deprecated": false,
+ "range": {
+ "start": {
+ "line": 4,
+ "character": 0
+ },
+ "end": {
+ "line": 9,
+ "character": 15
+ }
+ },
+ "selectionRange": {
+ "start": {
+ "line": 4,
+ "character": 0
+ },
+ "end": {
+ "line": 9,
+ "character": 15
+ }
+ },
+ "children": [
+ {
+ "name": "1",
+ "detail": "it:foo",
+ "kind": 22,
+ "deprecated": false,
+ "range": {
+ "start": {
+ "line": 5,
+ "character": 4
+ },
+ "end": {
+ "line": 5,
+ "character": 27
+ }
+ },
+ "selectionRange": {
+ "start": {
+ "line": 5,
+ "character": 9
+ },
+ "end": {
+ "line": 5,
+ "character": 23
+ }
+ },
+ "children": []
+ },
+ {
+ "name": "Item",
+ "detail": "it:bar",
+ "kind": 22,
+ "deprecated": false,
+ "range": {
+ "start": {
+ "line": 6,
+ "character": 4
+ },
+ "end": {
+ "line": 6,
+ "character": 27
+ }
+ },
+ "selectionRange": {
+ "start": {
+ "line": 6,
+ "character": 9
+ },
+ "end": {
+ "line": 6,
+ "character": 23
+ }
+ },
+ "children": []
+ },
+ {
+ "name": "Baz",
+ "kind": 22,
+ "deprecated": false,
+ "range": {
+ "start": {
+ "line": 7,
+ "character": 4
+ },
+ "end": {
+ "line": 7,
+ "character": 18
+ }
+ },
+ "selectionRange": {
+ "start": {
+ "line": 7,
+ "character": 4
+ },
+ "end": {
+ "line": 7,
+ "character": 18
+ }
+ },
+ "children": []
+ },
+ {
+ "name": "2",
+ "detail": "it:qux",
+ "kind": 22,
+ "deprecated": false,
+ "range": {
+ "start": {
+ "line": 8,
+ "character": 4
+ },
+ "end": {
+ "line": 8,
+ "character": 32
+ }
+ },
+ "selectionRange": {
+ "start": {
+ "line": 8,
+ "character": 14
+ },
+ "end": {
+ "line": 8,
+ "character": 28
+ }
+ },
+ "children": []
+ }
+ ]
+ }
+]
diff --git a/support/texlab/tests/integration/snapshots/integration__document_symbol__equation_flat.snap b/support/texlab/tests/integration/snapshots/integration__document_symbol__equation_flat.snap
new file mode 100644
index 0000000000..1f4b3bfe4d
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__document_symbol__equation_flat.snap
@@ -0,0 +1,61 @@
+---
+source: tests/integration/document_symbol.rs
+expression: server.find_document_symbols(uri)?
+
+---
+[
+ {
+ "name": "Equation (1)",
+ "kind": 14,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 4,
+ "character": 0
+ },
+ "end": {
+ "line": 6,
+ "character": 14
+ }
+ }
+ }
+ },
+ {
+ "name": "Equation",
+ "kind": 14,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 8,
+ "character": 0
+ },
+ "end": {
+ "line": 10,
+ "character": 14
+ }
+ }
+ }
+ },
+ {
+ "name": "Equation",
+ "kind": 14,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 12,
+ "character": 0
+ },
+ "end": {
+ "line": 14,
+ "character": 14
+ }
+ }
+ }
+ }
+]
diff --git a/support/texlab/tests/integration/snapshots/integration__document_symbol__equation_nested.snap b/support/texlab/tests/integration/snapshots/integration__document_symbol__equation_nested.snap
new file mode 100644
index 0000000000..9ffb191b89
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__document_symbol__equation_nested.snap
@@ -0,0 +1,87 @@
+---
+source: tests/integration/document_symbol.rs
+expression: server.find_document_symbols(uri)?
+
+---
+[
+ {
+ "name": "Equation (1)",
+ "detail": "eq:foo",
+ "kind": 14,
+ "deprecated": false,
+ "range": {
+ "start": {
+ "line": 4,
+ "character": 0
+ },
+ "end": {
+ "line": 6,
+ "character": 14
+ }
+ },
+ "selectionRange": {
+ "start": {
+ "line": 4,
+ "character": 16
+ },
+ "end": {
+ "line": 4,
+ "character": 30
+ }
+ },
+ "children": []
+ },
+ {
+ "name": "Equation",
+ "detail": "eq:bar",
+ "kind": 14,
+ "deprecated": false,
+ "range": {
+ "start": {
+ "line": 8,
+ "character": 0
+ },
+ "end": {
+ "line": 10,
+ "character": 14
+ }
+ },
+ "selectionRange": {
+ "start": {
+ "line": 8,
+ "character": 16
+ },
+ "end": {
+ "line": 8,
+ "character": 30
+ }
+ },
+ "children": []
+ },
+ {
+ "name": "Equation",
+ "kind": 14,
+ "deprecated": false,
+ "range": {
+ "start": {
+ "line": 12,
+ "character": 0
+ },
+ "end": {
+ "line": 14,
+ "character": 14
+ }
+ },
+ "selectionRange": {
+ "start": {
+ "line": 12,
+ "character": 0
+ },
+ "end": {
+ "line": 14,
+ "character": 14
+ }
+ },
+ "children": []
+ }
+]
diff --git a/support/texlab/tests/integration/snapshots/integration__document_symbol__float_flat.snap b/support/texlab/tests/integration/snapshots/integration__document_symbol__float_flat.snap
new file mode 100644
index 0000000000..c7465ddcc0
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__document_symbol__float_flat.snap
@@ -0,0 +1,61 @@
+---
+source: tests/integration/document_symbol.rs
+expression: server.find_document_symbols(uri)?
+
+---
+[
+ {
+ "name": "Figure 1: Foo",
+ "kind": 6,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 4,
+ "character": 0
+ },
+ "end": {
+ "line": 7,
+ "character": 12
+ }
+ }
+ }
+ },
+ {
+ "name": "Figure: Bar",
+ "kind": 6,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 9,
+ "character": 0
+ },
+ "end": {
+ "line": 12,
+ "character": 12
+ }
+ }
+ }
+ },
+ {
+ "name": "Figure: Baz",
+ "kind": 6,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 14,
+ "character": 0
+ },
+ "end": {
+ "line": 17,
+ "character": 12
+ }
+ }
+ }
+ }
+]
diff --git a/support/texlab/tests/integration/snapshots/integration__document_symbol__float_nested.snap b/support/texlab/tests/integration/snapshots/integration__document_symbol__float_nested.snap
new file mode 100644
index 0000000000..886ffc560c
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__document_symbol__float_nested.snap
@@ -0,0 +1,87 @@
+---
+source: tests/integration/document_symbol.rs
+expression: server.find_document_symbols(uri)?
+
+---
+[
+ {
+ "name": "Figure 1: Foo",
+ "detail": "fig:foo",
+ "kind": 6,
+ "deprecated": false,
+ "range": {
+ "start": {
+ "line": 4,
+ "character": 0
+ },
+ "end": {
+ "line": 7,
+ "character": 12
+ }
+ },
+ "selectionRange": {
+ "start": {
+ "line": 6,
+ "character": 17
+ },
+ "end": {
+ "line": 6,
+ "character": 32
+ }
+ },
+ "children": []
+ },
+ {
+ "name": "Figure: Bar",
+ "detail": "fig:bar",
+ "kind": 6,
+ "deprecated": false,
+ "range": {
+ "start": {
+ "line": 9,
+ "character": 0
+ },
+ "end": {
+ "line": 12,
+ "character": 12
+ }
+ },
+ "selectionRange": {
+ "start": {
+ "line": 11,
+ "character": 17
+ },
+ "end": {
+ "line": 11,
+ "character": 32
+ }
+ },
+ "children": []
+ },
+ {
+ "name": "Figure: Baz",
+ "kind": 6,
+ "deprecated": false,
+ "range": {
+ "start": {
+ "line": 14,
+ "character": 0
+ },
+ "end": {
+ "line": 17,
+ "character": 12
+ }
+ },
+ "selectionRange": {
+ "start": {
+ "line": 14,
+ "character": 0
+ },
+ "end": {
+ "line": 17,
+ "character": 12
+ }
+ },
+ "children": []
+ }
+]
diff --git a/support/texlab/tests/integration/snapshots/integration__document_symbol__section_flat.snap b/support/texlab/tests/integration/snapshots/integration__document_symbol__section_flat.snap
new file mode 100644
index 0000000000..2f7ad319a7
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__document_symbol__section_flat.snap
@@ -0,0 +1,61 @@
+---
+source: tests/integration/document_symbol.rs
+expression: server.find_document_symbols(uri)?
+
+---
+[
+ {
+ "name": "Foo",
+ "kind": 2,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 4,
+ "character": 0
+ },
+ "end": {
+ "line": 4,
+ "character": 13
+ }
+ }
+ }
+ },
+ {
+ "name": "2 Bar",
+ "kind": 2,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 6,
+ "character": 0
+ },
+ "end": {
+ "line": 8,
+ "character": 31
+ }
+ }
+ }
+ },
+ {
+ "name": "Baz",
+ "kind": 2,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 8,
+ "character": 0
+ },
+ "end": {
+ "line": 8,
+ "character": 31
+ }
+ }
+ }
+ }
+]
diff --git a/support/texlab/tests/integration/snapshots/integration__document_symbol__section_nested.snap b/support/texlab/tests/integration/snapshots/integration__document_symbol__section_nested.snap
new file mode 100644
index 0000000000..d5b5eb4b0c
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__document_symbol__section_nested.snap
@@ -0,0 +1,88 @@
+---
+source: tests/integration/document_symbol.rs
+expression: server.find_document_symbols(uri)?
+
+---
+[
+ {
+ "name": "Foo",
+ "kind": 2,
+ "deprecated": false,
+ "range": {
+ "start": {
+ "line": 4,
+ "character": 0
+ },
+ "end": {
+ "line": 4,
+ "character": 13
+ }
+ },
+ "selectionRange": {
+ "start": {
+ "line": 4,
+ "character": 0
+ },
+ "end": {
+ "line": 4,
+ "character": 13
+ }
+ },
+ "children": []
+ },
+ {
+ "name": "2 Bar",
+ "detail": "sec:bar",
+ "kind": 2,
+ "deprecated": false,
+ "range": {
+ "start": {
+ "line": 6,
+ "character": 0
+ },
+ "end": {
+ "line": 8,
+ "character": 31
+ }
+ },
+ "selectionRange": {
+ "start": {
+ "line": 6,
+ "character": 13
+ },
+ "end": {
+ "line": 6,
+ "character": 28
+ }
+ },
+ "children": [
+ {
+ "name": "Baz",
+ "detail": "sec:baz",
+ "kind": 2,
+ "deprecated": false,
+ "range": {
+ "start": {
+ "line": 8,
+ "character": 0
+ },
+ "end": {
+ "line": 8,
+ "character": 31
+ }
+ },
+ "selectionRange": {
+ "start": {
+ "line": 8,
+ "character": 16
+ },
+ "end": {
+ "line": 8,
+ "character": 31
+ }
+ },
+ "children": []
+ }
+ ]
+ }
+]
diff --git a/support/texlab/tests/integration/snapshots/integration__document_symbol__theorem_flat.snap b/support/texlab/tests/integration/snapshots/integration__document_symbol__theorem_flat.snap
new file mode 100644
index 0000000000..8ce3824bea
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__document_symbol__theorem_flat.snap
@@ -0,0 +1,79 @@
+---
+source: tests/integration/document_symbol.rs
+expression: server.find_document_symbols(uri)?
+
+---
+[
+ {
+ "name": "Lemma 1 (Foo)",
+ "kind": 13,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 6,
+ "character": 0
+ },
+ "end": {
+ "line": 8,
+ "character": 11
+ }
+ }
+ }
+ },
+ {
+ "name": "Lemma 2",
+ "kind": 13,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 10,
+ "character": 0
+ },
+ "end": {
+ "line": 12,
+ "character": 11
+ }
+ }
+ }
+ },
+ {
+ "name": "Lemma",
+ "kind": 13,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 14,
+ "character": 0
+ },
+ "end": {
+ "line": 16,
+ "character": 11
+ }
+ }
+ }
+ },
+ {
+ "name": "Lemma (Qux)",
+ "kind": 13,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 18,
+ "character": 0
+ },
+ "end": {
+ "line": 20,
+ "character": 11
+ }
+ }
+ }
+ }
+]
diff --git a/support/texlab/tests/integration/snapshots/integration__document_symbol__theorem_nested.snap b/support/texlab/tests/integration/snapshots/integration__document_symbol__theorem_nested.snap
new file mode 100644
index 0000000000..052f62fec7
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__document_symbol__theorem_nested.snap
@@ -0,0 +1,114 @@
+---
+source: tests/integration/document_symbol.rs
+expression: server.find_document_symbols(uri)?
+
+---
+[
+ {
+ "name": "Lemma 1 (Foo)",
+ "detail": "thm:foo",
+ "kind": 13,
+ "deprecated": false,
+ "range": {
+ "start": {
+ "line": 6,
+ "character": 0
+ },
+ "end": {
+ "line": 8,
+ "character": 11
+ }
+ },
+ "selectionRange": {
+ "start": {
+ "line": 6,
+ "character": 18
+ },
+ "end": {
+ "line": 6,
+ "character": 33
+ }
+ },
+ "children": []
+ },
+ {
+ "name": "Lemma 2",
+ "detail": "thm:bar",
+ "kind": 13,
+ "deprecated": false,
+ "range": {
+ "start": {
+ "line": 10,
+ "character": 0
+ },
+ "end": {
+ "line": 12,
+ "character": 11
+ }
+ },
+ "selectionRange": {
+ "start": {
+ "line": 10,
+ "character": 13
+ },
+ "end": {
+ "line": 10,
+ "character": 28
+ }
+ },
+ "children": []
+ },
+ {
+ "name": "Lemma",
+ "detail": "thm:baz",
+ "kind": 13,
+ "deprecated": false,
+ "range": {
+ "start": {
+ "line": 14,
+ "character": 0
+ },
+ "end": {
+ "line": 16,
+ "character": 11
+ }
+ },
+ "selectionRange": {
+ "start": {
+ "line": 14,
+ "character": 13
+ },
+ "end": {
+ "line": 14,
+ "character": 28
+ }
+ },
+ "children": []
+ },
+ {
+ "name": "Lemma (Qux)",
+ "kind": 13,
+ "deprecated": false,
+ "range": {
+ "start": {
+ "line": 18,
+ "character": 0
+ },
+ "end": {
+ "line": 20,
+ "character": 11
+ }
+ },
+ "selectionRange": {
+ "start": {
+ "line": 18,
+ "character": 0
+ },
+ "end": {
+ "line": 20,
+ "character": 11
+ }
+ },
+ "children": []
+ }
+]
diff --git a/support/texlab/tests/integration/snapshots/integration__hover__empty_bibtex_document.snap b/support/texlab/tests/integration/snapshots/integration__hover__empty_bibtex_document.snap
new file mode 100644
index 0000000000..e064602aa1
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__hover__empty_bibtex_document.snap
@@ -0,0 +1,6 @@
+---
+source: tests/integration/hover.rs
+expression: "server.hover(uri, 0, 0)?"
+
+---
+null
diff --git a/support/texlab/tests/integration/snapshots/integration__hover__empty_labtex_document.snap b/support/texlab/tests/integration/snapshots/integration__hover__empty_labtex_document.snap
new file mode 100644
index 0000000000..e064602aa1
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__hover__empty_labtex_document.snap
@@ -0,0 +1,6 @@
+---
+source: tests/integration/hover.rs
+expression: "server.hover(uri, 0, 0)?"
+
+---
+null
diff --git a/support/texlab/tests/integration/snapshots/integration__hover__label_theorem_child_file.snap b/support/texlab/tests/integration/snapshots/integration__hover__label_theorem_child_file.snap
new file mode 100644
index 0000000000..758c89cfac
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__hover__label_theorem_child_file.snap
@@ -0,0 +1,21 @@
+---
+source: tests/integration/hover.rs
+expression: "server.hover(uri, 3, 8)?"
+
+---
+{
+ "contents": {
+ "kind": "plaintext",
+ "value": "Lemma"
+ },
+ "range": {
+ "start": {
+ "line": 3,
+ "character": 5
+ },
+ "end": {
+ "line": 3,
+ "character": 12
+ }
+ }
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__hover__label_theorem_child_file_mumber.snap b/support/texlab/tests/integration/snapshots/integration__hover__label_theorem_child_file_mumber.snap
new file mode 100644
index 0000000000..43737f72a1
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__hover__label_theorem_child_file_mumber.snap
@@ -0,0 +1,21 @@
+---
+source: tests/integration/hover.rs
+expression: "server.hover(uri, 3, 8)?"
+
+---
+{
+ "contents": {
+ "kind": "plaintext",
+ "value": "Lemma 1 (Foo)"
+ },
+ "range": {
+ "start": {
+ "line": 3,
+ "character": 5
+ },
+ "end": {
+ "line": 3,
+ "character": 12
+ }
+ }
+}
diff --git a/support/texlab/tests/integration/snapshots/integration__workspace_symbol__filter_bibtex.snap b/support/texlab/tests/integration/snapshots/integration__workspace_symbol__filter_bibtex.snap
new file mode 100644
index 0000000000..bac8d65144
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__workspace_symbol__filter_bibtex.snap
@@ -0,0 +1,43 @@
+---
+source: tests/integration/workspace_symbol.rs
+expression: "run(\"bibtex\")?"
+
+---
+[
+ {
+ "name": "foo",
+ "kind": 24,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.bib",
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 0
+ },
+ "end": {
+ "line": 0,
+ "character": 14
+ }
+ }
+ }
+ },
+ {
+ "name": "bar",
+ "kind": 15,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.bib",
+ "range": {
+ "start": {
+ "line": 2,
+ "character": 0
+ },
+ "end": {
+ "line": 2,
+ "character": 20
+ }
+ }
+ }
+ }
+]
diff --git a/support/texlab/tests/integration/snapshots/integration__workspace_symbol__filter_type_figure.snap b/support/texlab/tests/integration/snapshots/integration__workspace_symbol__filter_type_figure.snap
new file mode 100644
index 0000000000..8842d02f7d
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__workspace_symbol__filter_type_figure.snap
@@ -0,0 +1,25 @@
+---
+source: tests/integration/workspace_symbol.rs
+expression: "run(\"figure\")?"
+
+---
+[
+ {
+ "name": "Figure 1: Bar",
+ "kind": 6,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 15,
+ "character": 0
+ },
+ "end": {
+ "line": 19,
+ "character": 12
+ }
+ }
+ }
+ }
+]
diff --git a/support/texlab/tests/integration/snapshots/integration__workspace_symbol__filter_type_item.snap b/support/texlab/tests/integration/snapshots/integration__workspace_symbol__filter_type_item.snap
new file mode 100644
index 0000000000..3434eaff9c
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__workspace_symbol__filter_type_item.snap
@@ -0,0 +1,61 @@
+---
+source: tests/integration/workspace_symbol.rs
+expression: "run(\"item\")?"
+
+---
+[
+ {
+ "name": "1",
+ "kind": 22,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 24,
+ "character": 4
+ },
+ "end": {
+ "line": 24,
+ "character": 28
+ }
+ }
+ }
+ },
+ {
+ "name": "2",
+ "kind": 22,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 25,
+ "character": 4
+ },
+ "end": {
+ "line": 25,
+ "character": 28
+ }
+ }
+ }
+ },
+ {
+ "name": "3",
+ "kind": 22,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 26,
+ "character": 4
+ },
+ "end": {
+ "line": 26,
+ "character": 28
+ }
+ }
+ }
+ }
+]
diff --git a/support/texlab/tests/integration/snapshots/integration__workspace_symbol__filter_type_math.snap b/support/texlab/tests/integration/snapshots/integration__workspace_symbol__filter_type_math.snap
new file mode 100644
index 0000000000..d2b7550efe
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__workspace_symbol__filter_type_math.snap
@@ -0,0 +1,43 @@
+---
+source: tests/integration/workspace_symbol.rs
+expression: "run(\"math\")?"
+
+---
+[
+ {
+ "name": "Equation (1)",
+ "kind": 14,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 9,
+ "character": 0
+ },
+ "end": {
+ "line": 11,
+ "character": 14
+ }
+ }
+ }
+ },
+ {
+ "name": "Lemma 1 (Qux)",
+ "kind": 13,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 33,
+ "character": 0
+ },
+ "end": {
+ "line": 35,
+ "character": 11
+ }
+ }
+ }
+ }
+]
diff --git a/support/texlab/tests/integration/snapshots/integration__workspace_symbol__filter_type_section.snap b/support/texlab/tests/integration/snapshots/integration__workspace_symbol__filter_type_section.snap
new file mode 100644
index 0000000000..e221d55dcb
--- /dev/null
+++ b/support/texlab/tests/integration/snapshots/integration__workspace_symbol__filter_type_section.snap
@@ -0,0 +1,79 @@
+---
+source: tests/integration/workspace_symbol.rs
+expression: "run(\"section\")?"
+
+---
+[
+ {
+ "name": "1 Foo",
+ "kind": 2,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 7,
+ "character": 0
+ },
+ "end": {
+ "line": 11,
+ "character": 14
+ }
+ }
+ }
+ },
+ {
+ "name": "2 Bar",
+ "kind": 2,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 13,
+ "character": 0
+ },
+ "end": {
+ "line": 19,
+ "character": 12
+ }
+ }
+ }
+ },
+ {
+ "name": "3 Baz",
+ "kind": 2,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 21,
+ "character": 0
+ },
+ "end": {
+ "line": 27,
+ "character": 15
+ }
+ }
+ }
+ },
+ {
+ "name": "4 Qux",
+ "kind": 2,
+ "deprecated": false,
+ "location": {
+ "uri": "http://www.example.com/main.tex",
+ "range": {
+ "start": {
+ "line": 29,
+ "character": 0
+ },
+ "end": {
+ "line": 35,
+ "character": 11
+ }
+ }
+ }
+ }
+]
diff --git a/support/texlab/tests/integration/workspace_symbol.rs b/support/texlab/tests/integration/workspace_symbol.rs
index 5eb8e1ace7..54a0a8808d 100644
--- a/support/texlab/tests/integration/workspace_symbol.rs
+++ b/support/texlab/tests/integration/workspace_symbol.rs
@@ -1,228 +1,119 @@
-use indoc::indoc;
-use texlab::{
- protocol::{Location, Range, RangeExt, SymbolInformation, WorkspaceSymbolParams},
- test::{TestBed, TestBedBuilder, TestLspClient, PULL_CAPABILITIES},
-};
-
-pub fn verify_symbol_info(
- symbol: &SymbolInformation,
- test_bed: &TestBed,
- relative_path: &str,
- name: &str,
- start_line: u64,
- start_character: u64,
- end_line: u64,
- end_character: u64,
-) {
- assert_eq!(symbol.name, name);
- let range = Range::new_simple(start_line, start_character, end_line, end_character);
- assert_eq!(
- symbol.location,
- Location::new(test_bed.uri(relative_path).into(), range)
- );
-}
+use anyhow::Result;
+use insta::assert_json_snapshot;
+use lsp_types::{ClientCapabilities, SymbolInformation, Url};
+
+use crate::common::ServerTester;
+
+fn run(query: &str) -> Result<Vec<SymbolInformation>> {
+ let server = ServerTester::launch_new_instance()?;
+ server.initialize(ClientCapabilities::default(), None)?;
+ let uri = Url::parse("http://www.example.com/main.tex")?;
+ server.open_memory(
+ uri.clone(),
+ r#"
+ \documentclass{article}
+ \usepackage{caption}
+ \usepackage{amsmath}
+ \usepackage{amsthm}
+
+ \begin{document}
+
+ \section{Foo}\label{sec:foo}
+
+ \begin{equation}\label{eq:foo}
+ Foo
+ \end{equation}
+
+ \section{Bar}\label{sec:bar}
+
+ \begin{figure}
+ Bar
+ \caption{Bar}
+ \label{fig:bar}
+ \end{figure}
+
+ \section{Baz}\label{sec:baz}
+
+ \begin{enumerate}
+ \item\label{itm:foo} Foo
+ \item\label{itm:bar} Bar
+ \item\label{itm:baz} Baz
+ \end{enumerate}
+
+ \section{Qux}\label{sec:qux}
+
+ \newtheorem{lemma}{Lemma}
+
+ \begin{lemma}[Qux]\label{thm:qux}
+ Qux
+ \end{lemma}
+
+ \end{document}
+ "#,
+ "latex",
+ )?;
+ server.open_memory(
+ Url::parse("http://www.example.com/main.aux")?,
+ r#"
+ \relax
+ \@writefile{lof}{\contentsline {figure}{\numberline {1}{\ignorespaces Bar\relax }}{1}\protected@file@percent }
+ \providecommand*\caption@xref[2]{\@setref\relax\@undefined{#1}}
+ \newlabel{fig:bar}{{1}{1}}
+ \@writefile{toc}{\contentsline {section}{\numberline {1}Foo}{1}\protected@file@percent }
+ \newlabel{sec:foo}{{1}{1}}
+ \newlabel{eq:foo}{{1}{1}}
+ \@writefile{toc}{\contentsline {section}{\numberline {2}Bar}{1}\protected@file@percent }
+ \newlabel{sec:bar}{{2}{1}}
+ \@writefile{toc}{\contentsline {section}{\numberline {3}Baz}{1}\protected@file@percent }
+ \newlabel{sec:baz}{{3}{1}}
+ \newlabel{itm:foo}{{1}{1}}
+ \newlabel{itm:bar}{{2}{1}}
+ \newlabel{itm:baz}{{3}{1}}
+ \@writefile{toc}{\contentsline {section}{\numberline {4}Qux}{1}\protected@file@percent }
+ \newlabel{sec:qux}{{4}{1}}
+ \newlabel{thm:qux}{{1}{1}}
+ "#,
+ "latex",
+ )?;
+ server.open_memory(
+ Url::parse("http://www.example.com/main.bib")?,
+ r#"
+ @article{foo,}
+
+ @string{bar = "bar"}
+ "#,
+ "bibtex",
+ )?;
-async fn run(query: &str) -> (TestBed, Vec<SymbolInformation>) {
- let mut test_bed = TestBedBuilder::new()
- .file(
- "foo.tex",
- indoc!(
- r#"
- \documentclass{article}
- \usepackage{caption}
- \usepackage{amsmath}
- \usepackage{amsthm}
- %
- \begin{document}
- %
- \section{Foo}\label{sec:foo}
- %
- \begin{equation}\label{eq:foo}
- Foo
- \end{equation}
- %
- \section{Bar}\label{sec:bar}
- %
- \begin{figure}
- Bar
- \caption{Bar}
- \label{fig:bar}
- \end{figure}
- %
- \section{Baz}\label{sec:baz}
- %
- \begin{enumerate}
- \item\label{itm:foo} Foo
- \item\label{itm:bar} Bar
- \item\label{itm:baz} Baz
- \end{enumerate}
- %
- \section{Qux}\label{sec:qux}
- %
- \newtheorem{lemma}{Lemma}
- %
- \begin{lemma}[Qux]\label{thm:qux}
- Qux
- \end{lemma}
- %
- \end{document}
- "#
- ),
- )
- .file(
- "foo.aux",
- indoc!(
- r#"
- \relax
- \@writefile{lof}{\contentsline {figure}{\numberline {1}{\ignorespaces Bar\relax }}{1}\protected@file@percent }
- \providecommand*\caption@xref[2]{\@setref\relax\@undefined{#1}}
- \newlabel{fig:bar}{{1}{1}}
- \@writefile{toc}{\contentsline {section}{\numberline {1}Foo}{1}\protected@file@percent }
- \newlabel{sec:foo}{{1}{1}}
- \newlabel{eq:foo}{{1}{1}}
- \@writefile{toc}{\contentsline {section}{\numberline {2}Bar}{1}\protected@file@percent }
- \newlabel{sec:bar}{{2}{1}}
- \@writefile{toc}{\contentsline {section}{\numberline {3}Baz}{1}\protected@file@percent }
- \newlabel{sec:baz}{{3}{1}}
- \newlabel{itm:foo}{{1}{1}}
- \newlabel{itm:bar}{{2}{1}}
- \newlabel{itm:baz}{{3}{1}}
- \@writefile{toc}{\contentsline {section}{\numberline {4}Qux}{1}\protected@file@percent }
- \newlabel{sec:qux}{{4}{1}}
- \newlabel{thm:qux}{{1}{1}}
- "#
- ),
- )
- .file(
- "bar.bib",
- indoc!(
- r#"
- @article{foo,}
- %
- @string{bar = "bar"}
- "#
- ),
- )
- .build()
- .await;
- test_bed.spawn();
- test_bed.initialize(PULL_CAPABILITIES.clone()).await;
- test_bed.open("foo.tex").await;
- test_bed.open("foo.aux").await;
- test_bed.open("bar.bib").await;
-
- let params = WorkspaceSymbolParams {
- query: query.into(),
- ..WorkspaceSymbolParams::default()
- };
- let actual_symbols = test_bed.client.workspace_symbol(params).await.unwrap();
-
- test_bed.shutdown().await;
-
- (test_bed, actual_symbols)
+ server.find_workspace_symbols(query)
}
-#[tokio::test]
-async fn filter_type_section() {
- let (test_bed, actual_symbols) = run("section").await;
- assert_eq!(actual_symbols.len(), 4);
- verify_symbol_info(
- &actual_symbols[0],
- &test_bed,
- "foo.tex",
- "1 Foo",
- 7,
- 0,
- 13,
- 0,
- );
- verify_symbol_info(
- &actual_symbols[1],
- &test_bed,
- "foo.tex",
- "2 Bar",
- 13,
- 0,
- 21,
- 0,
- );
- verify_symbol_info(
- &actual_symbols[2],
- &test_bed,
- "foo.tex",
- "3 Baz",
- 21,
- 0,
- 29,
- 0,
- );
- verify_symbol_info(
- &actual_symbols[3],
- &test_bed,
- "foo.tex",
- "4 Qux",
- 29,
- 0,
- 37,
- 0,
- );
+#[test]
+fn test_filter_type_section() -> Result<()> {
+ assert_json_snapshot!(run("section")?);
+ Ok(())
}
-#[tokio::test]
-async fn filter_type_figure() {
- let (test_bed, actual_symbols) = run("figure").await;
- assert_eq!(actual_symbols.len(), 1);
- verify_symbol_info(
- &actual_symbols[0],
- &test_bed,
- "foo.tex",
- "Figure 1: Bar",
- 15,
- 0,
- 19,
- 12,
- );
+#[test]
+fn test_filter_type_figure() -> Result<()> {
+ assert_json_snapshot!(run("figure")?);
+ Ok(())
}
-#[tokio::test]
-async fn filter_type_item() {
- let (test_bed, actual_symbols) = run("item").await;
- assert_eq!(actual_symbols.len(), 3);
- verify_symbol_info(&actual_symbols[0], &test_bed, "foo.tex", "1", 24, 4, 25, 4);
- verify_symbol_info(&actual_symbols[1], &test_bed, "foo.tex", "2", 25, 4, 26, 4);
- verify_symbol_info(&actual_symbols[2], &test_bed, "foo.tex", "3", 26, 4, 27, 0);
+#[test]
+fn test_filter_type_item() -> Result<()> {
+ assert_json_snapshot!(run("item")?);
+ Ok(())
}
-#[tokio::test]
-async fn filter_type_math() {
- let (test_bed, actual_symbols) = run("math").await;
- assert_eq!(actual_symbols.len(), 2);
- verify_symbol_info(
- &actual_symbols[0],
- &test_bed,
- "foo.tex",
- "Equation (1)",
- 9,
- 0,
- 11,
- 14,
- );
- verify_symbol_info(
- &actual_symbols[1],
- &test_bed,
- "foo.tex",
- "Lemma 1 (Qux)",
- 33,
- 0,
- 35,
- 11,
- );
+#[test]
+fn test_filter_type_math() -> Result<()> {
+ assert_json_snapshot!(run("math")?);
+ Ok(())
}
-#[tokio::test]
-async fn filter_bibtex() {
- let (test_bed, actual_symbols) = run("bibtex").await;
- assert_eq!(actual_symbols.len(), 2);
- verify_symbol_info(&actual_symbols[0], &test_bed, "bar.bib", "foo", 0, 0, 0, 14);
- verify_symbol_info(&actual_symbols[1], &test_bed, "bar.bib", "bar", 2, 0, 2, 20);
+#[test]
+fn test_filter_bibtex() -> Result<()> {
+ assert_json_snapshot!(run("bibtex")?);
+ Ok(())
}