315 lines
12 KiB
CMake
315 lines
12 KiB
CMake
# cmark-gfm: markdown -> block structure (pkg-config; no CMake config
|
|
# package is installed).
|
|
pkg_check_modules(CMARK_GFM REQUIRED IMPORTED_TARGET libcmark-gfm)
|
|
|
|
# tree-sitter runtime for code block highlighting (grammars are
|
|
# dlopen()'d at runtime and optional).
|
|
pkg_check_modules(TREE_SITTER REQUIRED IMPORTED_TARGET tree-sitter)
|
|
|
|
# JKQTMathText (JKQtPlotter) for LaTeX rendering. The config files live
|
|
# in a shared JKQTPlotter6 directory, not one named after the package.
|
|
find_path(JKQTPlotter6_CMAKE_DIR
|
|
NAMES JKQTMathText6Config.cmake
|
|
HINTS /usr/lib/cmake/JKQTPlotter6 /usr/local/lib/cmake/JKQTPlotter6
|
|
DOC "Directory containing the JKQtPlotter cmake package files")
|
|
find_package(JKQTMathText6 REQUIRED PATHS "${JKQTPlotter6_CMAKE_DIR}")
|
|
|
|
# --- tree-sitter grammar discovery (configure time) ---
|
|
#
|
|
# Discover installed tree-sitter grammars — system packages
|
|
# (libtree-sitter-<lang>.so) and the parsers Neovim's nvim-treesitter
|
|
# installs (~/.local/share/nvim/site/parser/*.so) — and pair each with
|
|
# highlight queries. Query sources, in priority order:
|
|
# 1. vendored highlight-queries/*.scm (version-pinned; see the
|
|
# per-file source headers, MIT),
|
|
# 2. Neovim's own queries (version-matched to its parsers),
|
|
# 3. tree-sitter/highlighting from GitHub (cached in the build dir).
|
|
# The result is embedded as highlight-queries.hpp. Re-run cmake to pick
|
|
# up grammars installed later.
|
|
#
|
|
# Candidate entry points are read from the .so with `nm` rather than
|
|
# assumed to be tree_sitter_<id>; a missing entry point just makes that
|
|
# candidate fail at runtime.
|
|
function(_ts_entry_point out file)
|
|
# OUTPUT_VARIABLE + OUTPUT_QUIET loses the output on CMake 4, so
|
|
# capture through a temp file.
|
|
get_filename_component(_ts_nm_base "${file}" NAME)
|
|
set(_ts_nm_file "${CMAKE_CURRENT_BINARY_DIR}/ts-entry-${_ts_nm_base}")
|
|
execute_process(
|
|
COMMAND nm -D --defined-only "${file}"
|
|
RESULT_VARIABLE _ts_nm_rc
|
|
OUTPUT_FILE "${_ts_nm_file}"
|
|
ERROR_FILE "${_ts_nm_file}.err")
|
|
set(_ts_sym "tree_sitter_missing")
|
|
if(_ts_nm_rc EQUAL 0 AND EXISTS "${_ts_nm_file}")
|
|
file(READ "${_ts_nm_file}" _ts_nm_out)
|
|
file(REMOVE "${_ts_nm_file}" "${_ts_nm_file}.err")
|
|
# The library also exports the external scanner functions; the
|
|
# entry point is the one without the _external suffix.
|
|
string(REGEX MATCHALL " T tree_sitter_[A-Za-z0-9_]+" _ts_syms "${_ts_nm_out}")
|
|
foreach(_ts_s IN LISTS _ts_syms)
|
|
string(REPLACE " T " "" _ts_s "${_ts_s}")
|
|
if(NOT _ts_s MATCHES "_external")
|
|
set(_ts_sym "${_ts_s}")
|
|
break()
|
|
endif()
|
|
endforeach()
|
|
endif()
|
|
set(${out} "${_ts_sym}" PARENT_SCOPE)
|
|
endfunction()
|
|
|
|
# Append a query text to <files> (PARENT_SCOPE) unless it duplicates a
|
|
# hash in <hashes>. The text is written to the build tree right away:
|
|
# the s-expression `;` comments would split CMake list items and the
|
|
# query `(` `)` break bracket arguments, so query text only ever lives
|
|
# in variables and files, never in lists.
|
|
# Expand Neovim's "; inherits:" directives by appending the inherited
|
|
# query set's highlights file (recursively; a visited set prevents
|
|
# cycles). Several languages are stubs that inherit the real query
|
|
# (html inherits html_tags, qmljs inherits ecma, ...).
|
|
function(_ts_expand_inherits query_dir text outVar)
|
|
set(result "${text}")
|
|
set(_visited "")
|
|
set(_depth 0)
|
|
while(_depth LESS 8)
|
|
# Do not match the leading `;`: a MATCHALL result that itself
|
|
# contains a semicolon is re-split into list items.
|
|
string(REGEX MATCHALL "inherits:[ \t]*[A-Za-z0-9_]+" _inh "${result}")
|
|
if(NOT _inh)
|
|
break()
|
|
endif()
|
|
set(_added FALSE)
|
|
foreach(_entry IN LISTS _inh)
|
|
string(REGEX REPLACE "^inherits:[ \t]*" "" _name "${_entry}")
|
|
set(_file "${query_dir}/${_name}/highlights.scm")
|
|
if(NOT EXISTS "${_file}" OR _file IN_LIST _visited)
|
|
continue()
|
|
endif()
|
|
list(APPEND _visited "${_file}")
|
|
file(READ "${_file}" _inh_text)
|
|
string(APPEND result "\n${_inh_text}")
|
|
set(_added TRUE)
|
|
endforeach()
|
|
if(NOT _added)
|
|
break()
|
|
endif()
|
|
math(EXPR _depth "${_depth} + 1")
|
|
endwhile()
|
|
set(${outVar} "${result}" PARENT_SCOPE)
|
|
endfunction()
|
|
|
|
function(_ts_add_query sid text filesVar hashesVar)
|
|
# filesVar/hashesVar hold the caller's variable names.
|
|
set(files "${${filesVar}}")
|
|
set(hashes "${${hashesVar}}")
|
|
string(SHA256 _ts_qh "${text}")
|
|
if(_ts_qh IN_LIST hashes)
|
|
return()
|
|
endif()
|
|
list(APPEND hashes "${_ts_qh}")
|
|
list(LENGTH files _ts_qi)
|
|
set(_ts_qfile "${CMAKE_CURRENT_BINARY_DIR}/ts-queries/${sid}_${_ts_qi}.scm")
|
|
file(WRITE "${_ts_qfile}" "${text}")
|
|
list(APPEND files "${_ts_qfile}")
|
|
set(${filesVar} "${files}" PARENT_SCOPE)
|
|
set(${hashesVar} "${hashes}" PARENT_SCOPE)
|
|
endfunction()
|
|
|
|
set(_ts_candidates "") # entries: <id>|<cmake-safe id>|<lib>|<symbol>
|
|
file(GLOB _ts_sys_files
|
|
"/usr/lib/libtree-sitter-*.so" "/usr/local/lib/libtree-sitter-*.so")
|
|
foreach(_ts_file IN LISTS _ts_sys_files)
|
|
get_filename_component(_ts_name "${_ts_file}" NAME)
|
|
string(REGEX REPLACE "^libtree-sitter-(.+)\.so$" "\\1" _ts_id "${_ts_name}")
|
|
string(REPLACE "-" "_" _ts_sid "${_ts_id}")
|
|
_ts_entry_point(_ts_sym "${_ts_file}")
|
|
list(APPEND _ts_candidates
|
|
"${_ts_id}|${_ts_sid}|libtree-sitter-${_ts_id}.so|${_ts_sym}")
|
|
endforeach()
|
|
set(_ts_nvim_parser_dirs
|
|
"$ENV{HOME}/.local/share/nvim/site/parser"
|
|
"$ENV{HOME}/.local/share/nvim/runtime/parser"
|
|
"$ENV{HOME}/.local/share/nvim/lazy/nvim-treesitter/parser")
|
|
set(_ts_nvim_query_dirs
|
|
"$ENV{HOME}/.local/share/nvim/site/queries"
|
|
"$ENV{HOME}/.local/share/nvim/lazy/nvim-treesitter/runtime/queries")
|
|
foreach(_ts_dir IN LISTS _ts_nvim_parser_dirs)
|
|
file(GLOB _ts_dir_files "${_ts_dir}/*.so")
|
|
foreach(_ts_file IN LISTS _ts_dir_files)
|
|
get_filename_component(_ts_name "${_ts_file}" NAME)
|
|
string(REGEX REPLACE "\\.so$" "" _ts_id "${_ts_name}")
|
|
string(REPLACE "-" "_" _ts_sid "${_ts_id}")
|
|
_ts_entry_point(_ts_sym "${_ts_file}")
|
|
list(APPEND _ts_candidates
|
|
"${_ts_id}|${_ts_sid}|${_ts_file}|${_ts_sym}")
|
|
endforeach()
|
|
endforeach()
|
|
|
|
set(_ts_ids "")
|
|
foreach(_ts_c IN LISTS _ts_candidates)
|
|
string(REPLACE "|" ";" _ts_parts "${_ts_c}")
|
|
list(GET _ts_parts 0 _ts_id)
|
|
if(NOT _ts_id IN_LIST _ts_ids)
|
|
list(APPEND _ts_ids "${_ts_id}")
|
|
endif()
|
|
endforeach()
|
|
list(SORT _ts_ids)
|
|
|
|
set(HIGHLIGHT_QUERIES_HPP "${CMAKE_CURRENT_BINARY_DIR}/highlight-queries.hpp")
|
|
set(_hl_header
|
|
"#pragma once\n\n// Generated by CMake. Discoverd tree-sitter grammars and their\n// highlight queries: vendored highlight-queries/*.scm (MIT), Neovim\n// nvim-treesitter queries, and tree-sitter/highlighting (MIT).\nnamespace ZShell::llm::hq {\nstruct Candidate { const char* lib; const char* symbol; };\nstruct Grammar { const char* id; int nCandidates; const Candidate* candidates; int nQueries; const char* const* queries; };\n")
|
|
set(_ts_grammar_rows "")
|
|
set(_ts_vendored
|
|
c cpp python javascript typescript tsx bash json rust go yaml toml sql)
|
|
foreach(_ts_id IN LISTS _ts_ids)
|
|
string(REPLACE "-" "_" _ts_sid "${_ts_id}")
|
|
|
|
# Query candidates in priority order (deduped; identical texts are
|
|
# skipped, the nvim site and plugin copies are usually the same
|
|
# file).
|
|
set(_ts_qfiles "")
|
|
set(_ts_qhashes "")
|
|
if(_ts_id STREQUAL "cpp")
|
|
# The C++ grammar is a superset of C and its query only covers
|
|
# the C++ delta; base C coverage comes from the C query.
|
|
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/highlight-queries/c.scm" _ts_qa)
|
|
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/highlight-queries/cpp.scm" _ts_qb)
|
|
_ts_add_query(${_ts_sid} "${_ts_qa}\n${_ts_qb}" _ts_qfiles _ts_qhashes)
|
|
elseif(_ts_id STREQUAL "typescript" OR _ts_id STREQUAL "tsx")
|
|
# The TS grammars reuse the JS node names; the JS query usually
|
|
# compiles against them and gives full coverage.
|
|
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/highlight-queries/javascript.scm" _ts_qa)
|
|
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/highlight-queries/typescript.scm" _ts_qb)
|
|
_ts_add_query(${_ts_sid} "${_ts_qa}\n${_ts_qb}" _ts_qfiles _ts_qhashes)
|
|
_ts_add_query(${_ts_sid} "${_ts_qb}" _ts_qfiles _ts_qhashes)
|
|
elseif(_ts_id IN_LIST _ts_vendored)
|
|
file(READ
|
|
"${CMAKE_CURRENT_SOURCE_DIR}/highlight-queries/${_ts_id}.scm" _ts_q)
|
|
_ts_add_query(${_ts_sid} "${_ts_q}" _ts_qfiles _ts_qhashes)
|
|
endif()
|
|
foreach(_ts_qd IN LISTS _ts_nvim_query_dirs)
|
|
if(EXISTS "${_ts_qd}/${_ts_id}/highlights.scm")
|
|
file(READ "${_ts_qd}/${_ts_id}/highlights.scm" _ts_q)
|
|
_ts_expand_inherits("${_ts_qd}" "${_ts_q}" _ts_q)
|
|
_ts_add_query(${_ts_sid} "${_ts_q}" _ts_qfiles _ts_qhashes)
|
|
endif()
|
|
endforeach()
|
|
if(NOT _ts_qfiles)
|
|
# Last resort: fetch from the tree-sitter/highlighting repo.
|
|
# Version-skewed against locally installed grammars, so only
|
|
# used when nothing local exists. Cached; offline builds simply
|
|
# drop the language.
|
|
set(_ts_dl "${CMAKE_CURRENT_BINARY_DIR}/ts-query-downloads/${_ts_sid}.scm")
|
|
if(NOT EXISTS "${_ts_dl}")
|
|
file(DOWNLOAD
|
|
"https://raw.githubusercontent.com/tree-sitter/highlighting/main/queries/${_ts_id}/highlight.scm"
|
|
"${_ts_dl}" STATUS _ts_dl_status TIMEOUT 30)
|
|
list(GET _ts_dl_status 0 _ts_dl_rc)
|
|
if(NOT _ts_dl_rc EQUAL 0)
|
|
file(REMOVE "${_ts_dl}")
|
|
endif()
|
|
endif()
|
|
if(EXISTS "${_ts_dl}")
|
|
file(READ "${_ts_dl}" _ts_q)
|
|
_ts_add_query(${_ts_sid} "${_ts_q}" _ts_qfiles _ts_qhashes)
|
|
endif()
|
|
endif()
|
|
list(LENGTH _ts_qfiles _ts_nq)
|
|
if(_ts_nq EQUAL 0)
|
|
continue()
|
|
endif()
|
|
|
|
# Emit query sources.
|
|
set(_ts_qn 0)
|
|
set(_ts_q_ptrs "")
|
|
foreach(_ts_qfile IN LISTS _ts_qfiles)
|
|
file(READ "${_ts_qfile}" _ts_q)
|
|
string(APPEND _hl_header
|
|
"inline constexpr const char* q_${_ts_sid}_${_ts_qn} = R\"ZSQUERY(${_ts_q})ZSQUERY\";\n")
|
|
string(APPEND _ts_q_ptrs "q_${_ts_sid}_${_ts_qn}, ")
|
|
math(EXPR _ts_qn "${_ts_qn} + 1")
|
|
endforeach()
|
|
string(APPEND _hl_header
|
|
"inline constexpr const char* const q_${_ts_sid}[] = { ${_ts_q_ptrs} };\n")
|
|
|
|
# Emit library candidates (system package first, then Neovim).
|
|
string(APPEND _hl_header "inline constexpr Candidate cand_${_ts_sid}[] = {\n")
|
|
set(_ts_nc 0)
|
|
foreach(_ts_c IN LISTS _ts_candidates)
|
|
string(REPLACE "|" ";" _ts_parts "${_ts_c}")
|
|
list(GET _ts_parts 0 _ts_cid)
|
|
if(_ts_cid STREQUAL _ts_id)
|
|
list(GET _ts_parts 2 _ts_lib)
|
|
list(GET _ts_parts 3 _ts_sym)
|
|
string(APPEND _hl_header
|
|
" { R\"ZSLIB(${_ts_lib})ZSLIB\", R\"ZSSYM(${_ts_sym})ZSSYM\" },\n")
|
|
math(EXPR _ts_nc "${_ts_nc} + 1")
|
|
endif()
|
|
endforeach()
|
|
string(APPEND _hl_header "};\n")
|
|
if(_ts_nc EQUAL 0)
|
|
# Queries but no library: pointless, drop the language.
|
|
string(APPEND _hl_header "") # (arrays stay; grammar row is skipped)
|
|
continue()
|
|
endif()
|
|
|
|
list(APPEND _ts_grammar_rows
|
|
"{ \"${_ts_id}\", ${_ts_nc}, cand_${_ts_sid}, ${_ts_nq}, q_${_ts_sid} },")
|
|
endforeach()
|
|
string(APPEND _hl_header "inline constexpr Grammar grammars[] = {\n")
|
|
foreach(_ts_row IN LISTS _ts_grammar_rows)
|
|
string(APPEND _hl_header " ${_ts_row}\n")
|
|
endforeach()
|
|
string(APPEND _hl_header "};\n}\n")
|
|
file(WRITE "${HIGHLIGHT_QUERIES_HPP}" "${_hl_header}")
|
|
|
|
# Embed the vendored Latin Modern fonts (GUST Font License; provenance
|
|
# in fonts/latinmodern/GUST-FONT-LICENSE.txt) as byte arrays.
|
|
set(LM_FONTS
|
|
lmroman10-regular
|
|
lmroman10-italic
|
|
lmroman10-bold
|
|
lmroman10-bolditalic
|
|
latinmodern-math)
|
|
set(LM_FONTS_HPP "${CMAKE_CURRENT_BINARY_DIR}/latinmodern-fonts.hpp")
|
|
set(_lm_header "#pragma once\n\n// Vendored Latin Modern fonts (GUST Font License; see\n// fonts/latinmodern/GUST-FONT-LICENSE.txt).\nnamespace ZShell::llm::lmfont {\n")
|
|
foreach(_lm_font IN LISTS LM_FONTS)
|
|
string(REPLACE "-" "_" _lm_sym "${_lm_font}")
|
|
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/fonts/latinmodern/${_lm_font}.otf" _lm_hex HEX)
|
|
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," _lm_bytes "${_lm_hex}")
|
|
string(APPEND _lm_header "inline const unsigned char ${_lm_sym}[] = { ${_lm_bytes} };\n")
|
|
endforeach()
|
|
string(APPEND _lm_header "}\n")
|
|
file(WRITE "${LM_FONTS_HPP}" "${_lm_header}")
|
|
|
|
qml_module(ZShell-llm
|
|
URI ZShell.Llm
|
|
SOURCES
|
|
chat.hpp chat.cpp
|
|
chatstore.hpp chatstore.cpp
|
|
codehighlighter.hpp codehighlighter.cpp
|
|
generation.hpp generation.cpp
|
|
llmclient.hpp llmclient.cpp
|
|
markdownblock.hpp
|
|
markdownparser.hpp markdownparser.cpp
|
|
mathtext.hpp mathtext.cpp
|
|
message.hpp message.cpp
|
|
messagemodel.hpp messagemodel.cpp
|
|
segment.hpp segment.cpp
|
|
session.hpp session.cpp
|
|
tool.hpp tool.cpp
|
|
webfetchtool.hpp webfetchtool.cpp
|
|
LIBRARIES
|
|
Qt::Network
|
|
Qt::Sql
|
|
Qt::Gui
|
|
Qt::Widgets
|
|
ZShell-config
|
|
JKQTPlotter::JKQTMathText
|
|
PkgConfig::CMARK_GFM
|
|
PkgConfig::TREE_SITTER
|
|
)
|
|
|
|
target_include_directories(ZShell-llm PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
|
target_include_directories(ZShell-llm PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../Config)
|