From bc86ed4474173a8996d7ce1e4f47ec5c2f45dd58 Mon Sep 17 00:00:00 2001 From: Matthias Thieroff Date: Tue, 21 Dec 2021 15:10:08 +0100 Subject: [PATCH] Display search result fixes (#1901) Fix syntax highlighting on non highlighted fields. Fix ellipsis on new lines in code syntax highlighting. Fix ellipsis on content start or end in non code fields. Co-authored-by: Sebastian Sdorra --- gradle/changelog/search_fixes.yaml | 6 + .../src/__resources__/ContentSearchHit.ts | 73 ----- .../src/__resources__/SearchHit.ts | 97 +++++++ .../src/__snapshots__/storyshots.test.ts.snap | 272 ++++++++++++++++++ .../src/search/TextHitField.stories.tsx | 17 +- .../ui-components/src/search/TextHitField.tsx | 37 ++- .../sonia/scm/search/LuceneHighlighter.java | 7 +- .../scm/search/LuceneHighlighterTest.java | 28 ++ 8 files changed, 455 insertions(+), 82 deletions(-) create mode 100644 gradle/changelog/search_fixes.yaml delete mode 100644 scm-ui/ui-components/src/__resources__/ContentSearchHit.ts create mode 100644 scm-ui/ui-components/src/__resources__/SearchHit.ts diff --git a/gradle/changelog/search_fixes.yaml b/gradle/changelog/search_fixes.yaml new file mode 100644 index 0000000000..35d6197ad8 --- /dev/null +++ b/gradle/changelog/search_fixes.yaml @@ -0,0 +1,6 @@ +- type: fixed + description: Syntax highlighting on non highlighted fields ([#1901](https://github.com/scm-manager/scm-manager/pull/1901)) +- type: fixed + description: Ellipsis on new lines in code syntax highlighting ([#1901](https://github.com/scm-manager/scm-manager/pull/1901)) +- type: fixed + description: Ellipsis on content start or end in non code fields ([#1901](https://github.com/scm-manager/scm-manager/pull/1901)) diff --git a/scm-ui/ui-components/src/__resources__/ContentSearchHit.ts b/scm-ui/ui-components/src/__resources__/ContentSearchHit.ts deleted file mode 100644 index b55c370c59..0000000000 --- a/scm-ui/ui-components/src/__resources__/ContentSearchHit.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2020-present Cloudogu GmbH and Contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import { Hit } from "@scm-manager/ui-types"; - -export const javaHit: Hit = { - score: 2.5, - fields: { - content: { - highlighted: true, - fragments: [ - "import org.slf4j.LoggerFactory;\n\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * Jwt implementation of {@link <|[[--AccessTokenBuilder--]]|>}.\n * \n * @author Sebastian Sdorra\n * @since 2.0.0\n */\npublic final class <|[[--JwtAccessTokenBuilder--]]|> implements <|[[--AccessTokenBuilder--]]|> {\n\n /**\n * the logger for <|[[--JwtAccessTokenBuilder--]]|>\n */\n private static final Logger LOG = LoggerFactory.getLogger(<|[[--JwtAccessTokenBuilder.class--]]|>);\n \n private final KeyGenerator keyGenerator; \n private final SecureKeyResolver keyResolver; \n \n private String subject;\n private String issuer;\n", - " private final Map custom = Maps.newHashMap();\n \n <|[[--JwtAccessTokenBuilder--]]|>(KeyGenerator keyGenerator, SecureKeyResolver keyResolver) {\n this.keyGenerator = keyGenerator;\n this.keyResolver = keyResolver;\n }\n\n @Override\n public <|[[--JwtAccessTokenBuilder--]]|> subject(String subject) {\n", - ' public <|[[--JwtAccessTokenBuilder--]]|> custom(String key, Object value) {\n Preconditions.checkArgument(!Strings.isNullOrEmpty(key), "null or empty value not allowed");\n Preconditions.checkArgument(value != null, "null or empty value not allowed");\n' - ], - matchesContentStart: false, - matchesContentEnd: false - } - }, - _links: {} -}; - -export const bashHit: Hit = { - score: 2.5, - fields: { - content: { - highlighted: true, - fragments: [ - '# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n#\n\n<|[[--getent--]]|> group scm >/dev/null || groupadd -r scm\n<|[[--getent--]]|> passwd scm >/dev/null || \\\n useradd -r -g scm -M \\\n -s /sbin/nologin -d /var/lib/scm \\\n -c "user for the scm-server process" scm\nexit 0\n\n' - ], - matchesContentStart: false, - matchesContentEnd: true - } - }, - _links: {} -}; - -export const markdownHit: Hit = { - score: 2.5, - fields: { - content: { - highlighted: true, - fragments: [ - "---\ntitle: SCM-Manager v2 Test <|[[--Cases--]]|>\n---\n\nDescribes the expected behaviour for SCMM v2 REST Resources using manual tests.\n\nThe following states general test <|[[--cases--]]|> per HTTP Method and en expected return code as well as exemplary curl calls.\nResource-specifics are stated \n\n## Test <|[[--Cases--]]|>\n\n### GET\n\n- Collection Resource (e.g. `/users`)\n - Without parameters -> 200\n - Parameters\n - `?pageSize=1` -> Only one embedded element, pageTotal reflects the correct number of pages, `last` link points to last page.\n - `?pageSize=1&page=1` -> `next` link points to page 0 ; `prev` link points to page 2\n - `?sortBy=admin` -> Sorted by `admin` field of embedded objects\n - `?sortBy=admin&desc=true` -> Invert sorting\n- Individual Resource (e.g. `/users/scmadmin`)\n - Exists -> 200\n", - "\n### DELETE\n\n- existing -> 204\n- not existing -> 204\n- without permission -> 401\n\n## Exemplary calls & Resource specific test <|[[--cases--]]|>\n\nIn order to extend those tests to other Resources, have a look at the rest docs. Note that the Content Type is specific to each resource as well.\n" - ], - matchesContentStart: true, - matchesContentEnd: false - } - }, - _links: {} -}; diff --git a/scm-ui/ui-components/src/__resources__/SearchHit.ts b/scm-ui/ui-components/src/__resources__/SearchHit.ts new file mode 100644 index 0000000000..60135e98cc --- /dev/null +++ b/scm-ui/ui-components/src/__resources__/SearchHit.ts @@ -0,0 +1,97 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { Hit } from "@scm-manager/ui-types"; + +export const javaHit: Hit = { + score: 2.5, + fields: { + content: { + highlighted: true, + fragments: [ + "import org.slf4j.LoggerFactory;\n\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * Jwt implementation of {@link <|[[--AccessTokenBuilder--]]|>}.\n * \n * @author Sebastian Sdorra\n * @since 2.0.0\n */\npublic final class <|[[--JwtAccessTokenBuilder--]]|> implements <|[[--AccessTokenBuilder--]]|> {\n\n /**\n * the logger for <|[[--JwtAccessTokenBuilder--]]|>\n */\n private static final Logger LOG = LoggerFactory.getLogger(<|[[--JwtAccessTokenBuilder.class--]]|>);\n \n private final KeyGenerator keyGenerator; \n private final SecureKeyResolver keyResolver; \n \n private String subject;\n private String issuer;\n", + " private final Map custom = Maps.newHashMap();\n \n <|[[--JwtAccessTokenBuilder--]]|>(KeyGenerator keyGenerator, SecureKeyResolver keyResolver) {\n this.keyGenerator = keyGenerator;\n this.keyResolver = keyResolver;\n }\n\n @Override\n public <|[[--JwtAccessTokenBuilder--]]|> subject(String subject) {\n", + ' public <|[[--JwtAccessTokenBuilder--]]|> custom(String key, Object value) {\n Preconditions.checkArgument(!Strings.isNullOrEmpty(key), "null or empty value not allowed");\n Preconditions.checkArgument(value != null, "null or empty value not allowed");\n' + ], + matchesContentStart: false, + matchesContentEnd: false + } + }, + _links: {} +}; + +export const bashHit: Hit = { + score: 2.5, + fields: { + content: { + highlighted: true, + fragments: [ + '# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n#\n\n<|[[--getent--]]|> group scm >/dev/null || groupadd -r scm\n<|[[--getent--]]|> passwd scm >/dev/null || \\\n useradd -r -g scm -M \\\n -s /sbin/nologin -d /var/lib/scm \\\n -c "user for the scm-server process" scm\nexit 0\n\n' + ], + matchesContentStart: false, + matchesContentEnd: true + } + }, + _links: {} +}; + +export const markdownHit: Hit = { + score: 2.5, + fields: { + content: { + highlighted: true, + fragments: [ + "---\ntitle: SCM-Manager v2 Test <|[[--Cases--]]|>\n---\n\nDescribes the expected behaviour for SCMM v2 REST Resources using manual tests.\n\nThe following states general test <|[[--cases--]]|> per HTTP Method and en expected return code as well as exemplary curl calls.\nResource-specifics are stated \n\n## Test <|[[--Cases--]]|>\n\n### GET\n\n- Collection Resource (e.g. `/users`)\n - Without parameters -> 200\n - Parameters\n - `?pageSize=1` -> Only one embedded element, pageTotal reflects the correct number of pages, `last` link points to last page.\n - `?pageSize=1&page=1` -> `next` link points to page 0 ; `prev` link points to page 2\n - `?sortBy=admin` -> Sorted by `admin` field of embedded objects\n - `?sortBy=admin&desc=true` -> Invert sorting\n- Individual Resource (e.g. `/users/scmadmin`)\n - Exists -> 200\n", + "\n### DELETE\n\n- existing -> 204\n- not existing -> 204\n- without permission -> 401\n\n## Exemplary calls & Resource specific test <|[[--cases--]]|>\n\nIn order to extend those tests to other Resources, have a look at the rest docs. Note that the Content Type is specific to each resource as well.\n" + ], + matchesContentStart: true, + matchesContentEnd: false + } + }, + _links: {} +}; + +export const filenameXmlHit: Hit = { + score: 10.592262, + fields: { + content: { + highlighted: false, + value: + '\n\n\n 4.0.0\n\n \n scm-clients\n sonia.scm.clients\n 2.0.0-SNAPSHOT\n \n\n scm-cli-client\n 2.0.0-SNAPSHOT\n scm-cli-client\n\n \n\n \n\n \n javax.servlet\n javax.servlet-api\n ${servlet.version}\n \n\n \n javax.transaction\n jta\n 1.1\n provided\n \n\n \n sonia.scm.clients\n scm-client-impl\n 2.0.0-SNAPSHOT\n \n\n \n args4j\n args4j\n 2.0.29\n \n\n \n ch.qos.logback\n logback-classic\n ${logback.version}\n \n\n \n org.freemarker\n freemarker\n 2.3.21\n \n\n \n\n \n \n\n \n com.mycila.maven-license-plugin\n maven-license-plugin\n 1.9.0\n \n
http://download.scm-manager.org/licenses/mvn-license.txt
\n \n src/**\n **/test/**\n \n \n target/**\n .hg/**\n **/*.ftl\n \n true\n
\n
\n\n \n org.apache.maven.plugins\n maven-assembly-plugin\n 2.3\n \n \n \n sonia.scm.cli.App\n \n \n \n jar-with-dependencies\n \n \n \n \n package\n \n single\n \n \n \n \n\n
\n
\n\n \n \n\n it\n\n \n \n\n \n org.apache.maven.plugins\n maven-dependency-plugin\n 2.4\n \n \n package\n \n copy\n \n \n \n \n sonia.scm\n scm-webapp\n ${project.version}\n war\n ${project.build.directory}/webapp\n scm-webapp.war\n \n \n \n \n \n \n\n \n org.apache.maven.plugins\n maven-failsafe-plugin\n 2.12\n \n \n ${project.version}\n \n \n \n \n integration-test\n \n integration-test\n \n \n \n verify\n \n verify\n \n \n \n \n\n \n org.eclipse.jetty\n jetty-maven-plugin\n ${jetty.maven.version}\n \n 8085\n STOP\n \n \n scm.home\n target/scm-it\n \n \n file.encoding\n UTF-8\n \n \n \n 8081\n \n \n /scm\n \n ${project.build.directory}/webapp/scm-webapp.war\n 0\n true\n \n \n \n start-jetty\n pre-integration-test\n \n deploy-war\n \n \n \n stop-jetty\n post-integration-test\n \n stop\n \n \n \n \n\n \n \n\n \n \n\n
\n' + } + }, + _links: {} +}; + +export const pullRequestHit: Hit = { + score: 0.2837065, + fields: { + description: { + highlighted: false, + value: + "The Hitchhiker's Guide to the Galaxy (sometimes referred to as **HG2G**, **HHGTTG** or **H2G2**) is a comedy science fiction series created by Douglas Adams. Originally a radio comedy broadcast on BBC Radio 4 in 1978, it was later adapted to other formats, including stage shows, novels, comic books, a 1981 TV series, a 1984 video game, and 2005 feature film.\n\nThis fixes a SQL Injection, a Race condition and a XSS\n\nA prominent series in British popular culture, The Hitchhiker's Guide to the Galaxy has become an international multi-media phenomenon; the novels are the most widely distributed, having been translated into more than 30 languages by 2005. In 2017, BBC Radio 4 announced a 40th-anniversary celebration with Dirk Maggs, one of the original producers, in charge. This sixth series of the sci-fi spoof has been based on Eoin Colfer's book And Another Thing, with additional unpublished material by Douglas Adams. The first of six new episodes was broadcast on 8 March 2018." + } + }, + _links: {} +}; diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index 4ab1b8a5e1..ba05a53b89 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -90255,6 +90255,278 @@ In order to extend those tests to other Resources, have a look at the rest docs. `; +exports[`Storyshots TextHitField Non Content Search 1`] = ` +
+  <?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <artifactId>scm-clients</artifactId>
+    <groupId>sonia.scm.clients</groupId>
+    <version>2.0.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>scm-cli-client</artifactId>
+  <version>2.0.0-SNAPSHOT</version>
+  <name>scm-cli-client</name>
+
+  <dependencies>
+
+    <!-- fix javadoc -->
+
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>javax.servlet-api</artifactId>
+      <version>\${servlet.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>javax.transaction</groupId>
+      <artifactId>jta</artifactId>
+      <version>1.1</version>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>sonia.scm.clients</groupId>
+      <artifactId>scm-client-impl</artifactId>
+      <version>2.0.0-SNAPSHOT</version>
+    </dependency>
+
+    <dependency>
+      <groupId>args4j</groupId>
+      <artifactId>args4j</artifactId>
+      <version>2.0.29</version>
+    </dependency>
+
+    <dependency>
+      <groupId>ch.qos.logback</groupId>
+      <artifactId>logback-classic</artifactId>
+      <version>\${logback.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.freemarker</groupId>
+      <artifactId>freemarker</artifactId>
+      <version>2.3.21</version>
+    </dependency>
+
+  </dependencies>
+
+  <build>
+    <plugins>
+
+      <plugin>
+        <groupId>com.mycila.maven-license-plugin</groupId>
+        <artifactId>maven-license-plugin</artifactId>
+        <version>1.9.0</version>
+        <configuration>
+          <header>http://download.scm-manager.org/licenses/mvn-license.txt</header>
+          <includes>
+            <include>src/**</include>
+            <include>**/test/**</include>
+          </includes>
+          <excludes>
+            <exclude>target/**</exclude>
+            <exclude>.hg/**</exclude>
+            <exclude>**/*.ftl</exclude>
+          </excludes>
+          <strictCheck>true</strictCheck>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-assembly-plugin</artifactId>
+        <version>2.3</version>
+        <configuration>
+          <archive>
+            <manifest>
+              <mainClass>sonia.scm.cli.App</mainClass>
+            </manifest>
+          </archive>
+          <descriptorRefs>
+            <descriptorRef>jar-with-dependencies</descriptorRef>
+          </descriptorRefs>
+        </configuration>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>single</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+
+    </plugins>
+  </build>
+
+  <profiles>
+    <profile>
+
+      <id>it</id>
+
+      <build>
+        <plugins>
+
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-dependency-plugin</artifactId>
+            <version>2.4</version>
+            <executions>
+              <execution>
+                <phase>package</phase>
+                <goals>
+                  <goal>copy</goal>
+                </goals>
+                <configuration>
+                  <artifactItems>
+                    <artifactItem>
+                      <groupId>sonia.scm</groupId>
+                      <artifactId>scm-webapp</artifactId>
+                      <version>\${project.version}</version>
+                      <type>war</type>
+                      <outputDirectory>\${project.build.directory}/webapp</outputDirectory>
+                      <destFileName>scm-webapp.war</destFileName>
+                    </artifactItem>
+                  </artifactItems>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <version>2.12</version>
+            <configuration>
+              <systemPropertyVariables>
+                <scm.version>\${project.version}</scm.version>
+              </systemPropertyVariables>
+            </configuration>
+            <executions>
+              <execution>
+                <id>integration-test</id>
+                <goals>
+                  <goal>integration-test</goal>
+                </goals>
+              </execution>
+              <execution>
+                <id>verify</id>
+                <goals>
+                  <goal>verify</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+
+          <plugin>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-maven-plugin</artifactId>
+            <version>\${jetty.maven.version}</version>
+            <configuration>
+              <stopPort>8085</stopPort>
+              <stopKey>STOP</stopKey>
+              <systemProperties>
+                <systemProperty>
+                  <name>scm.home</name>
+                  <value>target/scm-it</value>
+                </systemProperty>
+                <systemProperty>
+                  <name>file.encoding</name>
+                  <value>UTF-8</value>
+                </systemProperty>
+              </systemProperties>
+              <httpConnector>
+                <port>8081</port>
+              </httpConnector>
+              <webApp>
+                <contextPath>/scm</contextPath>
+              </webApp>
+              <war>\${project.build.directory}/webapp/scm-webapp.war</war>
+              <scanIntervalSeconds>0</scanIntervalSeconds>
+              <daemon>true</daemon>
+            </configuration>
+            <executions>
+              <execution>
+                <id>start-jetty</id>
+                <phase>pre-integration-test</phase>
+                <goals>
+                  <goal>deploy-war</goal>
+                </goals>
+              </execution>
+              <execution>
+                <id>stop-jetty</id>
+                <phase>post-integration-test</phase>
+                <goals>
+                  <goal>stop</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+
+        </plugins>
+      </build>
+
+    </profile>
+  </profiles>
+
+</project>
+
+
+`; + +exports[`Storyshots TextHitField Truncate 1`] = ` +
+  The Hitchhiker's Guide to the Galaxy (sometimes referred to as **HG2G**, **HHGTTG** or **H2G2**) is a comedy science fiction ser...
+
+`; + +exports[`Storyshots TextHitField Truncate Keep Whole Line 1`] = ` +
+  <?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <artifactId>scm-clients</artifactId>
+    <groupId>sonia.scm.clients</groupId>
+    <version>2.0.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>scm-cli-client</artifactId>
+  <version>2.0.0-SNAPSHOT</version>
+  <name>scm-cli-client</name>
+
+  <dependencies>
+
+    <!-- fix javadoc -->
+
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>javax.servlet-api</artifactId>
+      <version>\${servlet.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>javax.transaction</groupId>
+      <artifactId>jta</artifactId>
+      <version>1.1</version>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>sonia.scm.clients</groupId>
+      <artifactId>scm-client-impl</artifactId>
+      <version>2.0.0-SNAPSHOT</version>
+...
+
+`; + exports[`Storyshots TextHitField Unknown SyntaxHighlighting 1`] = `
   ...
diff --git a/scm-ui/ui-components/src/search/TextHitField.stories.tsx b/scm-ui/ui-components/src/search/TextHitField.stories.tsx
index 5a40127f59..dba9907415 100644
--- a/scm-ui/ui-components/src/search/TextHitField.stories.tsx
+++ b/scm-ui/ui-components/src/search/TextHitField.stories.tsx
@@ -23,7 +23,7 @@
  */
 import React from "react";
 import { storiesOf } from "@storybook/react";
-import { bashHit, javaHit, markdownHit } from "../__resources__/ContentSearchHit";
+import { bashHit, filenameXmlHit, javaHit, markdownHit, pullRequestHit } from "../__resources__/SearchHit";
 import TextHitField from "./TextHitField";
 
 storiesOf("TextHitField", module)
@@ -51,4 +51,19 @@ storiesOf("TextHitField", module)
     
       
     
+ )) + .add("Non Content Search", () => ( +
+      
+    
+ )) + .add("Truncate", () => ( +
+      
+    
+ )) + .add("Truncate Keep Whole Line", () => ( +
+      
+    
)); diff --git a/scm-ui/ui-components/src/search/TextHitField.tsx b/scm-ui/ui-components/src/search/TextHitField.tsx index ca2ce89e98..9da4a9b5cd 100644 --- a/scm-ui/ui-components/src/search/TextHitField.tsx +++ b/scm-ui/ui-components/src/search/TextHitField.tsx @@ -40,11 +40,7 @@ const HighlightedTextField: FC = ({ field, syntaxHigh {field.fragments.map((fragment, i) => ( {field.matchesContentStart ? null : separator} - {syntaxHighlightingLanguage ? ( - - ) : ( - - )} + {i + 1 >= field.fragments.length && !field.matchesContentEnd ? separator : null} ))} @@ -52,6 +48,18 @@ const HighlightedTextField: FC = ({ field, syntaxHigh ); }; +type FieldFragmentProps = { + fragment: string; + syntaxHighlightingLanguage?: string; +}; + +const FieldFragment: FC = ({ fragment, syntaxHighlightingLanguage }) => { + if (syntaxHighlightingLanguage) { + return ; + } + return ; +}; + type Props = { hit: Hit; field: string; @@ -59,6 +67,20 @@ type Props = { syntaxHighlightingLanguage?: string; }; +function truncate(value: string, truncateValueAt: number = 0, syntaxHighlightingLanguage?: string): string { + if (truncateValueAt > 0 && value.length > truncateValueAt) { + if (syntaxHighlightingLanguage) { + let nextLineBreak = value.indexOf("\n", truncateValueAt); + if (nextLineBreak >= 0 && nextLineBreak < value.length - 1) { + value = value.substring(0, nextLineBreak) + "\n..."; + } + } else { + value = value.substring(0, truncateValueAt) + "..."; + } + } + return value; +} + const TextHitField: FC = ({ hit, field: fieldName, @@ -75,8 +97,9 @@ const TextHitField: FC = ({ let value = field.value; if (value === "") { return <>{children}; - } else if (typeof value === "string" && truncateValueAt > 0 && value.length > truncateValueAt) { - value = value.substring(0, truncateValueAt) + "..."; + } else if (typeof value === "string") { + const v = truncate(value, truncateValueAt, syntaxHighlightingLanguage); + return ; } return <>{value}; } diff --git a/scm-webapp/src/main/java/sonia/scm/search/LuceneHighlighter.java b/scm-webapp/src/main/java/sonia/scm/search/LuceneHighlighter.java index 5f7bcd989f..826f4265e9 100644 --- a/scm-webapp/src/main/java/sonia/scm/search/LuceneHighlighter.java +++ b/scm-webapp/src/main/java/sonia/scm/search/LuceneHighlighter.java @@ -75,7 +75,7 @@ public final class LuceneHighlighter { if (fieldAnalyzer == Indexed.Analyzer.CODE) { return keepWholeLine(value, fragments); } - return Arrays.stream(fragments).map(ContentFragment::new) + return Arrays.stream(fragments).map(f -> createContentFragment(value, f)) .toArray(ContentFragment[]::new); } @@ -122,4 +122,9 @@ public final class LuceneHighlighter { return new ContentFragment(snippet + content.substring(index + raw.length(), end) + (matchesContentEnd ? "" : "\n"), matchesContentStart, matchesContentEnd); } + private ContentFragment createContentFragment(String content, String fragment) { + String raw = fragment.replace(PRE_TAG, "").replace(POST_TAG, ""); + return new ContentFragment(fragment, content.startsWith(raw), content.endsWith(raw)); + } + } diff --git a/scm-webapp/src/test/java/sonia/scm/search/LuceneHighlighterTest.java b/scm-webapp/src/test/java/sonia/scm/search/LuceneHighlighterTest.java index 7e8c7d2843..4eb9bde71b 100644 --- a/scm-webapp/src/test/java/sonia/scm/search/LuceneHighlighterTest.java +++ b/scm-webapp/src/test/java/sonia/scm/search/LuceneHighlighterTest.java @@ -123,6 +123,24 @@ class LuceneHighlighterTest { assertThat(contentFragments[0].isMatchesContentEnd()).isTrue(); } + @Test + void shouldMatchContentStartWithDefaultAnalyzer() throws InvalidTokenOffsetsException, IOException { + ContentFragment[] contentFragments = highlight("GameOfLife.java", "gameoflife"); + + assertThat(contentFragments).hasSize(1); + assertThat(contentFragments[0].isMatchesContentStart()).isTrue(); + assertThat(contentFragments[0].isMatchesContentEnd()).isFalse(); + } + + @Test + void shouldMatchContentEndWithDefaultAnalyzer() throws InvalidTokenOffsetsException, IOException { + ContentFragment[] contentFragments = highlight("Button.tsx", "default"); + + assertThat(contentFragments).hasSize(1); + assertThat(contentFragments[0].isMatchesContentStart()).isFalse(); + assertThat(contentFragments[0].isMatchesContentEnd()).isTrue(); + } + @Nested class IsHighlightableTests { @@ -162,6 +180,16 @@ class LuceneHighlighterTest { } + private ContentFragment[] highlight(String resource, String search) throws IOException, InvalidTokenOffsetsException { + StandardAnalyzer analyzer = new StandardAnalyzer(); + Query query = new TermQuery(new Term("content", search)); + + String content = content(resource); + + LuceneHighlighter highlighter = new LuceneHighlighter(analyzer, query); + return highlighter.highlight("content", Indexed.Analyzer.DEFAULT, content); + } + private ContentFragment[] highlightCode(String resource, String search) throws IOException, InvalidTokenOffsetsException { NonNaturalLanguageAnalyzer analyzer = new NonNaturalLanguageAnalyzer(); Query query = new TermQuery(new Term("content", search));