summaryrefslogtreecommitdiff
path: root/support/texlab/src
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/texlab/src
parent58aa1ac09b1d9e4769d0a0661cf12e2b2db41b14 (diff)
CTAN sync 202105230300
Diffstat (limited to 'support/texlab/src')
-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
348 files changed, 19694 insertions, 19326 deletions
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)
+ }
+}