Skip to content

Commit f7da341

Browse files
authored
fix(search): properly display multiple search terms (#7980)
When searching for multiple terms in npm, the highlighting code has a bug where it duplicates the output any time there are matching terms. This fixes the highlighting code. Before: ![output of "npm search gar promisify" showing the name being duplicated](https://github.com/user-attachments/assets/2f34ece7-7563-4db1-a540-3bb661a4c3e0) After: ![output of "node . search gar promisify" showing the name being displayed correctly](https://github.com/user-attachments/assets/ba31fcd9-caf3-4a08-8bbb-7f5242f0098b)
1 parent a481f57 commit f7da341

File tree

3 files changed

+194
-27
lines changed

3 files changed

+194
-27
lines changed

‎lib/utils/format-search-stream.js

+24-27
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ class TextOutputStream extends Minipass {
8282

8383
constructor (opts) {
8484
super()
85-
this.#args = opts.args.map(s => s.toLowerCase()).filter(Boolean)
85+
// Consider a search for "cowboys" and "boy". If we highlight "boys" first the "cowboys" string will no longer string match because of the ansi highlighting added to "boys". If we highlight "boy" second then the ansi reset at the end will make the highlighting only on "cowboy" with a normal "s". Neither is perfect but at least the first option doesn't do partial highlighting. So, we sort strings smaller to larger
86+
this.#args = opts.args.map(s => s.toLowerCase()).filter(Boolean).sort((a, b) => a.length - b.length)
8687
this.#chalk = opts.npm.chalk
8788
this.#exclude = opts.exclude
8889
this.#parseable = opts.parseable
@@ -124,38 +125,17 @@ class TextOutputStream extends Minipass {
124125
}
125126
}).join(' ')
126127

127-
let description = []
128-
for (const arg of this.#args) {
129-
const finder = pkg.description.toLowerCase().split(arg.toLowerCase())
130-
let p = 0
131-
for (const f of finder) {
132-
description.push(pkg.description.slice(p, p + f.length))
133-
const word = pkg.description.slice(p + f.length, p + f.length + arg.length)
134-
description.push(this.#chalk.cyan(word))
135-
p += f.length + arg.length
136-
}
137-
}
138-
description = description.filter(Boolean)
139-
let name = pkg.name
128+
const description = this.#highlight(pkg.description)
129+
let name
140130
if (this.#args.includes(pkg.name)) {
141131
name = this.#chalk.cyan(pkg.name)
142132
} else {
143-
name = []
144-
for (const arg of this.#args) {
145-
const finder = pkg.name.toLowerCase().split(arg.toLowerCase())
146-
let p = 0
147-
for (const f of finder) {
148-
name.push(pkg.name.slice(p, p + f.length))
149-
const word = pkg.name.slice(p + f.length, p + f.length + arg.length)
150-
name.push(this.#chalk.cyan(word))
151-
p += f.length + arg.length
152-
}
153-
}
154-
name = this.#chalk.blue(name.join(''))
133+
name = this.#highlight(pkg.name)
134+
name = this.#chalk.blue(name)
155135
}
156136

157137
if (description.length) {
158-
output = `${name}\n${description.join('')}\n`
138+
output = `${name}\n${description}\n`
159139
} else {
160140
output = `${name}\n`
161141
}
@@ -171,4 +151,21 @@ class TextOutputStream extends Minipass {
171151
output += `${this.#chalk.blue(`https://npm.im/${pkg.name}`)}\n`
172152
return super.write(output)
173153
}
154+
155+
#highlight (input) {
156+
let output = input
157+
for (const arg of this.#args) {
158+
let i = output.toLowerCase().indexOf(arg)
159+
while (i > -1) {
160+
const highlit = this.#chalk.cyan(output.slice(i, i + arg.length))
161+
output = [
162+
output.slice(0, i),
163+
highlit,
164+
output.slice(i + arg.length),
165+
].join('')
166+
i = output.toLowerCase().indexOf(arg, i + highlit.length)
167+
}
168+
}
169+
return output
170+
}
174171
}

‎tap-snapshots/test/lib/commands/search.js.test.cjs

+146
Original file line numberDiff line numberDiff line change
@@ -932,6 +932,152 @@ Maintainers: lukekarrys
932932
https://npm.im/pkg-no-desc
933933
`
934934

935+
exports[`test/lib/commands/search.js TAP search multiple terms --color > should have expected search results with color 1`] = `
936+
libnpm
937+
Collection of programmatic APIs for the npm CLI
938+
Version 3.0.1 published 2019-07-16 by isaacs
939+
Maintainers: nlf ruyadorno darcyclarke isaacs
940+
Keywords: npm api package manager lib
941+
https://npm.im/libnpm
942+
libnpmaccess
943+
programmatic library for \`npm access\` commands
944+
Version 4.0.1 published 2020-11-03 by nlf
945+
Maintainers: nlf ruyadorno darcyclarke isaacs
946+
Keywords: libnpmaccess
947+
https://npm.im/libnpmaccess
948+
@evocateur/libnpmaccess
949+
programmatic library for \`npm access\` commands
950+
Version 3.1.2 published 2019-07-16 by evocateur
951+
Maintainers: evocateur
952+
https://npm.im/@evocateur/libnpmaccess
953+
@evocateur/libnpmpublish
954+
Programmatic API for the bits behind npm publish and unpublish
955+
Version 1.2.2 published 2019-07-16 by evocateur
956+
Maintainers: evocateur
957+
https://npm.im/@evocateur/libnpmpublish
958+
libnpmorg
959+
Programmatic api for \`npm org\` commands
960+
Version 2.0.1 published 2020-11-03 by nlf
961+
Maintainers: nlf ruyadorno darcyclarke isaacs
962+
Keywords: libnpm npm package manager api orgs teams
963+
https://npm.im/libnpmorg
964+
libnpmsearch
965+
Programmatic API for searching in npm and compatible registries.
966+
Version 3.1.0 published 2020-12-08 by isaacs
967+
Maintainers: nlf ruyadorno darcyclarke isaacs
968+
Keywords: npm search api libnpm
969+
https://npm.im/libnpmsearch
970+
libnpmteam
971+
npm Team management APIs
972+
Version 2.0.2 published 2020-11-03 by nlf
973+
Maintainers: nlf ruyadorno darcyclarke isaacs
974+
https://npm.im/libnpmteam
975+
libnpmpublish
976+
Programmatic API for the bits behind npm publish and unpublish
977+
Version 4.0.0 published 2020-11-03 by nlf
978+
Maintainers: nlf ruyadorno darcyclarke isaacs
979+
https://npm.im/libnpmpublish
980+
libnpmfund
981+
Programmatic API for npm fund
982+
Version 1.0.2 published 2020-12-08 by isaacs
983+
Maintainers: nlf ruyadorno darcyclarke isaacs
984+
Keywords: npm npmcli libnpm cli git fund gitfund
985+
https://npm.im/libnpmfund
986+
@npmcli/map-workspaces
987+
Retrieves a name:pathname Map for a given workspaces config
988+
Version 1.0.1 published 2020-09-30 by ruyadorno
989+
Maintainers: nlf ruyadorno darcyclarke isaacs
990+
Keywords: npm bad map npmcli libnpm cli workspaces map-workspaces
991+
https://npm.im/@npmcli/map-workspaces
992+
libnpmversion
993+
library to do the things that 'npm version' does
994+
Version 1.0.7 published 2020-11-04 by isaacs
995+
Maintainers: nlf ruyadorno darcyclarke isaacs
996+
https://npm.im/libnpmversion
997+
@types/libnpmsearch
998+
TypeScript definitions for libnpmsearch
999+
Version 2.0.1 published 2019-09-26 by types
1000+
Maintainers: types
1001+
https://npm.im/@types/libnpmsearch
1002+
pkg-no-desc
1003+
Version 1.0.0 published 2019-09-26 by lukekarrys
1004+
Maintainers: lukekarrys
1005+
https://npm.im/pkg-no-desc
1006+
`
1007+
1008+
exports[`test/lib/commands/search.js TAP search multiple terms text > should have expected search results 1`] = `
1009+
libnpm
1010+
Collection of programmatic APIs for the npm CLI
1011+
Version 3.0.1 published 2019-07-16 by isaacs
1012+
Maintainers: nlf ruyadorno darcyclarke isaacs
1013+
Keywords: npm api package manager lib
1014+
https://npm.im/libnpm
1015+
libnpmaccess
1016+
programmatic library for \`npm access\` commands
1017+
Version 4.0.1 published 2020-11-03 by nlf
1018+
Maintainers: nlf ruyadorno darcyclarke isaacs
1019+
Keywords: libnpmaccess
1020+
https://npm.im/libnpmaccess
1021+
@evocateur/libnpmaccess
1022+
programmatic library for \`npm access\` commands
1023+
Version 3.1.2 published 2019-07-16 by evocateur
1024+
Maintainers: evocateur
1025+
https://npm.im/@evocateur/libnpmaccess
1026+
@evocateur/libnpmpublish
1027+
Programmatic API for the bits behind npm publish and unpublish
1028+
Version 1.2.2 published 2019-07-16 by evocateur
1029+
Maintainers: evocateur
1030+
https://npm.im/@evocateur/libnpmpublish
1031+
libnpmorg
1032+
Programmatic api for \`npm org\` commands
1033+
Version 2.0.1 published 2020-11-03 by nlf
1034+
Maintainers: nlf ruyadorno darcyclarke isaacs
1035+
Keywords: libnpm npm package manager api orgs teams
1036+
https://npm.im/libnpmorg
1037+
libnpmsearch
1038+
Programmatic API for searching in npm and compatible registries.
1039+
Version 3.1.0 published 2020-12-08 by isaacs
1040+
Maintainers: nlf ruyadorno darcyclarke isaacs
1041+
Keywords: npm search api libnpm
1042+
https://npm.im/libnpmsearch
1043+
libnpmteam
1044+
npm Team management APIs
1045+
Version 2.0.2 published 2020-11-03 by nlf
1046+
Maintainers: nlf ruyadorno darcyclarke isaacs
1047+
https://npm.im/libnpmteam
1048+
libnpmpublish
1049+
Programmatic API for the bits behind npm publish and unpublish
1050+
Version 4.0.0 published 2020-11-03 by nlf
1051+
Maintainers: nlf ruyadorno darcyclarke isaacs
1052+
https://npm.im/libnpmpublish
1053+
libnpmfund
1054+
Programmatic API for npm fund
1055+
Version 1.0.2 published 2020-12-08 by isaacs
1056+
Maintainers: nlf ruyadorno darcyclarke isaacs
1057+
Keywords: npm npmcli libnpm cli git fund gitfund
1058+
https://npm.im/libnpmfund
1059+
@npmcli/map-workspaces
1060+
Retrieves a name:pathname Map for a given workspaces config
1061+
Version 1.0.1 published 2020-09-30 by ruyadorno
1062+
Maintainers: nlf ruyadorno darcyclarke isaacs
1063+
Keywords: npm bad map npmcli libnpm cli workspaces map-workspaces
1064+
https://npm.im/@npmcli/map-workspaces
1065+
libnpmversion
1066+
library to do the things that 'npm version' does
1067+
Version 1.0.7 published 2020-11-04 by isaacs
1068+
Maintainers: nlf ruyadorno darcyclarke isaacs
1069+
https://npm.im/libnpmversion
1070+
@types/libnpmsearch
1071+
TypeScript definitions for libnpmsearch
1072+
Version 2.0.1 published 2019-09-26 by types
1073+
Maintainers: types
1074+
https://npm.im/@types/libnpmsearch
1075+
pkg-no-desc
1076+
Version 1.0.0 published 2019-09-26 by lukekarrys
1077+
Maintainers: lukekarrys
1078+
https://npm.im/pkg-no-desc
1079+
`
1080+
9351081
exports[`test/lib/commands/search.js TAP search no publisher > should have filtered expected search results 1`] = `
9361082
custom-registry
9371083
Version 1.0.0 published prehistoric by ???

‎test/lib/commands/search.js

+24
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,18 @@ t.test('search', t => {
2626
t.matchSnapshot(joinedOutput(), 'should have expected search results')
2727
})
2828

29+
t.test('multiple terms text', async t => {
30+
const { npm, joinedOutput } = await loadMockNpm(t)
31+
const registry = new MockRegistry({
32+
tap: t,
33+
registry: npm.config.get('registry'),
34+
})
35+
36+
registry.search({ results: libnpmsearchResultFixture })
37+
await npm.exec('search', ['libnpm', 'publish'])
38+
t.matchSnapshot(joinedOutput(), 'should have expected search results')
39+
})
40+
2941
t.test('<name> --json', async t => {
3042
const { npm, joinedOutput } = await loadMockNpm(t, { config: { json: true } })
3143
const registry = new MockRegistry({
@@ -68,6 +80,18 @@ t.test('search', t => {
6880
t.matchSnapshot(joinedOutput(), 'should have expected search results with color')
6981
})
7082

83+
t.test('multiple terms --color', async t => {
84+
const { npm, joinedOutput } = await loadMockNpm(t, { config: { color: 'always' } })
85+
const registry = new MockRegistry({
86+
tap: t,
87+
registry: npm.config.get('registry'),
88+
})
89+
90+
registry.search({ results: libnpmsearchResultFixture })
91+
await npm.exec('search', ['libnpm', 'publish'])
92+
t.matchSnapshot(joinedOutput(), 'should have expected search results with color')
93+
})
94+
7195
t.test('/<name>/--color', async t => {
7296
const { npm, joinedOutput } = await loadMockNpm(t, { config: { color: 'always' } })
7397
const registry = new MockRegistry({

0 commit comments

Comments
 (0)