diff --git a/.gitignore b/.gitignore --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ cypress/junit/ cypress/downloads/ .eslintcache +tree-sitter-swh_search_ql.wasm diff --git a/Makefile.local b/Makefile.local --- a/Makefile.local +++ b/Makefile.local @@ -5,6 +5,7 @@ SETTINGS_TEST ?= swh.web.settings.tests SETTINGS_DEV ?= swh.web.settings.development SETTINGS_PROD = swh.web.settings.production +SWH_SEARCH_DIR := $(shell python -c "import os;from swh import search; print(os.path.dirname(search.__file__))") yarn-install: package.json $(YARN) install --frozen-lockfile @@ -54,17 +55,17 @@ clear-memcached: echo "flush_all" | nc -q 2 localhost 11211 2>/dev/null -run-django-webpack-devserver: add-users-dev yarn-install +run-django-webpack-devserver: add-users-dev yarn-install build-ts-wasm bash -c "trap 'trap - SIGINT SIGTERM ERR EXIT && \ # ensure all child processes will be killed by PGID when exiting \ ps -o pgid= $$$$ | grep -o [0-9]* | xargs pkill -g' SIGINT SIGTERM ERR EXIT; \ $(YARN) start-dev & sleep 10 && cd swh/web && \ python3 manage.py runserver --nostatic --settings=$(SETTINGS_DEV) || exit 1" -run-django-webpack-dev: build-webpack-dev add-users-dev +run-django-webpack-dev: build-ts-wasm build-webpack-dev add-users-dev python3 swh/web/manage.py runserver --nostatic --settings=$(SETTINGS_DEV) -run-django-webpack-prod: build-webpack-prod add-users-prod clear-memcached +run-django-webpack-prod: build-ts-wasm build-webpack-prod add-users-prod clear-memcached python3 swh/web/manage.py runserver --nostatic --settings=$(SETTINGS_PROD) run-django-server-dev: add-users-dev @@ -79,7 +80,7 @@ --threads 2 \ --workers 2 'django.core.wsgi:get_wsgi_application()' -run-django-webpack-memory-storages: build-webpack-dev add-users-test +run-django-webpack-memory-storages: build-ts-wasm build-webpack-dev add-users-test python3 swh/web/manage.py runserver --nostatic --settings=$(SETTINGS_TEST) test-full: @@ -123,3 +124,7 @@ # https://github.com/typeddjango/django-stubs/issues/166 check-mypy: DJANGO_SETTINGS_MODULE=$(SETTINGS_DEV) $(MYPY) $(MYPYFLAGS) swh + +build-ts-wasm: + yarn run tree-sitter build-wasm $(SWH_SEARCH_DIR)/query_language + cp $(SWH_SEARCH_DIR)/query_language/tokens.js assets/tokens.js diff --git a/assets/config/webpack.config.development.js b/assets/config/webpack.config.development.js --- a/assets/config/webpack.config.development.js +++ b/assets/config/webpack.config.development.js @@ -388,6 +388,14 @@ { from: path.resolve(nodeModules, 'mathjax/es5/output/chtml/fonts/woff-v2/**'), to: path.resolve(__dirname, '../../static/fonts/[name][ext]') + }, + { + from: path.resolve(__dirname, '../../tree-sitter-swh_search_ql.wasm'), + to: path.resolve(__dirname, '../../static/js/swh_ql.wasm') + }, + { + from: path.resolve(__dirname, '../tokens.js'), + to: path.resolve(__dirname, '../../static/js/') } ] }), diff --git a/assets/src/utils/autocomplete.js b/assets/src/utils/autocomplete.js --- a/assets/src/utils/autocomplete.js +++ b/assets/src/utils/autocomplete.js @@ -20,18 +20,21 @@ this.inputBox.addEventListener('keydown', (e) => { if (e.keyCode === 40) { // down + e.preventDefault(); this.currentIndex++; this.addActive(); } else if (e.keyCode === 38) { // up + e.preventDefault(); this.currentIndex--; this.addActive(); - } else if (e.keyCode === 13) { // enter + } else if (e.keyCode === 13 || e.keyCode === 9) { // enter or tab e.preventDefault(); if (this.currentIndex > -1) { // Simulate a click on the "active" item: if (this.autocompleteList) this.autocompleteList.children[this.currentIndex].click(); } } else if (e.keyCode === 27) { // escape + e.preventDefault(); this.removeAllItems(e.target); } }); @@ -71,9 +74,14 @@ } itemDiv.setAttribute('data-value', suggestion); + itemDiv.setAttribute('data-editable-suggestion', 'false'); + itemDiv.setAttribute('title', 'Include repos with the provided term in their url (origin)'); const suggestionClick = (e) => { const toInsert = e.target.getAttribute('data-value'); + const isEditableSuggestion = e.target.getAttribute('data-editable-suggestion'); + + if (isEditableSuggestion === 'true') return; const oldValue = this.inputBox.value; const tokens = oldValue.split(); diff --git a/assets/src/utils/search-ql-autocomplete.js b/assets/src/utils/search-ql-autocomplete.js --- a/assets/src/utils/search-ql-autocomplete.js +++ b/assets/src/utils/search-ql-autocomplete.js @@ -1,12 +1,13 @@ -import '../../../node_modules/web-tree-sitter/tree-sitter.wasm'; +import {staticAsset} from 'utils/functions'; +import 'web-tree-sitter/tree-sitter.wasm'; import Parser from 'web-tree-sitter'; import {Autocomplete} from 'utils/autocomplete.js'; import { fields, limitField, sortByField, // fields sortByOptions, visitTypeOptions, // options - equalOp, rangeOp, choiceOp, // operators + equalOp, containOp, rangeOp, choiceOp, // operators AND, OR, TRUE, FALSE // special tokens -} from './tokens.js'; +} from '../../tokens.js'; const filterNames = fields.concat(sortByField, limitField); @@ -14,7 +15,7 @@ { category: 'patternFilter', field: 'patternField', - operator: 'equalOp', + operator: 'containOp', value: 'patternVal', suggestion: ['string', '"string"'] }, @@ -72,7 +73,7 @@ } ]; -const filterOperators = {equalOp, choiceOp, rangeOp}; +const filterOperators = {equalOp, containOp, choiceOp, rangeOp}; const findMissingNode = (node) => { if (node.isMissing()) { @@ -191,8 +192,7 @@ export const initAutocomplete = (inputBox, validQueryCallback) => { Parser.init().then(async() => { const parser = new Parser(); - - const swhSearchQL = await Parser.Language.load(`${window.location.origin}/static/swh_ql.wasm`); + const swhSearchQL = await Parser.Language.load(staticAsset('js/swh_ql.wasm')); parser.setLanguage(swhSearchQL); const autocomplete = new Autocomplete( diff --git a/assets/tokens.js b/assets/tokens.js new file mode 100644 --- /dev/null +++ b/assets/tokens.js @@ -0,0 +1,109 @@ +// Copyright (C) 2021 The Software Heritage developers +// See the AUTHORS file at the top-level directory of this distribution +// License: GNU General Public License version 3, or any later version +// See top-level LICENSE file for more information + +// Field tokens +const visitTypeField = 'visit_type'; +const sortByField = 'sort_by'; +const limitField = 'limit'; + +// Field categories +const patternFields = ['origin', 'metadata']; +const booleanFields = ['visited']; +const numericFields = ['visits']; +const boundedListFields = [visitTypeField]; +const listFields = ['language', 'license', 'keyword']; +const dateFields = [ + 'last_visit', + 'last_eventful_visit', + 'last_revision', + 'last_release', + 'created', + 'modified', + 'published' +]; + +const fields = [].concat( + patternFields, + booleanFields, + numericFields, + boundedListFields, + listFields, + dateFields +); + +// Operators +const equalOp = ['=']; +const containOp = [':']; +const rangeOp = ['<', '<=', '=', '!=', '>=', '>']; +const choiceOp = ['in', 'not in']; + +// Values +const sortByOptions = [ + 'visits', + 'last_visit', + 'last_eventful_visit', + 'last_revision', + 'last_release', + 'created', + 'modified', + 'published' +]; + +const visitTypeOptions = [ + 'any', + 'bzr', + 'cran', + 'cvs', + 'deb', + 'deposit', + 'ftp', + 'hg', + 'git', + 'nixguix', + 'npm', + 'opam', + 'pypi', + 'svn', + 'tar' +]; + +// Extra tokens +const OR = 'or'; +const AND = 'and'; + +const TRUE = 'true'; +const FALSE = 'false'; + +module.exports = { + // Field tokens + visitTypeField, + sortByField, + limitField, + + // Field categories + patternFields, + booleanFields, + numericFields, + boundedListFields, + listFields, + dateFields, + fields, + + // Operators + equalOp, + containOp, + rangeOp, + choiceOp, + + // Values + sortByOptions, + visitTypeOptions, + + // Extra tokens + OR, + AND, + TRUE, + FALSE +}; diff --git a/package.json b/package.json --- a/package.json +++ b/package.json @@ -151,6 +151,7 @@ "stylelint": "^14.6.0", "stylelint-config-standard": "^25.0.0", "terser-webpack-plugin": "^5.3.1", + "tree-sitter-cli": "^0.20.6", "url-loader": "^4.1.1", "webpack": "^5.70.0", "webpack-bundle-tracker": "^1.4.0", diff --git a/swh/web/settings/common.py b/swh/web/settings/common.py --- a/swh/web/settings/common.py +++ b/swh/web/settings/common.py @@ -12,7 +12,6 @@ import sys from typing import Any, Dict -from swh import search from swh.web.auth.utils import OIDC_SWH_WEB_CLIENT_ID from swh.web.config import get_config @@ -139,14 +138,7 @@ # static folder location when developping swh-web STATIC_DIR = os.path.join(PROJECT_DIR, "../../../static") -SEARCH_DIR = os.path.dirname(search.__file__) -# static folder location when swh-search has been installed with pip -SEARCH_STATIC_DIR = os.path.join(SEARCH_DIR, "static") -if not os.path.exists(SEARCH_STATIC_DIR): - # static folder location when developping swh-search - SEARCH_STATIC_DIR = os.path.join(SEARCH_DIR, "../../static") - -STATICFILES_DIRS = [STATIC_DIR, SEARCH_STATIC_DIR] +STATICFILES_DIRS = [STATIC_DIR] INTERNAL_IPS = ["127.0.0.1"] diff --git a/swh/web/templates/includes/origin-search-form.html b/swh/web/templates/includes/origin-search-form.html --- a/swh/web/templates/includes/origin-search-form.html +++ b/swh/web/templates/includes/origin-search-form.html @@ -48,7 +48,6 @@ search in metadata (instead of URL) - {% if user.is_authenticated and user.is_staff or "swh.web.search_ql" in user.get_all_permissions %}
@@ -60,7 +59,6 @@
- {% endif %}