New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feature: Implement Search Settings Ability #416
Merged
Merged
Changes from 3 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
fc1ebbd
Initial setup of new pane, testing settings list items
confused-Techie ba9c93f
More realize setup
confused-Techie 0980109
All final aspects to make functional
confused-Techie a6d27ed
Moved things around a bit to make testing easier, adding some tests, …
confused-Techie 61903ae
Resolve failing spec
confused-Techie b1ced99
Custom styling, include package name, pull out `getSettingTitle` fro…
confused-Techie edf8092
Modify bonus' on search
confused-Techie 794247a
Add Icon next to setting namespace
confused-Techie a000fee
Switched which side the package icon appears
confused-Techie f8ed84a
Some settings filters, display search score, configurable meta
confused-Techie 49461e5
Parse search text by normalizing all text data, not just package entries
confused-Techie aa0c192
Hide new feature behind `enableSettingsSearch` toggle
confused-Techie 7c6546f
Enable the feature by default, add warning in panel
confused-Techie 75d051b
Final stylistic choices on warning
confused-Techie 6575748
Final Final stylistic choices for the warning
confused-Techie dbb40bb
Merge branch 'master' into search-settings
confused-Techie File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
/** @babel */ | ||
/** @jsx etch.dom */ | ||
|
||
import etch from 'etch' | ||
import { shell } from 'electron' | ||
import { Disposable, CompositeDisposable } from 'atom' | ||
|
||
export default class SearchSettingView { | ||
constructor(setting, settingsView) { | ||
this.settingsView = settingsView | ||
this.setting = setting | ||
this.disposables = new CompositeDisposable() | ||
|
||
etch.initialize(this) | ||
|
||
this.handleButtonEvents() | ||
} | ||
|
||
render () { | ||
const title = this.setting.title ?? ""; | ||
const path = this.setting.path; | ||
const description = this.setting.description ?? ""; | ||
|
||
return ( | ||
<div className='package-card col-lg-8'> | ||
<div className='body'> | ||
<h4 className='card-name'> | ||
<a ref='settingLink'> | ||
<span className='package-name'>{title}</span> | ||
<span className='value'>{path}</span> | ||
</a> | ||
</h4> | ||
<span className='package-description'>{description}</span> | ||
|
||
</div> | ||
|
||
</div> | ||
) | ||
} | ||
|
||
update () {} | ||
|
||
destroy () { | ||
this.disposables.dispose() | ||
return etch.destroy(this) | ||
} | ||
|
||
handleButtonEvents () { | ||
const settingsClickHandler = (event) => { | ||
event.stopPropagation() | ||
//this.settingsView.showPanelForURI("atom://settings-view") | ||
|
||
// Lets check if the setting we want to open is built in or from a package | ||
const settingLocation = this.setting.path.split(".")[0] | ||
// The above is the location where the setting exists, such as Core, or a packages name | ||
|
||
switch(settingLocation) { | ||
case "core": | ||
// There are some special cases of settings broken off into other panels | ||
let settingName = this.setting.path.split(".")[1] | ||
if (settingName === 'uriHandlerRegistration') { | ||
// the URI handler doesn't have any registered uri to actually reach it | ||
// funnily enough. So we will prompt a notification to go there | ||
atom.notifications.addInfo("Sorry, Pulsar is unable to link to this setting. Please select 'URI Handling' on the sidebar.") | ||
} else { | ||
atom.workspace.open("atom://config/core") | ||
} | ||
break; | ||
case "editor": | ||
atom.workspace.open("atom://config/editor") | ||
break; | ||
default: | ||
// The handling for any packages name | ||
atom.workspace.open(`atom://config/packages/${settingLocation}`) | ||
break; | ||
} | ||
//atom.workspace.open("atom://config/core/closeDeletedFileTabs") | ||
// Open the relevant settings page | ||
} | ||
|
||
this.refs.settingLink.addEventListener('click', settingsClickHandler) | ||
this.disposables.add(new Disposable(() => { this.refs.settingLink.removeEventListener('click', settingsClickHandler) })) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,249 @@ | ||
/** @babel */ | ||
/** @jsx etch.dom */ | ||
|
||
import { TextEditor, CompositeDisposable } from 'atom' | ||
import etch from 'etch' | ||
import CollapsibleSectionPanel from './collapsible-section-panel' | ||
import SearchSettingView from './search-setting-view' | ||
|
||
export default class SearchSettingsPanel extends CollapsibleSectionPanel { | ||
constructor(settingsView) { | ||
super() | ||
etch.initialize(this) | ||
this.settingsView = settingsView | ||
this.searchResults = [] | ||
// Get all available settings | ||
this.settingsSchema = atom.config.schema.properties; | ||
|
||
this.subscriptions = new CompositeDisposable() | ||
this.subscriptions.add(this.handleEvents()) | ||
this.subscriptions.add(atom.commands.add(this.element, { | ||
'core:move-up': () => { this.scrollUp() }, | ||
'core:move-down': () => { this.scrollDown() }, | ||
'core:page-up': () => { this.pageUp() }, | ||
'core:page-down': () => { this.pageDown() }, | ||
'core:move-to-top': () => { this.scrollToTop() }, | ||
'core:move-to-bottom': () => { this.scrollToBottom() } | ||
})) | ||
|
||
this.subscriptions.add( | ||
this.refs.searchEditor.onDidStopChanging(() => { this.matchSettings() }) | ||
) | ||
} | ||
|
||
focus () { | ||
this.refs.searchEditor.element.focus() | ||
} | ||
|
||
show () { | ||
this.element.style.display = '' | ||
// Don't show the loading for search results as soon as page appears | ||
this.refs.loadingArea.style.display = 'none' | ||
} | ||
|
||
destroy () { | ||
this.subscriptions.dispose() | ||
return etch.destroy(this) | ||
} | ||
|
||
update () {} | ||
|
||
render () { | ||
return ( | ||
<div className='panels-item' tabIndex='-1'> | ||
<section className='section'> | ||
<div className='section-container'> | ||
<div className='section-heading icon icon-search-save'> | ||
Search Pulsar's Settings | ||
</div> | ||
<div className='editor-container'> | ||
<TextEditor ref='searchEditor' mini placeholderText='Start Searching for Settings' /> | ||
</div> | ||
|
||
<section className='sub-section search-results'> | ||
<h3 ref='searchHeader' className='sub-section-heading icon icon-package'> | ||
Search Results | ||
</h3> | ||
<div ref='searchResults' className='container package-container'> | ||
<div ref='loadingArea' className='alert alert-info loading-area icon icon-hourglass'> | ||
Loading Results... | ||
</div> | ||
</div> | ||
</section> | ||
|
||
</div> | ||
</section> | ||
</div> | ||
) | ||
} | ||
|
||
matchSettings () { | ||
// this is called after the user types. | ||
// So lets show our loading message after removing any previous results | ||
this.clearSearchResults() | ||
this.refs.loadingArea.style.display = '' | ||
this.filterSettings(this.refs.searchEditor.getText()) | ||
} | ||
|
||
clearSearchResults () { | ||
for (let i = 0; i < this.searchResults.length; i++) { | ||
this.searchResults[i].destroy() | ||
} | ||
this.searchResults = [] | ||
} | ||
|
||
filterSettings (text) { | ||
let rankedResults = []; | ||
|
||
for (const setting in this.settingsSchema) { | ||
// The top level item should always be an object, but just in case we will check. | ||
// If the top level item returned is not an object it will NOT be listed | ||
if (this.settingsSchema[setting].type === "object") { | ||
for (const item in this.settingsSchema[setting].properties) { | ||
|
||
// Now to generate results for the top level settings within each package | ||
// or area of settings such as `core` or `find-and-replace`. | ||
// We will also still descend one level further if we find an object | ||
const passString = (string) => { | ||
return string?.toLowerCase() ?? ""; | ||
}; | ||
|
||
let schema = this.settingsSchema[setting].properties[item]; | ||
|
||
let rankedTitle = this.getScore(text, passString(schema.title)); | ||
let rankedDescription = this.getScore(text, passString(schema.description)); | ||
let rankedSettingName = this.getScore(text, passString(setting)); | ||
let rankedSettingItem = this.getScore(text, passString(item)); | ||
schema.rank = { | ||
title: rankedTitle, | ||
description: rankedDescription, | ||
settingName: rankedSettingName, | ||
settingItem: rankedSettingItem | ||
}; | ||
schema.path = `${setting}.${item}`; | ||
|
||
// Now to calculate the total score of the search results. | ||
// The total score will be a sum of all individual scores, with | ||
// weighted bonus' for higher matches depending on where the match was | ||
let titleBonus = (schema.rank.title.score > 0.8) ? 0.2 : 0; | ||
let perfectTitleBonus = (schema.rank.title.score === 1) ? 0.1 : 0; | ||
let descriptionBonus = (schema.rank.description.score > 0.5) ? 0.1 : 0; | ||
let perfectDescriptionBonus = (schema.rank.title.score === 1) ? 0.1 : 0; | ||
let settingNameBonus = (schema.rank.settingName.score > 0.8) ? 0.2 : 0; | ||
let perfectSettingNameBonus = (schema.rank.title.score === 1) ? 0.1 : 0; | ||
let settingItemBonus = (schema.rank.settingItem.score > 0.8) ? 0.2 : 0; | ||
let perfectSettingItemBonus = (schema.rank.settingItem.score === 1) ? 0.1 : 0; | ||
let totalScore = | ||
schema.rank.title.score + titleBonus + perfectTitleBonus | ||
+ schema.rank.description.score + descriptionBonus + perfectDescriptionBonus | ||
+ schema.rank.settingName.score + settingNameBonus + perfectSettingNameBonus | ||
+ schema.rank.settingItem.score + settingItemBonus + perfectSettingItemBonus; | ||
schema.rank.totalScore = totalScore; | ||
rankedResults.push(schema); | ||
} | ||
} | ||
} | ||
|
||
this.processRanks(rankedResults) | ||
} | ||
|
||
processRanks (ranks) { | ||
// Gets an array of schemas with ranks included | ||
|
||
// Removes any scores below a specific limit | ||
let filteredRanks = ranks.filter(item => item.rank.totalScore > atom.config.get("settings-view.searchSettingsMinimumScore")); | ||
|
||
// Sorts the array from highest score to lowest score | ||
filteredRanks.sort((a, b) => { | ||
if (a.rank.totalScore < b.rank.totalScore) { | ||
return 1; | ||
} | ||
if (a.rank.totalScore > b.rank.totalScore) { | ||
return -1; | ||
} | ||
return 0; | ||
}); | ||
|
||
// Remove our loading symbol | ||
this.refs.loadingArea.style.display = 'none' | ||
|
||
for (const setting of filteredRanks) { | ||
let searchView = new SearchSettingView(setting, this.settingsView) | ||
this.refs.searchResults.appendChild(searchView.element) | ||
this.searchResults.push(searchView) | ||
} | ||
|
||
} | ||
|
||
getScore (s1, s2) { | ||
// s1 is the text we are calculating the score against | ||
// s2 is the text the user typed | ||
// Below is an exact implmentation of Longest Common Subsequence | ||
|
||
let height = s1.length + 1; | ||
let width = s2.length + 1; | ||
let matrix = Array(height) | ||
.fill(0) | ||
.map(() => Array(width).fill(0)); | ||
|
||
for (let row = 1; row < height; row++) { | ||
for (let col = 1; col < width; col++) { | ||
if (s1[row - 1] == s2[col - 1]) { | ||
matrix[row][col] = matrix[row - 1][col - 1] + 1; | ||
} else { | ||
matrix[row][col] = Math.max(matrix[row][col - 1], matrix[row - 1][col]); | ||
} | ||
} | ||
} | ||
|
||
let longest = this.lcsTraceback(matrix, s1, s2, height, width); | ||
// Now longest is a literal string of the longest common subsequence. | ||
// We will now assign a score to help ranking, but will still return the | ||
// text sequence, in case we want to use that for display purposes | ||
return { | ||
score: longest.length / s1.length, | ||
sequence: longest | ||
}; | ||
} | ||
|
||
lcsTraceback (matrix, s1, s2, height, width) { | ||
if (height === 0 || width === 0) { | ||
return ""; | ||
} | ||
if (s1[height - 1] == s2[width - 1]) { | ||
return ( | ||
this.lcsTraceback(matrix, s1, s2, height - 1, width - 1) + | ||
(s1[height - 1] ? s1[height - 1] : "") | ||
); | ||
} | ||
if (matrix[height][width - 1] > matrix[height - 1][width]) { | ||
return this.lcsTraceback(matrix, s1, s2, height, width - 1); | ||
} | ||
return this.lcsTraceback(matrix, s1, s2, height - 1, width); | ||
} | ||
|
||
// Boiler Plate Functions | ||
scrollUp () { | ||
this.element.scrollTop -= document.body.offsetHeight / 20 | ||
} | ||
|
||
scrollDown () { | ||
this.element.scrollTop += document.body.offsetHeight / 20 | ||
} | ||
|
||
pageUp () { | ||
this.element.scrollTop -= this.element.offsetHeight | ||
} | ||
|
||
pageDown () { | ||
this.element.scrollTop += this.element.offsetHeight | ||
} | ||
|
||
scrollToTop () { | ||
this.element.scrollTop = 0 | ||
} | ||
|
||
scrollToBottom () { | ||
this.element.scrollTop = this.element.scrollHeight | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a difference between builtin packages and installed packages that makes installed packages not work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Surprisingly the URI for the settings page of all packages is exactly the same. Made sure this functioned for either the core packages or community packages.
Only thing not explicitly tested for is Git installed packages, but I'd imagine they should work as well, especially since the
settings-view
showPackage()
function is written identically.But of course anyone with Git installed packages on their system could double check by navigating to this URI format in the browser and letting it open up Pulsar
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been using the package-settings package for ages in order to jump quickly to a given package's settings, and this is the exact method that package uses. Never observed it not to work, no matter how a package is installed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I got confused, that's relieving. I think for clarity, the packages caveat means:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh I see how that can be confusing. Yeah just means it's not the search that's available on the "Packages" pane, as in it won't accomplish the same thing.
(Although technically you could search by package name to help bring your package up, but it will only show results for packages that have settings. As the name of the package is considered when doing a search. But this makes the whole thing a little confusing, so though it was easier to say it doesn't search for packages)