Merge branch 'fb-pw-simple-project-config' of github.com:atom/atom into fb-pw-simple-project-config

This commit is contained in:
Philip Weiss
2018-03-01 13:28:25 -08:00
39 changed files with 1493 additions and 600 deletions

View File

@@ -70,6 +70,11 @@ repeat these steps to upgrade to future releases.
* [macOS](https://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/#platform-mac)
* [Windows](https://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/#platform-windows)
## Discussion
* Discuss Atom on our [forums](https://discuss.atom.io/)
* Chat about Atom on our Slack team -- [instructions for joining](https://discuss.atom.io/t/join-us-on-slack/16638?source_topic_id=25406)
## License
[MIT](https://github.com/atom/atom/blob/master/LICENSE.md)

55
docs/focus/2018-02-12.md Normal file
View File

@@ -0,0 +1,55 @@
## Highlights from the past week
- Atom IDE
- Started conversion of atom-languageclient to TypeScript [atom/atom-languageclient#175](https://github.com/atom/atom-languageclient/pull/175)
- @atom/watcher
- Report events related to [symlinks](https://github.com/atom/watcher/pull/111) and [test for symlink-related edge cases.](https://github.com/atom/watcher/pull/114)
- Produce filesystem events with a [consistent parent path](https://github.com/atom/watcher/pull/113) to the one used to create a watcher, even if the watcher was created with a a path containing symlinks.
- Verified correct behavior with regard to [filesystem case sensitivity.](https://github.com/atom/watcher/pull/116)
- Corrected buggy [utf8 to utf16 conversion](https://github.com/atom/watcher/pull/115) on Windows.
- Ran through the MacOS cases in the [testing matrix.](https://github.com/atom/atom/pull/16124)
- Set up a Samba share on @ungb's testing server to exercise Samba network drives.
- Published version 1.0.0 on [npm.](https://www.npmjs.com/package/@atom/watcher)
- GitHub Package
- Introduce a package configuration option to [disable the in-editor merge conflict resolution.](https://github.com/atom/github/pull/1305)
- Published a new release v0.10.0
- Investigated and spiked on a fix for amending bug in single-commit repos, which was surfaced by failing cache invalidation tests that were blocking release
- Deferred fixing underlying bug - [atom/github#1303](https://github.com/atom/github/issues/1303)
- Fixed failing tests - [atom/github#1302](https://github.com/atom/github/pull/1302)
- Teletype
- Released [Teletype 0.7.0](https://github.com/atom/teletype/releases/tag/v0.7.0) with improved diagnostics for errors that occur during package initialization ([atom/teletype#266](https://github.com/atom/teletype/issues/266), [atom/teletype#297](https://github.com/atom/teletype/issues/297))
- Opened [atom/teletype#323](https://github.com/atom/teletype/pull/323), [atom/teletype-client#52](https://github.com/atom/teletype-client/pull/52), and [atom/fuzzy-finder#335](https://github.com/atom/fuzzy-finder/pull/335) to pave the way for guests to use the fuzzy-finder to open any remote editor shared by the host ([atom/teletype#268](https://github.com/atom/teletype/issues/268))
## Focus for week ahead
- Atom IDE
- Finish conversion of atom-languageclient to TypeScript [atom/atom-languageclient#175](https://github.com/atom/atom-languageclient/pull/175)
- Contribute TypeScript type definitions for Atom IDE to [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped)
- Contribute missing TypeScript type defintions for Atom to [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/atom)
- @atom/watcher
- Complete [the testing matrix](https://github.com/atom/atom/pull/16124) on Linux and Windows.
- :shipit: Merge [@atom/watcher support]((https://github.com/atom/atom/pull/16124)) into Atom _(as a non-default `PathWatcher` backend)_. :shipit:
- GitHub Package
- Quarterly planning. Which might change all of these :wink:
- Finish tracking down our [freezing CI builds.](https://github.com/atom/github/pull/1289)
- Resurrect the [gargantuan credential helper and GPG pinentry refactoring PR](https://github.com/atom/github/pull/846) and see how much work is needed to get it over the finish line.
- Fix issue with diff view popping up unexpectedly - [atom/github#1287](https://github.com/atom/github/issues/1287)
- Teletype
- Complete initial implementation and merge pull requests ([atom/teletype#323](https://github.com/atom/teletype/pull/323), [atom/teletype-client#52](https://github.com/atom/teletype-client/pull/52), and [atom/fuzzy-finder#335](https://github.com/atom/fuzzy-finder/pull/335)) allowing guests to use the fuzzy-finder to open any remote editor shared by the host ([atom/teletype#268](https://github.com/atom/teletype/issues/268))
- Use fuzzy-finder support internally in our day-to-day workflows to assess usability
- Tree-sitter
- Finish and merge [tree-sitter/tree-sitter#128](https://github.com/tree-sitter/tree-sitter/pull/128), which fixes a fundamental performance problem when editing large files.
- Fix syntax highlighting bugs [#16643](https://github.com/atom/atom/issues/16643) and [#16642](https://github.com/atom/atom/issues/16642).
- Fix [#16621](https://github.com/atom/atom/issues/16621) - snippets not working when using Tree-sitter.
- Xray
* @nathansobo (and @as-cii part time) will be focusing the next 12 weeks on a prototype for [a new Electron-based text editor](https://github.com/atom/xray). The goal is to explore the viability of radical performance improvements that could be possible if we make breaking changes to Atom's APIs. At the end of the 12 weeks, we will reassess our plans based on what we have managed to learn and accomplish.
* Week 1 of 12
* Clarify and document goals for the next 12 weeks.
* Ensure that the guide matches our current plans.
* Refine WebGL based text rendering.
* Make sure ASCII text renders correctly without being clipped
* Render text correctly on high DPI displays
* Use correct API for texture atlas updates
* Add mouse-wheel scrolling support
* Non-ASCII rendering, using the HarfBuzz text shaping library to detect combining characters
* Stretch goal: Switch document encoding to UTF-8 for memory compactness and support multi-byte-aware character indexing.

51
docs/focus/2018-02-19.md Normal file
View File

@@ -0,0 +1,51 @@
## Highlights from the past week
- Atom IDE
- Converted atom-languageclient to TypeScript
- ide-typescript updated to use TypeScript 2.7.2
- Published updates to ide-typescript, ide-json, and ide-csharp to improve language server stability
- @atom/watcher
- Gracefully handle the situation where a network share with a watch root is disconnected ([#119](https://github.com/atom/watcher/pull/119))
- Merged into Atom master behind a feature flag ([#16124](https://github.com/atom/atom/pull/16124)) just after the 1.24.0 / 1.25.0-beta0 release
- Fixed a crash when messages are sent to the worker thread before it's properly initialized ([atom/watcher#121](https://github.com/atom/watcher/pull/121))
- GitHub Package
- Investigate intermittently freezing tests on Travis in [atom/github#1289](https://github.com/atom/github/pull/1289). Not much luck so far
- Fixed issue with diff views popping up unexpectedly [atom/github#1311](https://github.com/atom/github/pull/1311). Just waiting on review
- Teletype
- Fixed an unanticipated bug that would cause non-existent selections to appear in the editor of other participants ([atom/teletype#326](https://github.com/atom/teletype/pull/326)).
- Published [version 0.8.0](https://github.com/atom/teletype/releases/tag/v0.8.0).
- Refactored teletype-client and simplified how added/removed editors are broadcasted to participants ([atom/teletype-client#52](https://github.com/atom/teletype-client/pull/52)).
- Polished the design of fuzzy-finder ([atom/fuzzy-finder#335](https://github.com/atom/fuzzy-finder/pull/335))
- Pushed [atom/teletype#323](https://github.com/atom/teletype/pull/323) over the finish line.
- Xray
- We made a slight change of plans and decided to spend more time clarifying the overall vision for the project.
- We have a [branch](https://github.com/atom/xray/tree/roadmap) with a new README that matches our current thinking, but the Q1 roadmap is still in progress.
- We did manage to get text rendering with retina displays and non-clipped characters, but there's still work to do. We are also experimenting populating our glyph atlas with up to 4 variants of each glyph at different subpixel positions to more closely match text rendered purely on the CPU.
- Tree-sitter
- Took some time to fix unrelated regressions from the bug-bash month
- Fixed a bug where atom --wait did not work correctly on Windows (#16740)
- Fixed a bug that prevented Atom from reusing an existing window when the same path was opened twice (#16764)
- Fixed regressions in the behavior of the atom.textEditors.getGrammarOverride and atom.grammars.loadGrammar methods (#16733, #16747)
- Fixed several syntax highlighting bugs (#16642, #16643)
## Focus for week ahead
- Atom IDE
- Investigate new Atom IDE UI features for rename operations and workspace symbol search
- Publish TypeScript definitions for atom-ide/atom-languageclient to DefinitelyTyped
- Wire up atom-ide-ui console to LSP server logging
- @atom/watcher
- Diagnose and correct crashes and lock-ups as people report them
- GitHub Package
- Establish high-level goals and scope bounds for the GitHub side of the integration
- Document a protocol for the evolution of major features: ensure they contribute to a cohesive experience with the rest of the package, make sure that @simurai is looped in to the conversation, make sure the community has visibility to our goals
- Show recent commits in Git panel
- Teletype
- Merge and use [atom/fuzzy-finder#335](https://github.com/atom/fuzzy-finder/pull/335), [atom/teletype-client#52](https://github.com/atom/teletype-client/pull/52) and [atom/teletype#323](https://github.com/atom/teletype/pull/323).
- Publish Teletype v0.9.0 containing the new fuzzy-finder support.
- Tree-sitter
- Fix an issue where snippets are not available when using tree-sitter (#16621)
- Start work on optimizing editing in the presence of large parse errors (#16590)
- Start work on allowing parsing to take place on a background thread
- Xray
- We will continue clarifying the overall vision with a focus on real time collaboration. This may extend beyond the scope of Xray, but is important to get clarity on before comitting to a roadmap.
- We hope to iron out the remaining issues with subpixel-positioning of glyphs to more faithfully reproduce Chrome's behavior when rendering text via the normal DOM-based code path.

View File

@@ -1,7 +1,7 @@
{
"name": "atom",
"productName": "Atom",
"version": "1.25.0-dev",
"version": "1.26.0-dev",
"description": "A hackable text editor for the 21st Century.",
"main": "./src/main-process/main.js",
"repository": {
@@ -15,9 +15,10 @@
"electronVersion": "1.7.11",
"dependencies": {
"@atom/nsfw": "^1.0.18",
"@atom/watcher": "1.0.3",
"@atom/source-map-support": "^0.3.4",
"async": "0.2.6",
"atom-keymap": "8.2.9",
"atom-keymap": "8.2.10",
"atom-select-list": "^0.7.0",
"atom-ui": "0.4.1",
"babel-core": "5.8.38",
@@ -38,7 +39,7 @@
"fs-plus": "^3.0.1",
"fstream": "0.1.24",
"fuzzaldrin": "^2.1",
"git-utils": "5.2.1",
"git-utils": "5.3.1",
"glob": "^7.1.1",
"grim": "1.5.0",
"jasmine-json": "~0.0",
@@ -71,7 +72,7 @@
"sinon": "1.17.4",
"temp": "^0.8.3",
"text-buffer": "13.11.8",
"tree-sitter": "^0.8.6",
"tree-sitter": "^0.9.2",
"typescript-simple": "1.0.0",
"underscore-plus": "^1.6.6",
"winreg": "^1.2.1",
@@ -79,41 +80,41 @@
},
"packageDependencies": {
"atom-dark-syntax": "0.29.0",
"atom-dark-ui": "0.53.1",
"atom-dark-ui": "0.53.2",
"atom-light-syntax": "0.29.0",
"atom-light-ui": "0.46.1",
"atom-light-ui": "0.46.2",
"base16-tomorrow-dark-theme": "1.5.0",
"base16-tomorrow-light-theme": "1.5.0",
"one-dark-ui": "1.10.10",
"one-light-ui": "1.10.10",
"one-dark-ui": "1.10.11",
"one-light-ui": "1.10.11",
"one-dark-syntax": "1.8.2",
"one-light-syntax": "1.8.2",
"solarized-dark-syntax": "1.1.4",
"solarized-light-syntax": "1.1.4",
"about": "1.8.0",
"archive-view": "0.64.2",
"archive-view": "0.64.3",
"autocomplete-atom-api": "0.10.7",
"autocomplete-css": "0.17.5",
"autocomplete-html": "0.8.4",
"autocomplete-plus": "2.40.2",
"autocomplete-plus": "2.40.4",
"autocomplete-snippets": "1.12.0",
"autoflow": "0.29.3",
"autosave": "0.24.6",
"background-tips": "0.27.1",
"background-tips": "0.28.0",
"bookmarks": "0.45.1",
"bracket-matcher": "0.89.1",
"command-palette": "0.43.0",
"command-palette": "0.43.4",
"dalek": "0.2.1",
"deprecation-cop": "0.56.9",
"dev-live-reload": "0.48.1",
"encoding-selector": "0.23.8",
"exception-reporting": "0.42.0",
"exception-reporting": "0.43.1",
"find-and-replace": "0.215.5",
"fuzzy-finder": "1.7.5",
"github": "0.9.1",
"fuzzy-finder": "1.7.6",
"github": "0.10.1",
"git-diff": "1.3.9",
"go-to-line": "0.33.0",
"grammar-selector": "0.49.9",
"grammar-selector": "0.50.0",
"image-view": "0.62.4",
"incompatible-packages": "0.27.3",
"keybinding-resolver": "0.38.1",
@@ -124,8 +125,8 @@
"notifications": "0.70.2",
"open-on-github": "1.3.1",
"package-generator": "1.3.0",
"settings-view": "0.254.0",
"snippets": "1.3.0",
"settings-view": "0.254.1",
"snippets": "1.3.1",
"spell-check": "0.72.7",
"status-bar": "1.8.15",
"styleguide": "0.49.10",
@@ -137,39 +138,39 @@
"welcome": "0.36.6",
"whitespace": "0.37.5",
"wrap-guide": "0.40.3",
"language-c": "0.59.1",
"language-c": "0.59.2",
"language-clojure": "0.22.7",
"language-coffee-script": "0.49.3",
"language-csharp": "0.14.4",
"language-csharp": "1.0.1",
"language-css": "0.42.10",
"language-gfm": "0.90.3",
"language-git": "0.19.1",
"language-go": "0.45.0",
"language-go": "0.45.2",
"language-html": "0.49.0",
"language-hyperlink": "0.16.3",
"language-java": "0.28.0",
"language-javascript": "0.128.1",
"language-javascript": "0.128.3",
"language-json": "0.19.1",
"language-less": "0.34.2",
"language-make": "0.22.3",
"language-mustache": "0.14.4",
"language-mustache": "0.14.5",
"language-objective-c": "0.15.1",
"language-perl": "0.38.1",
"language-php": "0.43.0",
"language-php": "0.43.1",
"language-property-list": "0.9.1",
"language-python": "0.48.0",
"language-python": "0.49.2",
"language-ruby": "0.71.4",
"language-ruby-on-rails": "0.25.3",
"language-sass": "0.61.4",
"language-shellscript": "0.26.0",
"language-shellscript": "0.26.1",
"language-source": "0.9.0",
"language-sql": "0.25.10",
"language-text": "0.7.3",
"language-todo": "0.29.4",
"language-toml": "0.18.2",
"language-typescript": "0.3.0",
"language-typescript": "0.3.2",
"language-xml": "0.35.2",
"language-yaml": "0.31.1"
"language-yaml": "0.31.2"
},
"private": true,
"scripts": {

View File

@@ -32,6 +32,8 @@ module.exports = function (packagedAppPath) {
relativePath.endsWith(path.join('node_modules', 'graceful-fs', 'graceful-fs.js')) ||
relativePath.endsWith(path.join('node_modules', 'htmlparser2', 'lib', 'index.js')) ||
relativePath.endsWith(path.join('node_modules', 'minimatch', 'minimatch.js')) ||
relativePath.endsWith(path.join('node_modules', 'request', 'index.js')) ||
relativePath.endsWith(path.join('node_modules', 'request', 'request.js')) ||
relativePath === path.join('..', 'exports', 'atom.js') ||
relativePath === path.join('..', 'src', 'electron-shims.js') ||
relativePath === path.join('..', 'src', 'safe-clipboard.js') ||
@@ -50,7 +52,6 @@ module.exports = function (packagedAppPath) {
relativePath === path.join('..', 'node_modules', 'node-fetch', 'lib', 'fetch-error.js') ||
relativePath === path.join('..', 'node_modules', 'superstring', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'oniguruma', 'src', 'oniguruma.js') ||
relativePath === path.join('..', 'node_modules', 'request', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'resolve', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'resolve', 'lib', 'core.js') ||
relativePath === path.join('..', 'node_modules', 'settings-view', 'node_modules', 'glob', 'glob.js') ||

View File

@@ -1,64 +1,63 @@
'use strict'
const csslint = require('csslint').CSSLint
const expandGlobPaths = require('./expand-glob-paths')
const LessCache = require('less-cache')
const stylelint = require('stylelint')
const path = require('path')
const readFiles = require('./read-files')
const CONFIG = require('../config')
const LESS_CACHE_VERSION = require('less-cache/package.json').version
module.exports = function () {
const globPathsToLint = [
path.join(CONFIG.repositoryRootPath, 'static/**/*.less')
]
const lintOptions = {
'adjoining-classes': false,
'duplicate-background-images': false,
'box-model': false,
'box-sizing': false,
'bulletproof-font-face': false,
'compatible-vendor-prefixes': false,
'display-property-grouping': false,
'duplicate-properties': false,
'fallback-colors': false,
'font-sizes': false,
'gradients': false,
'ids': false,
'important': false,
'known-properties': false,
'order-alphabetical': false,
'outline-none': false,
'overqualified-elements': false,
'regex-selectors': false,
'qualified-headings': false,
'unique-headings': false,
'universal-selector': false,
'vendor-prefix': false
}
for (let rule of csslint.getRules()) {
if (!lintOptions.hasOwnProperty(rule.id)) lintOptions[rule.id] = true
}
const lessCache = new LessCache({
cacheDir: path.join(CONFIG.intermediateAppPath, 'less-compile-cache'),
fallbackDir: path.join(CONFIG.atomHomeDirPath, 'compile-cache', 'prebuild-less', LESS_CACHE_VERSION),
syncCaches: true,
resourcePath: CONFIG.repositoryRootPath,
importPaths: [
path.join(CONFIG.intermediateAppPath, 'static', 'variables'),
path.join(CONFIG.intermediateAppPath, 'static')
]
})
return expandGlobPaths(globPathsToLint).then(readFiles).then((files) => {
const errors = []
for (let file of files) {
const css = lessCache.cssForFile(file.path, file.content)
const result = csslint.verify(css, lintOptions)
for (let message of result.messages) {
errors.push({path: file.path.replace(/\.less$/, '.css'), lineNumber: message.line, message: message.message, rule: message.rule.id})
return stylelint
.lint({
files: path.join(CONFIG.repositoryRootPath, 'static/**/*.less'),
configBasedir: __dirname,
configFile: path.resolve(__dirname, '..', '..', 'stylelint.config.js')
})
.then(({results}) => {
const errors = []
for (const result of results) {
for (const deprecation of result.deprecations) {
console.log('stylelint encountered deprecation:', deprecation.text)
if (deprecation.reference != null) {
console.log('more information at', deprecation.reference)
}
}
for (const invalidOptionWarning of result.invalidOptionWarnings) {
console.warn(
'stylelint encountered invalid option:',
invalidOptionWarning.text
)
}
if (result.errored) {
for (const warning of result.warnings) {
if (warning.severity === 'error') {
errors.push({
path: result.source,
lineNumber: warning.line,
message: warning.text,
rule: warning.rule
})
} else {
console.warn(
'stylelint encountered non-critical warning in file',
result.source,
'at line',
warning.line,
'for rule',
warning.rule + ':',
warning.text
)
}
}
}
}
}
return errors
})
return errors
})
.catch(err => {
console.error('There was a problem linting LESS:')
throw err
})
}

View File

@@ -6,7 +6,6 @@
"babel-core": "5.8.38",
"coffeelint": "1.15.7",
"colors": "1.1.2",
"csslint": "1.0.2",
"donna": "1.0.16",
"electron-chromedriver": "~1.7",
"electron-link": "0.1.2",
@@ -31,8 +30,10 @@
"season": "5.3.0",
"semver": "5.3.0",
"standard": "8.4.0",
"stylelint": "^9.0.0",
"stylelint-config-standard": "^18.1.0",
"sync-request": "3.0.1",
"tello": "1.0.5",
"tello": "1.0.7",
"webdriverio": "2.4.5",
"yargs": "4.8.1"
}

View File

@@ -111,7 +111,7 @@ describe('Config', () => {
describe('when the first component of the scope descriptor matches a legacy scope alias', () =>
it('falls back to properties defined for the legacy scope if no value is found for the original scope descriptor', () => {
atom.config.addLegacyScopeAlias('javascript', '.source.js')
atom.config.setLegacyScopeAliasForNewScope('javascript', '.source.js')
atom.config.set('foo', 100, {scopeSelector: '.source.js'})
atom.config.set('foo', 200, {scopeSelector: 'javascript for_statement'})
@@ -150,7 +150,7 @@ describe('Config', () => {
describe('when the first component of the scope descriptor matches a legacy scope alias', () =>
it('includes the values defined for the legacy scope', () => {
atom.config.addLegacyScopeAlias('javascript', '.source.js')
atom.config.setLegacyScopeAliasForNewScope('javascript', '.source.js')
expect(atom.config.set('foo', 41)).toBe(true)
expect(atom.config.set('foo', 42, {scopeSelector: 'javascript'})).toBe(true)

View File

@@ -333,3 +333,46 @@ describe "ContextMenuManager", ->
}
]
])
describe "::templateForEvent(target) (sorting)", ->
it "applies simple sorting rules", ->
contextMenu.add('.parent': [{
label: 'My Command',
command: "test:my-command",
after: ["test:my-other-command"]
}, {
label: 'My Other Command',
command: "test:my-other-command",
}])
dispatchedEvent = {target: parent}
expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual([{
label: 'My Other Command',
command: 'test:my-other-command',
}, {
label: 'My Command',
command: 'test:my-command',
after: ["test:my-other-command"]
}])
it "applies sorting rules recursively to submenus", ->
contextMenu.add('.parent': [{
submenu: [{
label: 'My Command',
command: "test:my-command",
after: ["test:my-other-command"]
}, {
label: 'My Other Command',
command: "test:my-other-command",
}]
}])
dispatchedEvent = {target: parent}
expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual([{
submenu: [{
label: 'My Other Command',
command: 'test:my-other-command',
}, {
label: 'My Command',
command: 'test:my-command',
after: ["test:my-other-command"]
}]
}])

View File

@@ -24,6 +24,7 @@ describe('GrammarRegistry', () => {
const buffer = new TextBuffer()
expect(grammarRegistry.assignLanguageMode(buffer, 'source.js')).toBe(true)
expect(buffer.getLanguageMode().getLanguageId()).toBe('source.js')
expect(grammarRegistry.getAssignedLanguageId(buffer)).toBe('source.js')
// Returns true if we found the grammar, even if it didn't change
expect(grammarRegistry.assignLanguageMode(buffer, 'source.js')).toBe(true)
@@ -47,6 +48,7 @@ describe('GrammarRegistry', () => {
expect(grammarRegistry.assignLanguageMode(buffer, null)).toBe(true)
expect(buffer.getLanguageMode().getLanguageId()).toBe('text.plain.null-grammar')
expect(grammarRegistry.getAssignedLanguageId(buffer)).toBe(null)
})
})
})

View File

@@ -0,0 +1,243 @@
const {sortMenuItems} = require('../src/menu-sort-helpers')
describe('contextMenu', () => {
describe('dedupes separators', () => {
it('preserves existing submenus', () => {
const items = [{ submenu: [] }]
expect(sortMenuItems(items)).toEqual(items)
})
})
describe('dedupes separators', () => {
it('trims leading separators', () => {
const items = [{ type: 'separator' }, { command: 'core:one' }]
const expected = [{ command: 'core:one' }]
expect(sortMenuItems(items)).toEqual(expected)
})
it('preserves separators at the begining of set two', () => {
const items = [
{ command: 'core:one' },
{ type: 'separator' }, { command: 'core:two' }
]
const expected = [
{ command: 'core:one' },
{ type: 'separator' },
{ command: 'core:two' }
]
expect(sortMenuItems(items)).toEqual(expected)
})
it('trims trailing separators', () => {
const items = [{ command: 'core:one' }, { type: 'separator' }]
const expected = [{ command: 'core:one' }]
expect(sortMenuItems(items)).toEqual(expected)
})
it('removes duplicate separators across sets', () => {
const items = [
{ command: 'core:one' }, { type: 'separator' },
{ type: 'separator' }, { command: 'core:two' }
]
const expected = [
{ command: 'core:one' },
{ type: 'separator' },
{ command: 'core:two' }
]
expect(sortMenuItems(items)).toEqual(expected)
})
})
describe('can move an item to a different group by merging groups', () => {
it('can move a group of one item', () => {
const items = [
{ command: 'core:one' },
{ type: 'separator' },
{ command: 'core:two' },
{ type: 'separator' },
{ command: 'core:three', after: ['core:one'] },
{ type: 'separator' }
]
const expected = [
{ command: 'core:one' },
{ command: 'core:three', after: ['core:one'] },
{ type: 'separator' },
{ command: 'core:two' }
]
expect(sortMenuItems(items)).toEqual(expected)
})
it("moves all items in the moving item's group", () => {
const items = [
{ command: 'core:one' },
{ type: 'separator' },
{ command: 'core:two' },
{ type: 'separator' },
{ command: 'core:three', after: ['core:one'] },
{ command: 'core:four' },
{ type: 'separator' }
]
const expected = [
{ command: 'core:one' },
{ command: 'core:three', after: ['core:one'] },
{ command: 'core:four' },
{ type: 'separator' },
{ command: 'core:two' }
]
expect(sortMenuItems(items)).toEqual(expected)
})
it("ignores positions relative to commands that don't exist", () => {
const items = [
{ command: 'core:one' },
{ type: 'separator' },
{ command: 'core:two' },
{ type: 'separator' },
{ command: 'core:three', after: ['core:does-not-exist'] },
{ command: 'core:four', after: ['core:one'] },
{ type: 'separator' }
]
const expected = [
{ command: 'core:one' },
{ command: 'core:three', after: ['core:does-not-exist'] },
{ command: 'core:four', after: ['core:one'] },
{ type: 'separator' },
{ command: 'core:two' }
]
expect(sortMenuItems(items)).toEqual(expected)
})
it('can handle recursive group merging', () => {
const items = [
{ command: 'core:one', after: ['core:three'] },
{ command: 'core:two', before: ['core:one'] },
{ command: 'core:three' }
]
const expected = [
{ command: 'core:three' },
{ command: 'core:two', before: ['core:one'] },
{ command: 'core:one', after: ['core:three'] }
]
expect(sortMenuItems(items)).toEqual(expected)
})
it('can merge multiple groups when given a list of before/after commands', () => {
const items = [
{ command: 'core:one' },
{ type: 'separator' },
{ command: 'core:two' },
{ type: 'separator' },
{ command: 'core:three', after: ['core:one', 'core:two'] }
]
const expected = [
{ command: 'core:two' },
{ command: 'core:one' },
{ command: 'core:three', after: ['core:one', 'core:two'] }
]
expect(sortMenuItems(items)).toEqual(expected)
})
it('can merge multiple groups based on both before/after commands', () => {
const items = [
{ command: 'core:one' },
{ type: 'separator' },
{ command: 'core:two' },
{ type: 'separator' },
{ command: 'core:three', after: ['core:one'], before: ['core:two'] }
]
const expected = [
{ command: 'core:one' },
{ command: 'core:three', after: ['core:one'], before: ['core:two'] },
{ command: 'core:two' }
]
expect(sortMenuItems(items)).toEqual(expected)
})
})
describe('sorts items within their ultimate group', () => {
it('does a simple sort', () => {
const items = [
{ command: 'core:two', after: ['core:one'] },
{ command: 'core:one' }
]
expect(sortMenuItems(items)).toEqual([
{ command: 'core:one' },
{ command: 'core:two', after: ['core:one'] }
])
})
it('resolves cycles by ignoring things that conflict', () => {
const items = [
{ command: 'core:two', after: ['core:one'] },
{ command: 'core:one', after: ['core:two'] }
]
expect(sortMenuItems(items)).toEqual([
{ command: 'core:one', after: ['core:two'] },
{ command: 'core:two', after: ['core:one'] }
])
})
})
describe('sorts groups', () => {
it('does a simple sort', () => {
const items = [
{ command: 'core:two', afterGroupContaining: ['core:one'] },
{ type: 'separator' },
{ command: 'core:one' }
]
expect(sortMenuItems(items)).toEqual([
{ command: 'core:one' },
{ type: 'separator' },
{ command: 'core:two', afterGroupContaining: ['core:one'] }
])
})
it('resolves cycles by ignoring things that conflict', () => {
const items = [
{ command: 'core:two', afterGroupContaining: ['core:one'] },
{ type: 'separator' },
{ command: 'core:one', afterGroupContaining: ['core:two'] }
]
expect(sortMenuItems(items)).toEqual([
{ command: 'core:one', afterGroupContaining: ['core:two'] },
{ type: 'separator' },
{ command: 'core:two', afterGroupContaining: ['core:one'] }
])
})
it('ignores references to commands that do not exist', () => {
const items = [
{ command: 'core:one' },
{ type: 'separator' },
{
command: 'core:two',
afterGroupContaining: ['core:does-not-exist']
}
]
expect(sortMenuItems(items)).toEqual([
{ command: 'core:one' },
{ type: 'separator' },
{ command: 'core:two', afterGroupContaining: ['core:does-not-exist'] }
])
})
it('only respects the first matching [before|after]GroupContaining rule in a given group', () => {
const items = [
{ command: 'core:one' },
{ type: 'separator' },
{ command: 'core:three', beforeGroupContaining: ['core:one'] },
{ command: 'core:four', afterGroupContaining: ['core:two'] },
{ type: 'separator' },
{ command: 'core:two' }
]
expect(sortMenuItems(items)).toEqual([
{ command: 'core:three', beforeGroupContaining: ['core:one'] },
{ command: 'core:four', afterGroupContaining: ['core:two'] },
{ type: 'separator' },
{ command: 'core:one' },
{ type: 'separator' },
{ command: 'core:two' }
])
})
})
})

View File

@@ -905,6 +905,46 @@ describe('TextEditorComponent', () => {
expect(component.getLineNumberGutterWidth()).toBe(originalLineNumberGutterWidth)
})
it('gracefully handles edits that change the maxScrollTop by causing the horizontal scrollbar to disappear', async () => {
const rowsPerTile = 1
const {component, element, editor} = buildComponent({rowsPerTile, autoHeight: false})
await setEditorHeightInLines(component, 1)
await setEditorWidthInCharacters(component, 7)
// Updating scrollbar styles.
const style = document.createElement('style')
style.textContent = '::-webkit-scrollbar { height: 17px; width: 10px; }'
jasmine.attachToDOM(style)
TextEditor.didUpdateScrollbarStyles()
await component.getNextUpdatePromise()
element.focus()
component.setScrollTop(component.measurements.lineHeight)
component.scheduleUpdate()
await component.getNextUpdatePromise()
editor.setSelectedBufferRange([[0, 1], [12, 2]])
editor.backspace()
// component.scheduleUpdate()
await component.getNextUpdatePromise()
expect(component.getScrollTop()).toBe(0)
const renderedLines = queryOnScreenLineElements(element).sort((a, b) => a.dataset.screenRow - b.dataset.screenRow)
const renderedLineNumbers = queryOnScreenLineNumberElements(element).sort((a, b) => a.dataset.screenRow - b.dataset.screenRow)
const renderedStartRow = component.getRenderedStartRow()
const expectedLines = editor.displayLayer.getScreenLines(renderedStartRow, component.getRenderedEndRow())
expect(renderedLines.length).toBe(expectedLines.length)
expect(renderedLineNumbers.length).toBe(expectedLines.length)
element.remove()
editor.destroy()
})
describe('randomized tests', () => {
let originalTimeout
@@ -921,7 +961,7 @@ describe('TextEditorComponent', () => {
const initialSeed = Date.now()
for (var i = 0; i < 20; i++) {
let seed = initialSeed + i
// seed = 1507224195357
// seed = 1507231571985
const failureMessage = 'Randomized test failed with seed: ' + seed
const random = Random(seed)
@@ -930,6 +970,7 @@ describe('TextEditorComponent', () => {
editor.setSoftWrapped(Boolean(random(2)))
await setEditorWidthInCharacters(component, random(20))
await setEditorHeightInLines(component, random(10))
element.focus()
for (var j = 0; j < 5; j++) {
@@ -1368,40 +1409,6 @@ describe('TextEditorComponent', () => {
}
})
it('always scrolls by a minimum of 1, even when the delta is small or the scroll sensitivity is low', () => {
const scrollSensitivity = 10
const {component, editor} = buildComponent({height: 50, width: 50, scrollSensitivity})
{
component.didMouseWheel({wheelDeltaX: 0, wheelDeltaY: -3})
expect(component.getScrollTop()).toBe(1)
expect(component.getScrollLeft()).toBe(0)
expect(component.refs.content.style.transform).toBe(`translate(0px, -1px)`)
}
{
component.didMouseWheel({wheelDeltaX: -4, wheelDeltaY: 0})
expect(component.getScrollTop()).toBe(1)
expect(component.getScrollLeft()).toBe(1)
expect(component.refs.content.style.transform).toBe(`translate(-1px, -1px)`)
}
editor.update({scrollSensitivity: 100})
{
component.didMouseWheel({wheelDeltaX: 0, wheelDeltaY: 0.3})
expect(component.getScrollTop()).toBe(0)
expect(component.getScrollLeft()).toBe(1)
expect(component.refs.content.style.transform).toBe(`translate(-1px, 0px)`)
}
{
component.didMouseWheel({wheelDeltaX: 0.1, wheelDeltaY: 0})
expect(component.getScrollTop()).toBe(0)
expect(component.getScrollLeft()).toBe(0)
expect(component.refs.content.style.transform).toBe(`translate(0px, 0px)`)
}
})
it('inverts deltaX and deltaY when holding shift on Windows and Linux', async () => {
const scrollSensitivity = 50
const {component, editor} = buildComponent({height: 50, width: 50, scrollSensitivity})

View File

@@ -6747,6 +6747,14 @@ describe('TextEditor', () => {
editor.destroy()
})
describe('.scopeDescriptorForBufferPosition(position)', () => {
it('returns a default scope descriptor when no language mode is assigned', () => {
editor = new TextEditor({buffer: new TextBuffer()})
const scopeDescriptor = editor.scopeDescriptorForBufferPosition([0, 0])
expect(scopeDescriptor.getScopesArray()).toEqual(['text'])
})
})
describe('.shouldPromptToSave()', () => {
beforeEach(async () => {
editor = await atom.workspace.open('sample.js')

View File

@@ -170,6 +170,49 @@ describe('TreeSitterLanguageMode', () => {
[{text: ')', scopes: []}]
])
})
it('handles edits after tokens that end between CR and LF characters (regression)', () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
scopes: {
'comment': 'comment',
'string': 'string',
'property_identifier': 'property',
}
})
buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText([
'// abc',
'',
'a("b").c'
].join('\r\n'))
expectTokensToEqual(editor, [
[{text: '// abc', scopes: ['comment']}],
[{text: '', scopes: []}],
[
{text: 'a(', scopes: []},
{text: '"b"', scopes: ['string']},
{text: ').', scopes: []},
{text: 'c', scopes: ['property']}
]
])
buffer.insert([2, 0], ' ')
expectTokensToEqual(editor, [
[{text: '// abc', scopes: ['comment']}],
[{text: '', scopes: []}],
[
{text: ' ', scopes: ['whitespace']},
{text: 'a(', scopes: []},
{text: '"b"', scopes: ['string']},
{text: ').', scopes: []},
{text: 'c', scopes: ['property']}
]
])
})
})
describe('folding', () => {
@@ -499,7 +542,7 @@ describe('TreeSitterLanguageMode', () => {
buffer.setText('foo({bar: baz});')
editor.screenLineForScreenRow(0)
expect(editor.scopeDescriptorForBufferPosition({row: 0, column: 6}).getScopesArray()).toEqual([
expect(editor.scopeDescriptorForBufferPosition([0, 6]).getScopesArray()).toEqual([
'javascript',
'program',
'expression_statement',

View File

@@ -182,7 +182,7 @@ class ApplicationDelegate {
async setUserSettings (config) {
this.pendingSettingsUpdateCount++
try {
await ipcHelpers.call('set-user-settings', config)
await ipcHelpers.call('set-user-settings', JSON.stringify(config))
} finally {
this.pendingSettingsUpdateCount--
}
@@ -236,7 +236,7 @@ class ApplicationDelegate {
return chosen
} else {
const callback = buttons[buttonLabels[chosen]]
if (typeof callback === 'function') callback()
if (typeof callback === 'function') return callback()
}
}
}
@@ -249,7 +249,7 @@ class ApplicationDelegate {
this.getCurrentWindow().showSaveDialog(options, callback)
} else {
// Sync
if (typeof params === 'string') {
if (typeof options === 'string') {
options = {defaultPath: options}
}
return this.getCurrentWindow().showSaveDialog(options)

View File

@@ -337,6 +337,14 @@ const configSchema = {
value: 'native',
description: 'Native operating system APIs'
},
{
value: 'experimental',
description: 'Experimental filesystem watching library'
},
{
value: 'poll',
description: 'Polling'
},
{
value: 'atom',
description: 'Emulated with Atom events'
@@ -372,7 +380,7 @@ const configSchema = {
// These can be used as globals or scoped, thus defaults.
fontFamily: {
type: 'string',
default: '',
default: 'Menlo, Consolas, DejaVu Sans Mono, monospace',
description: 'The name of the font family used for editor text.'
},
fontSize: {

View File

@@ -432,7 +432,7 @@ class Config {
this.settingsLoaded = false
this.transactDepth = 0
this.pendingOperations = []
this.legacyScopeAliases = {}
this.legacyScopeAliases = new Map()
this.requestSave = _.debounce(() => this.save(), 1)
}
@@ -662,7 +662,7 @@ class Config {
keyPath,
priority.options
)
legacyScopeDescriptor = this.getLegacyScopeDescriptor(scopeDescriptor)
legacyScopeDescriptor = this.getLegacyScopeDescriptorForNewScopeDescriptor(scopeDescriptor)
if (legacyScopeDescriptor) {
result.push(...Array.from(this.scopedSettingsStore.getAll(
legacyScopeDescriptor.getScopeChain(),
@@ -872,12 +872,22 @@ class Config {
}
}
addLegacyScopeAlias (languageId, legacyScopeName) {
this.legacyScopeAliases[languageId] = legacyScopeName
getLegacyScopeDescriptorForNewScopeDescriptor (scopeDescriptor) {
scopeDescriptor = ScopeDescriptor.fromObject(scopeDescriptor)
const legacyAlias = this.legacyScopeAliases.get(scopeDescriptor.scopes[0])
if (legacyAlias) {
const scopes = scopeDescriptor.scopes.slice()
scopes[0] = legacyAlias
return new ScopeDescriptor({scopes})
}
}
removeLegacyScopeAlias (languageId) {
delete this.legacyScopeAliases[languageId]
setLegacyScopeAliasForNewScope (languageId, legacyScopeName) {
this.legacyScopeAliases.set(languageId, legacyScopeName)
}
removeLegacyScopeAliasForNewScope (languageId) {
this.legacyScopeAliases.delete(languageId)
}
/*
@@ -1269,7 +1279,7 @@ class Config {
options
)
const legacyScopeDescriptor = this.getLegacyScopeDescriptor(scopeDescriptor)
const legacyScopeDescriptor = this.getLegacyScopeDescriptorForNewScopeDescriptor(scopeDescriptor)
if (result != null) {
return result
} else if (legacyScopeDescriptor) {
@@ -1297,15 +1307,6 @@ class Config {
}
})
}
getLegacyScopeDescriptor (scopeDescriptor) {
const legacyAlias = this.legacyScopeAliases[scopeDescriptor.scopes[0]]
if (legacyAlias) {
const scopes = scopeDescriptor.scopes.slice()
scopes[0] = legacyAlias
return new ScopeDescriptor({scopes})
}
}
};
// Base schema enforcers. These will coerce raw input into the specified type,

View File

@@ -5,6 +5,7 @@ fs = require 'fs-plus'
{Disposable} = require 'event-kit'
{remote} = require 'electron'
MenuHelpers = require './menu-helpers'
{sortMenuItems} = require './menu-sort-helpers'
platformContextMenu = require('../package.json')?._atomMenu?['context-menu']
@@ -149,7 +150,7 @@ class ContextMenuManager
@pruneRedundantSeparators(template)
@addAccelerators(template)
template
return @sortTemplate(template)
# Adds an `accelerator` property to items that have key bindings. Electron
# uses this property to surface the relevant keymaps in the context menu.
@@ -175,6 +176,13 @@ class ContextMenuManager
keepNextItemIfSeparator = true
index++
sortTemplate: (template) ->
template = sortMenuItems(template)
for id, item of template
if Array.isArray(item.submenu)
item.submenu = @sortTemplate(item.submenu)
return template
# Returns an object compatible with `::add()` or `null`.
cloneItemForEvent: (item, event) ->
return null if item.devMode and not @devMode

View File

@@ -135,6 +135,14 @@ class GrammarRegistry {
return true
}
// Extended: Get the `languageId` that has been explicitly assigned to
// to the given buffer, if any.
//
// Returns a {String} id of the language
getAssignedLanguageId (buffer) {
return this.languageOverridesByBufferId.get(buffer.id)
}
// Extended: Remove any language mode override that has been set for the
// given {TextBuffer}. This will assign to the buffer the best language
// mode available.
@@ -293,7 +301,7 @@ class GrammarRegistry {
grammarOverrideForPath (filePath) {
Grim.deprecate('Use buffer.getLanguageMode().getLanguageId() instead')
const buffer = atom.project.findBufferForPath(filePath)
if (buffer) return this.languageOverridesByBufferId.get(buffer.id)
if (buffer) return this.getAssignedLanguageId(buffer)
}
// Deprecated: Set the grammar override for the given file path.
@@ -391,7 +399,7 @@ class GrammarRegistry {
if (grammar instanceof TreeSitterGrammar) {
this.treeSitterGrammarsById[grammar.id] = grammar
if (grammar.legacyScopeName) {
this.config.addLegacyScopeAlias(grammar.id, grammar.legacyScopeName)
this.config.setLegacyScopeAliasForNewScope(grammar.id, grammar.legacyScopeName)
this.textMateScopeNamesByTreeSitterLanguageId.set(grammar.id, grammar.legacyScopeName)
this.treeSitterLanguageIdsByTextMateScopeName.set(grammar.legacyScopeName, grammar.id)
}
@@ -406,7 +414,7 @@ class GrammarRegistry {
if (grammar instanceof TreeSitterGrammar) {
delete this.treeSitterGrammarsById[grammar.id]
if (grammar.legacyScopeName) {
this.config.removeLegacyScopeAlias(grammar.id)
this.config.removeLegacyScopeAliasForNewScope(grammar.id)
this.textMateScopeNamesByTreeSitterLanguageId.delete(grammar.id)
this.treeSitterLanguageIdsByTextMateScopeName.delete(grammar.legacyScopeName)
}
@@ -429,7 +437,7 @@ class GrammarRegistry {
this.readGrammar(grammarPath, (error, grammar) => {
if (error) return callback(error)
this.addGrammar(grammar)
callback(grammar)
callback(null, grammar)
})
}

View File

@@ -33,7 +33,7 @@ class AtomApplication extends EventEmitter {
// Public: The entry point into the Atom application.
static open (options) {
if (!options.socketPath) {
const username = process.platform === 'win32' ? process.env.USERNAME : process.env.USER
const {username} = os.userInfo()
// Lowercasing the ATOM_HOME to make sure that we don't get multiple sockets
// on case-insensitive filesystems due to arbitrary case differences in paths.
@@ -44,7 +44,7 @@ class AtomApplication extends EventEmitter {
.update('|')
.update(process.arch)
.update('|')
.update(username)
.update(username || '')
.update('|')
.update(atomHomeUnique)
@@ -116,7 +116,9 @@ class AtomApplication extends EventEmitter {
this.configFile = new ConfigFile(configFilePath)
this.config = new Config({
saveCallback: settings => this.configFile.update(settings)
saveCallback: settings => {
if (!this.quitting) return this.configFile.update(settings)
}
})
this.config.setSchema(null, {type: 'object', properties: _.clone(ConfigSchema)})
@@ -146,8 +148,6 @@ class AtomApplication extends EventEmitter {
this.config.set('core.titleBar', 'custom')
}
this.config.onDidChange('core.titleBar', this.promptForRestart.bind(this))
process.nextTick(() => this.autoUpdateManager.initialize())
this.applicationMenu = new ApplicationMenu(this.version, this.autoUpdateManager)
this.atomProtocolHandler = new AtomProtocolHandler(this.resourcePath, this.safeMode)
@@ -171,6 +171,7 @@ class AtomApplication extends EventEmitter {
if (!this.configFilePromise) {
this.configFilePromise = this.configFile.watch()
this.disposable.add(await this.configFilePromise)
this.config.onDidChange('core.titleBar', this.promptForRestart.bind(this))
}
const optionsForWindowsToOpen = []
@@ -562,7 +563,7 @@ class AtomApplication extends EventEmitter {
}))
this.disposable.add(ipcHelpers.respondTo('set-user-settings', (window, settings) =>
this.configFile.update(settings)
this.configFile.update(JSON.parse(settings))
))
this.disposable.add(ipcHelpers.respondTo('center-window', window => window.center()))
@@ -842,13 +843,12 @@ class AtomApplication extends EventEmitter {
let existingWindow
if (!newWindow) {
existingWindow = this.windowForPaths(pathsToOpen, devMode)
const stats = pathsToOpen.map(pathToOpen => fs.statSyncNoException(pathToOpen))
if (!existingWindow) {
let lastWindow = window || this.getLastFocusedWindow()
if (lastWindow && lastWindow.devMode === devMode) {
if (addToLastWindow || (
stats.every(s => s.isFile && s.isFile()) ||
(stats.some(s => s.isDirectory && s.isDirectory()) && !lastWindow.hasProjectPath()))) {
locationsToOpen.every(({stat}) => stat && stat.isFile()) ||
(locationsToOpen.some(({stat}) => stat && stat.isDirectory()) && !lastWindow.hasProjectPath()))) {
existingWindow = lastWindow
}
}
@@ -1273,11 +1273,11 @@ class AtomApplication extends EventEmitter {
initialLine = initialColumn = null
}
if (url.parse(pathToOpen).protocol == null) {
pathToOpen = path.resolve(executedFrom, fs.normalize(pathToOpen))
}
const normalizedPath = path.normalize(path.resolve(executedFrom, fs.normalize(pathToOpen)))
const stat = fs.statSyncNoException(normalizedPath)
if (stat || !url.parse(pathToOpen).protocol) pathToOpen = normalizedPath
return {pathToOpen, initialLine, initialColumn}
return {pathToOpen, stat, initialLine, initialColumn}
}
// Opens a native dialog to prompt the user for a path.

View File

@@ -61,7 +61,6 @@ class AtomWindow extends EventEmitter {
}
this.loadDataOverProcessBoundary()
this.handleEvents()
this.loadSettings = Object.assign({}, settings)
@@ -74,14 +73,13 @@ class AtomWindow extends EventEmitter {
if (!this.loadSettings.initialPaths) {
this.loadSettings.initialPaths = []
for (const {pathToOpen} of locationsToOpen) {
for (const {pathToOpen, stat} of locationsToOpen) {
if (!pathToOpen) continue
const stat = fs.statSyncNoException(pathToOpen) || null
if (stat && stat.isDirectory()) {
this.loadSettings.initialPaths.push(pathToOpen)
} else {
const parentDirectory = path.dirname(pathToOpen)
if ((stat && stat.isFile()) || fs.existsSync(parentDirectory)) {
if (stat && stat.isFile() || fs.existsSync(parentDirectory)) {
this.loadSettings.initialPaths.push(parentDirectory)
} else {
this.loadSettings.initialPaths.push(pathToOpen)
@@ -165,12 +163,13 @@ class AtomWindow extends EventEmitter {
containsPath (pathToCheck) {
if (!pathToCheck) return false
const stat = fs.statSyncNoException(pathToCheck)
if (stat && stat.isDirectory()) return false
return this.representedDirectoryPaths.some(projectPath =>
pathToCheck === projectPath || pathToCheck.startsWith(path.join(projectPath, path.sep))
)
let stat
return this.representedDirectoryPaths.some(projectPath => {
if (pathToCheck === projectPath) return true
if (!pathToCheck.startsWith(path.join(projectPath, path.sep))) return false
if (stat === undefined) stat = fs.statSyncNoException(pathToCheck)
return !stat || !stat.isDirectory()
})
}
handleEvents () {

View File

@@ -83,7 +83,11 @@ function cloneMenuItem (item) {
'submenu',
'commandDetail',
'role',
'accelerator'
'accelerator',
'before',
'after',
'beforeGroupContaining',
'afterGroupContaining'
)
if (item.submenu != null) {
item.submenu = item.submenu.map(submenuItem => cloneMenuItem(submenuItem))

186
src/menu-sort-helpers.js Normal file
View File

@@ -0,0 +1,186 @@
// UTILS
function splitArray (arr, predicate) {
let lastArr = []
const multiArr = [lastArr]
arr.forEach(item => {
if (predicate(item)) {
if (lastArr.length > 0) {
lastArr = []
multiArr.push(lastArr)
}
} else {
lastArr.push(item)
}
})
return multiArr
}
function joinArrays (arrays, joiner) {
const joinedArr = []
arrays.forEach((arr, i) => {
if (i > 0 && arr.length > 0) {
joinedArr.push(joiner)
}
joinedArr.push(...arr)
})
return joinedArr
}
const pushOntoMultiMap = (map, key, value) => {
if (!map.has(key)) {
map.set(key, [])
}
map.get(key).push(value)
}
function indexOfGroupContainingCommand (groups, command, ignoreGroup) {
return groups.findIndex(
candiateGroup =>
candiateGroup !== ignoreGroup &&
candiateGroup.some(
candidateItem => candidateItem.command === command
)
)
}
// Sort nodes topologically using a depth-first approach. Encountered cycles
// are broken.
function sortTopologically (originalOrder, edgesById) {
const sorted = []
const marked = new Set()
function visit (id) {
if (marked.has(id)) {
// Either this node has already been placed, or we have encountered a
// cycle and need to exit.
return
}
marked.add(id)
const edges = edgesById.get(id)
if (edges != null) {
edges.forEach(visit)
}
sorted.push(id)
}
originalOrder.forEach(visit)
return sorted
}
function attemptToMergeAGroup (groups) {
for (let i = 0; i < groups.length; i++) {
const group = groups[i]
for (const item of group) {
const toCommands = [...(item.before || []), ...(item.after || [])]
for (const command of toCommands) {
const index = indexOfGroupContainingCommand(groups, command, group)
if (index === -1) {
// No valid edge for this command
continue
}
const mergeTarget = groups[index]
// Merge with group containing `command`
mergeTarget.push(...group)
groups.splice(i, 1)
return true
}
}
}
return false
}
// Merge groups based on before/after positions
// Mutates both the array of groups, and the individual group arrays.
function mergeGroups (groups) {
let mergedAGroup = true
while (mergedAGroup) {
mergedAGroup = attemptToMergeAGroup(groups)
}
return groups
}
function sortItemsInGroup (group) {
const originalOrder = group.map((node, i) => i)
const edges = new Map()
const commandToIndex = new Map(group.map((item, i) => [item.command, i]))
group.forEach((item, i) => {
if (item.before) {
item.before.forEach(toCommand => {
const to = commandToIndex.get(toCommand)
if (to != null) {
pushOntoMultiMap(edges, to, i)
}
})
}
if (item.after) {
item.after.forEach(toCommand => {
const to = commandToIndex.get(toCommand)
if (to != null) {
pushOntoMultiMap(edges, i, to)
}
})
}
})
const sortedNodes = sortTopologically(originalOrder, edges)
return sortedNodes.map(i => group[i])
}
function findEdgesInGroup (groups, i, edges) {
const group = groups[i]
for (const item of group) {
if (item.beforeGroupContaining) {
for (const command of item.beforeGroupContaining) {
const to = indexOfGroupContainingCommand(groups, command, group)
if (to !== -1) {
pushOntoMultiMap(edges, to, i)
return
}
}
}
if (item.afterGroupContaining) {
for (const command of item.afterGroupContaining) {
const to = indexOfGroupContainingCommand(groups, command, group)
if (to !== -1) {
pushOntoMultiMap(edges, i, to)
return
}
}
}
}
}
function sortGroups (groups) {
const originalOrder = groups.map((item, i) => i)
const edges = new Map()
for (let i = 0; i < groups.length; i++) {
findEdgesInGroup(groups, i, edges)
}
const sortedGroupIndexes = sortTopologically(originalOrder, edges)
return sortedGroupIndexes.map(i => groups[i])
}
function isSeparator (item) {
return item.type === 'separator'
}
function sortMenuItems (menuItems) {
// Split the items into their implicit groups based upon separators.
const groups = splitArray(menuItems, isSeparator)
// Merge groups that contain before/after references to eachother.
const mergedGroups = mergeGroups(groups)
// Sort each individual group internally.
const mergedGroupsWithSortedItems = mergedGroups.map(sortItemsInGroup)
// Sort the groups based upon their beforeGroupContaining/afterGroupContaining
// references.
const sortedGroups = sortGroups(mergedGroupsWithSortedItems)
// Join the groups back
return joinArrays(sortedGroups, { type: 'separator' })
}
module.exports = {sortMenuItems}

View File

@@ -43,8 +43,8 @@ class Package {
? params.bundledPackage
: this.packageManager.isBundledPackagePath(this.path)
this.name =
params.name ||
(this.metadata && this.metadata.name) ||
params.name ||
path.basename(this.path)
this.reset()
}

View File

@@ -1,28 +0,0 @@
{CompositeDisposable} = require 'event-kit'
_ = require 'underscore-plus'
module.exports =
class PaneContainerElement extends HTMLElement
createdCallback: ->
@subscriptions = new CompositeDisposable
@classList.add 'panes'
initialize: (@model, {@views}) ->
throw new Error("Must pass a views parameter when initializing PaneContainerElements") unless @views?
@subscriptions.add @model.observeRoot(@rootChanged.bind(this))
this
rootChanged: (root) ->
focusedElement = document.activeElement if @hasFocus()
@firstChild?.remove()
if root?
view = @views.getView(root)
@appendChild(view)
focusedElement?.focus()
hasFocus: ->
this is document.activeElement or @contains(document.activeElement)
module.exports = PaneContainerElement = document.registerElement 'atom-pane-container', prototype: PaneContainerElement.prototype

View File

@@ -0,0 +1,40 @@
const {CompositeDisposable} = require('event-kit')
class PaneContainerElement extends HTMLElement {
createdCallback () {
this.subscriptions = new CompositeDisposable()
this.classList.add('panes')
}
initialize (model, {views}) {
this.model = model
this.views = views
if (this.views == null) {
throw new Error('Must pass a views parameter when initializing PaneContainerElements')
}
this.subscriptions.add(this.model.observeRoot(this.rootChanged.bind(this)))
return this
}
rootChanged (root) {
const focusedElement = this.hasFocus() ? document.activeElement : null
if (this.firstChild != null) {
this.firstChild.remove()
}
if (root != null) {
const view = this.views.getView(root)
this.appendChild(view)
if (focusedElement != null) {
focusedElement.focus()
}
}
}
hasFocus () {
return this === document.activeElement || this.contains(document.activeElement)
}
}
module.exports = document.registerElement('atom-pane-container', {
prototype: PaneContainerElement.prototype
})

View File

@@ -1,139 +0,0 @@
path = require 'path'
{CompositeDisposable} = require 'event-kit'
class PaneElement extends HTMLElement
attached: false
createdCallback: ->
@attached = false
@subscriptions = new CompositeDisposable
@inlineDisplayStyles = new WeakMap
@initializeContent()
@subscribeToDOMEvents()
attachedCallback: ->
@attached = true
@focus() if @model.isFocused()
detachedCallback: ->
@attached = false
initializeContent: ->
@setAttribute 'class', 'pane'
@setAttribute 'tabindex', -1
@appendChild @itemViews = document.createElement('div')
@itemViews.setAttribute 'class', 'item-views'
subscribeToDOMEvents: ->
handleFocus = (event) =>
@model.focus() unless @isActivating or @model.isDestroyed() or @contains(event.relatedTarget)
if event.target is this and view = @getActiveView()
view.focus()
event.stopPropagation()
handleBlur = (event) =>
@model.blur() unless @contains(event.relatedTarget)
handleDragOver = (event) ->
event.preventDefault()
event.stopPropagation()
handleDrop = (event) =>
event.preventDefault()
event.stopPropagation()
@getModel().activate()
pathsToOpen = Array::map.call event.dataTransfer.files, (file) -> file.path
@applicationDelegate.open({pathsToOpen}) if pathsToOpen.length > 0
@addEventListener 'focus', handleFocus, true
@addEventListener 'blur', handleBlur, true
@addEventListener 'dragover', handleDragOver
@addEventListener 'drop', handleDrop
initialize: (@model, {@views, @applicationDelegate}) ->
throw new Error("Must pass a views parameter when initializing PaneElements") unless @views?
throw new Error("Must pass an applicationDelegate parameter when initializing PaneElements") unless @applicationDelegate?
@subscriptions.add @model.onDidActivate(@activated.bind(this))
@subscriptions.add @model.observeActive(@activeStatusChanged.bind(this))
@subscriptions.add @model.observeActiveItem(@activeItemChanged.bind(this))
@subscriptions.add @model.onDidRemoveItem(@itemRemoved.bind(this))
@subscriptions.add @model.onDidDestroy(@paneDestroyed.bind(this))
@subscriptions.add @model.observeFlexScale(@flexScaleChanged.bind(this))
this
getModel: -> @model
activated: ->
@isActivating = true
@focus() unless @hasFocus() # Don't steal focus from children.
@isActivating = false
activeStatusChanged: (active) ->
if active
@classList.add('active')
else
@classList.remove('active')
activeItemChanged: (item) ->
delete @dataset.activeItemName
delete @dataset.activeItemPath
@changePathDisposable?.dispose()
return unless item?
hasFocus = @hasFocus()
itemView = @views.getView(item)
if itemPath = item.getPath?()
@dataset.activeItemName = path.basename(itemPath)
@dataset.activeItemPath = itemPath
if item.onDidChangePath?
@changePathDisposable = item.onDidChangePath =>
itemPath = item.getPath()
@dataset.activeItemName = path.basename(itemPath)
@dataset.activeItemPath = itemPath
unless @itemViews.contains(itemView)
@itemViews.appendChild(itemView)
for child in @itemViews.children
if child is itemView
@showItemView(child) if @attached
else
@hideItemView(child)
itemView.focus() if hasFocus
showItemView: (itemView) ->
inlineDisplayStyle = @inlineDisplayStyles.get(itemView)
if inlineDisplayStyle?
itemView.style.display = inlineDisplayStyle
else
itemView.style.display = ''
hideItemView: (itemView) ->
inlineDisplayStyle = itemView.style.display
unless inlineDisplayStyle is 'none'
@inlineDisplayStyles.set(itemView, inlineDisplayStyle) if inlineDisplayStyle?
itemView.style.display = 'none'
itemRemoved: ({item, index, destroyed}) ->
if viewToRemove = @views.getView(item)
viewToRemove.remove()
paneDestroyed: ->
@subscriptions.dispose()
@changePathDisposable?.dispose()
flexScaleChanged: (flexScale) ->
@style.flexGrow = flexScale
getActiveView: -> @views.getView(@model.getActiveItem())
hasFocus: ->
this is document.activeElement or @contains(document.activeElement)
module.exports = PaneElement = document.registerElement 'atom-pane', prototype: PaneElement.prototype

218
src/pane-element.js Normal file
View File

@@ -0,0 +1,218 @@
const path = require('path')
const {CompositeDisposable} = require('event-kit')
class PaneElement extends HTMLElement {
createdCallback () {
this.attached = false
this.subscriptions = new CompositeDisposable()
this.inlineDisplayStyles = new WeakMap()
this.initializeContent()
this.subscribeToDOMEvents()
}
attachedCallback () {
this.attached = true
if (this.model.isFocused()) {
this.focus()
}
}
detachedCallback () {
this.attached = false
}
initializeContent () {
this.setAttribute('class', 'pane')
this.setAttribute('tabindex', -1)
this.itemViews = document.createElement('div')
this.appendChild(this.itemViews)
this.itemViews.setAttribute('class', 'item-views')
}
subscribeToDOMEvents () {
const handleFocus = event => {
if (
!(
this.isActivating ||
this.model.isDestroyed() ||
this.contains(event.relatedTarget)
)
) {
this.model.focus()
}
if (event.target !== this) return
const view = this.getActiveView()
if (view) {
view.focus()
event.stopPropagation()
}
}
const handleBlur = event => {
if (!this.contains(event.relatedTarget)) {
this.model.blur()
}
}
const handleDragOver = event => {
event.preventDefault()
event.stopPropagation()
}
const handleDrop = event => {
event.preventDefault()
event.stopPropagation()
this.getModel().activate()
const pathsToOpen = [...event.dataTransfer.files].map(file => file.path)
if (pathsToOpen.length > 0) {
this.applicationDelegate.open({pathsToOpen})
}
}
this.addEventListener('focus', handleFocus, true)
this.addEventListener('blur', handleBlur, true)
this.addEventListener('dragover', handleDragOver)
this.addEventListener('drop', handleDrop)
}
initialize (model, {views, applicationDelegate}) {
this.model = model
this.views = views
this.applicationDelegate = applicationDelegate
if (this.views == null) {
throw new Error(
'Must pass a views parameter when initializing PaneElements'
)
}
if (this.applicationDelegate == null) {
throw new Error(
'Must pass an applicationDelegate parameter when initializing PaneElements'
)
}
this.subscriptions.add(this.model.onDidActivate(this.activated.bind(this)))
this.subscriptions.add(
this.model.observeActive(this.activeStatusChanged.bind(this))
)
this.subscriptions.add(
this.model.observeActiveItem(this.activeItemChanged.bind(this))
)
this.subscriptions.add(
this.model.onDidRemoveItem(this.itemRemoved.bind(this))
)
this.subscriptions.add(
this.model.onDidDestroy(this.paneDestroyed.bind(this))
)
this.subscriptions.add(
this.model.observeFlexScale(this.flexScaleChanged.bind(this))
)
return this
}
getModel () {
return this.model
}
activated () {
this.isActivating = true
if (!this.hasFocus()) {
// Don't steal focus from children.
this.focus()
}
this.isActivating = false
}
activeStatusChanged (active) {
if (active) {
this.classList.add('active')
} else {
this.classList.remove('active')
}
}
activeItemChanged (item) {
delete this.dataset.activeItemName
delete this.dataset.activeItemPath
if (this.changePathDisposable != null) {
this.changePathDisposable.dispose()
}
if (item == null) {
return
}
const hasFocus = this.hasFocus()
const itemView = this.views.getView(item)
const itemPath = typeof item.getPath === 'function' ? item.getPath() : null
if (itemPath) {
this.dataset.activeItemName = path.basename(itemPath)
this.dataset.activeItemPath = itemPath
if (item.onDidChangePath != null) {
this.changePathDisposable = item.onDidChangePath(() => {
const itemPath = item.getPath()
this.dataset.activeItemName = path.basename(itemPath)
this.dataset.activeItemPath = itemPath
})
}
}
if (!this.itemViews.contains(itemView)) {
this.itemViews.appendChild(itemView)
}
for (const child of this.itemViews.children) {
if (child === itemView) {
if (this.attached) {
this.showItemView(child)
}
} else {
this.hideItemView(child)
}
}
if (hasFocus) {
itemView.focus()
}
}
showItemView (itemView) {
const inlineDisplayStyle = this.inlineDisplayStyles.get(itemView)
if (inlineDisplayStyle != null) {
itemView.style.display = inlineDisplayStyle
} else {
itemView.style.display = ''
}
}
hideItemView (itemView) {
const inlineDisplayStyle = itemView.style.display
if (inlineDisplayStyle !== 'none') {
if (inlineDisplayStyle != null) {
this.inlineDisplayStyles.set(itemView, inlineDisplayStyle)
}
itemView.style.display = 'none'
}
}
itemRemoved ({item, index, destroyed}) {
const viewToRemove = this.views.getView(item)
if (viewToRemove) {
viewToRemove.remove()
}
}
paneDestroyed () {
this.subscriptions.dispose()
if (this.changePathDisposable != null) {
this.changePathDisposable.dispose()
}
}
flexScaleChanged (flexScale) {
this.style.flexGrow = flexScale
}
getActiveView () {
return this.views.getView(this.model.getActiveItem())
}
hasFocus () {
return (
this === document.activeElement || this.contains(document.activeElement)
)
}
}
module.exports = document.registerElement('atom-pane', {
prototype: PaneElement.prototype
})

View File

@@ -3,6 +3,7 @@ const path = require('path')
const {Emitter, Disposable, CompositeDisposable} = require('event-kit')
const nsfw = require('@atom/nsfw')
const watcher = require('@atom/watcher')
const {NativeWatcherRegistry} = require('./native-watcher-registry')
// Private: Associate native watcher action flags with descriptive String equivalents.
@@ -21,145 +22,7 @@ const WATCHER_STATE = {
STOPPING: Symbol('stopping')
}
// Private: Emulate a "filesystem watcher" by subscribing to Atom events like buffers being saved. This will miss
// any changes made to files outside of Atom, but it also has no overhead.
class AtomBackend {
async start (rootPath, eventCallback, errorCallback) {
const getRealPath = givenPath => {
return new Promise(resolve => {
fs.realpath(givenPath, (err, resolvedPath) => {
err ? resolve(null) : resolve(resolvedPath)
})
})
}
this.subs = new CompositeDisposable()
this.subs.add(atom.workspace.observeTextEditors(async editor => {
let realPath = await getRealPath(editor.getPath())
if (!realPath || !realPath.startsWith(rootPath)) {
return
}
const announce = (action, oldPath) => {
const payload = {action, path: realPath}
if (oldPath) payload.oldPath = oldPath
eventCallback([payload])
}
const buffer = editor.getBuffer()
this.subs.add(buffer.onDidConflict(() => announce('modified')))
this.subs.add(buffer.onDidReload(() => announce('modified')))
this.subs.add(buffer.onDidSave(event => {
if (event.path === realPath) {
announce('modified')
} else {
const oldPath = realPath
realPath = event.path
announce('renamed', oldPath)
}
}))
this.subs.add(buffer.onDidDelete(() => announce('deleted')))
this.subs.add(buffer.onDidChangePath(newPath => {
if (newPath !== realPath) {
const oldPath = realPath
realPath = newPath
announce('renamed', oldPath)
}
}))
}))
// Giant-ass brittle hack to hook files (and eventually directories) created from the TreeView.
const treeViewPackage = await atom.packages.getLoadedPackage('tree-view')
if (!treeViewPackage) return
await treeViewPackage.activationPromise
const treeViewModule = treeViewPackage.mainModule
if (!treeViewModule) return
const treeView = treeViewModule.getTreeViewInstance()
const isOpenInEditor = async eventPath => {
const openPaths = await Promise.all(
atom.workspace.getTextEditors().map(editor => getRealPath(editor.getPath()))
)
return openPaths.includes(eventPath)
}
this.subs.add(treeView.onFileCreated(async event => {
const realPath = await getRealPath(event.path)
if (!realPath) return
eventCallback([{action: 'added', path: realPath}])
}))
this.subs.add(treeView.onEntryDeleted(async event => {
const realPath = await getRealPath(event.path)
if (!realPath || isOpenInEditor(realPath)) return
eventCallback([{action: 'deleted', path: realPath}])
}))
this.subs.add(treeView.onEntryMoved(async event => {
const [realNewPath, realOldPath] = await Promise.all([
getRealPath(event.newPath),
getRealPath(event.initialPath)
])
if (!realNewPath || !realOldPath || isOpenInEditor(realNewPath) || isOpenInEditor(realOldPath)) return
eventCallback([{action: 'renamed', path: realNewPath, oldPath: realOldPath}])
}))
}
async stop () {
this.subs && this.subs.dispose()
}
}
// Private: Implement a native watcher by translating events from an NSFW watcher.
class NSFWBackend {
async start (rootPath, eventCallback, errorCallback) {
const handler = events => {
eventCallback(events.map(event => {
const action = ACTION_MAP.get(event.action) || `unexpected (${event.action})`
const payload = {action}
if (event.file) {
payload.path = path.join(event.directory, event.file)
} else {
payload.oldPath = path.join(event.directory, event.oldFile)
payload.path = path.join(event.directory, event.newFile)
}
return payload
}))
}
this.watcher = await nsfw(
rootPath,
handler,
{debounceMS: 100, errorCallback}
)
await this.watcher.start()
}
stop () {
return this.watcher.stop()
}
}
// Private: Map configuration settings from the feature flag to backend implementations.
const BACKENDS = {
atom: AtomBackend,
native: NSFWBackend
}
// Private: the backend implementation to fall back to if the config setting is invalid.
const DEFAULT_BACKEND = BACKENDS.nsfw
// Private: Interface with and normalize events from a native OS filesystem watcher.
// Private: Interface with and normalize events from a filesystem watcher implementation.
class NativeWatcher {
// Private: Initialize a native watcher on a path.
@@ -170,37 +33,10 @@ class NativeWatcher {
this.emitter = new Emitter()
this.subs = new CompositeDisposable()
this.backend = null
this.state = WATCHER_STATE.STOPPED
this.onEvents = this.onEvents.bind(this)
this.onError = this.onError.bind(this)
this.subs.add(atom.config.onDidChange('core.fileSystemWatcher', async () => {
if (this.state === WATCHER_STATE.STARTING) {
// Wait for this watcher to finish starting.
await new Promise(resolve => {
const sub = this.onDidStart(() => {
sub.dispose()
resolve()
})
})
}
// Re-read the config setting in case it's changed again while we were waiting for the watcher
// to start.
const Backend = this.getCurrentBackend()
if (this.state === WATCHER_STATE.RUNNING && !(this.backend instanceof Backend)) {
await this.stop()
await this.start()
}
}))
}
// Private: Read the `core.fileSystemWatcher` setting to determine the filesystem backend to use.
getCurrentBackend () {
const setting = atom.config.get('core.fileSystemWatcher')
return BACKENDS[setting] || DEFAULT_BACKEND
}
// Private: Begin watching for filesystem events.
@@ -212,15 +48,16 @@ class NativeWatcher {
}
this.state = WATCHER_STATE.STARTING
const Backend = this.getCurrentBackend()
this.backend = new Backend()
await this.backend.start(this.normalizedPath, this.onEvents, this.onError)
await this.doStart()
this.state = WATCHER_STATE.RUNNING
this.emitter.emit('did-start')
}
doStart () {
return Promise.reject('doStart() not overridden')
}
// Private: Return true if the underlying watcher is actively listening for filesystem events.
isRunning () {
return this.state === WATCHER_STATE.RUNNING
@@ -283,8 +120,8 @@ class NativeWatcher {
//
// * `replacement` the new {NativeWatcher} instance that a live {Watcher} instance should reattach to instead.
// * `watchedPath` absolute path watched by the new {NativeWatcher}.
reattachTo (replacement, watchedPath) {
this.emitter.emit('should-detach', {replacement, watchedPath})
reattachTo (replacement, watchedPath, options) {
this.emitter.emit('should-detach', {replacement, watchedPath, options})
}
// Private: Stop the native watcher and release any operating system resources associated with it.
@@ -297,12 +134,17 @@ class NativeWatcher {
this.state = WATCHER_STATE.STOPPING
this.emitter.emit('will-stop')
await this.backend.stop()
await this.doStop()
this.state = WATCHER_STATE.STOPPED
this.emitter.emit('did-stop')
}
doStop () {
return Promise.resolve()
}
// Private: Detach any event subscribers.
dispose () {
this.emitter.dispose()
@@ -324,6 +166,129 @@ class NativeWatcher {
}
}
// Private: Emulate a "filesystem watcher" by subscribing to Atom events like buffers being saved. This will miss
// any changes made to files outside of Atom, but it also has no overhead.
class AtomNativeWatcher extends NativeWatcher {
async doStart () {
const getRealPath = givenPath => {
return new Promise(resolve => {
fs.realpath(givenPath, (err, resolvedPath) => {
err ? resolve(null) : resolve(resolvedPath)
})
})
}
this.subs.add(atom.workspace.observeTextEditors(async editor => {
let realPath = await getRealPath(editor.getPath())
if (!realPath || !realPath.startsWith(this.normalizedPath)) {
return
}
const announce = (action, oldPath) => {
const payload = {action, path: realPath}
if (oldPath) payload.oldPath = oldPath
this.onEvents([payload])
}
const buffer = editor.getBuffer()
this.subs.add(buffer.onDidConflict(() => announce('modified')))
this.subs.add(buffer.onDidReload(() => announce('modified')))
this.subs.add(buffer.onDidSave(event => {
if (event.path === realPath) {
announce('modified')
} else {
const oldPath = realPath
realPath = event.path
announce('renamed', oldPath)
}
}))
this.subs.add(buffer.onDidDelete(() => announce('deleted')))
this.subs.add(buffer.onDidChangePath(newPath => {
if (newPath !== this.normalizedPath) {
const oldPath = this.normalizedPath
this.normalizedPath = newPath
announce('renamed', oldPath)
}
}))
}))
// Giant-ass brittle hack to hook files (and eventually directories) created from the TreeView.
const treeViewPackage = await atom.packages.getLoadedPackage('tree-view')
if (!treeViewPackage) return
await treeViewPackage.activationPromise
const treeViewModule = treeViewPackage.mainModule
if (!treeViewModule) return
const treeView = treeViewModule.getTreeViewInstance()
const isOpenInEditor = async eventPath => {
const openPaths = await Promise.all(
atom.workspace.getTextEditors().map(editor => getRealPath(editor.getPath()))
)
return openPaths.includes(eventPath)
}
this.subs.add(treeView.onFileCreated(async event => {
const realPath = await getRealPath(event.path)
if (!realPath) return
this.onEvents([{action: 'added', path: realPath}])
}))
this.subs.add(treeView.onEntryDeleted(async event => {
const realPath = await getRealPath(event.path)
if (!realPath || isOpenInEditor(realPath)) return
this.onEvents([{action: 'deleted', path: realPath}])
}))
this.subs.add(treeView.onEntryMoved(async event => {
const [realNewPath, realOldPath] = await Promise.all([
getRealPath(event.newPath),
getRealPath(event.initialPath)
])
if (!realNewPath || !realOldPath || isOpenInEditor(realNewPath) || isOpenInEditor(realOldPath)) return
this.onEvents([{action: 'renamed', path: realNewPath, oldPath: realOldPath}])
}))
}
}
// Private: Implement a native watcher by translating events from an NSFW watcher.
class NSFWNativeWatcher extends NativeWatcher {
async doStart (rootPath, eventCallback, errorCallback) {
const handler = events => {
this.onEvents(events.map(event => {
const action = ACTION_MAP.get(event.action) || `unexpected (${event.action})`
const payload = {action}
if (event.file) {
payload.path = path.join(event.directory, event.file)
} else {
payload.oldPath = path.join(event.directory, event.oldFile)
payload.path = path.join(event.directory, event.newFile)
}
return payload
}))
}
this.watcher = await nsfw(
this.normalizedPath,
handler,
{debounceMS: 100, errorCallback: this.onError}
)
await this.watcher.start()
}
doStop () {
return this.watcher.stop()
}
}
// Extended: Manage a subscription to filesystem events that occur beneath a root directory. Construct these by
// calling `watchPath`. To watch for events within active project directories, use {Project::onDidChangeFiles}
// instead.
@@ -384,6 +349,15 @@ class PathWatcher {
this.native = null
this.changeCallbacks = new Map()
this.attachedPromise = new Promise(resolve => {
this.resolveAttachedPromise = resolve
})
this.startPromise = new Promise((resolve, reject) => {
this.resolveStartPromise = resolve
this.rejectStartPromise = reject
})
this.normalizedPathPromise = new Promise((resolve, reject) => {
fs.realpath(watchedPath, (err, real) => {
if (err) {
@@ -395,13 +369,7 @@ class PathWatcher {
resolve(real)
})
})
this.attachedPromise = new Promise(resolve => {
this.resolveAttachedPromise = resolve
})
this.startPromise = new Promise(resolve => {
this.resolveStartPromise = resolve
})
this.normalizedPathPromise.catch(err => this.rejectStartPromise(err))
this.emitter = new Emitter()
this.subs = new CompositeDisposable()
@@ -543,46 +511,139 @@ class PathWatcher {
}
}
// Private: Globally tracked state used to de-duplicate related [PathWatchers]{PathWatcher}.
// Private: Globally tracked state used to de-duplicate related [PathWatchers]{PathWatcher} backed by emulated Atom
// events or NSFW.
class PathWatcherManager {
// Private: Access or lazily initialize the singleton manager instance.
//
// Returns the one and only {PathWatcherManager}.
static instance () {
if (!PathWatcherManager.theManager) {
PathWatcherManager.theManager = new PathWatcherManager()
// Private: Access the currently active manager instance, creating one if necessary.
static active () {
if (!this.activeManager) {
this.activeManager = new PathWatcherManager(atom.config.get('core.fileSystemWatcher'))
this.sub = atom.config.onDidChange('core.fileSystemWatcher', ({newValue}) => { this.transitionTo(newValue) })
}
return PathWatcherManager.theManager
return this.activeManager
}
// Private: Replace the active {PathWatcherManager} with a new one that creates [NativeWatchers]{NativeWatcher}
// based on the value of `setting`.
static async transitionTo (setting) {
const current = this.active()
if (this.transitionPromise) {
await this.transitionPromise
}
if (current.setting === setting) {
return
}
current.isShuttingDown = true
let resolveTransitionPromise = () => {}
this.transitionPromise = new Promise(resolve => {
resolveTransitionPromise = resolve
})
const replacement = new PathWatcherManager(setting)
this.activeManager = replacement
await Promise.all(
Array.from(current.live, async ([root, native]) => {
const w = await replacement.createWatcher(root, {}, () => {})
native.reattachTo(w.native, root, w.native.options || {})
})
)
current.stopAllWatchers()
resolveTransitionPromise()
this.transitionPromise = null
}
// Private: Initialize global {PathWatcher} state.
constructor () {
this.live = new Set()
this.nativeRegistry = new NativeWatcherRegistry(
normalizedPath => {
const nativeWatcher = new NativeWatcher(normalizedPath)
constructor (setting) {
this.setting = setting
this.live = new Map()
this.live.add(nativeWatcher)
const sub = nativeWatcher.onWillStop(() => {
this.live.delete(nativeWatcher)
sub.dispose()
})
const initLocal = NativeConstructor => {
this.nativeRegistry = new NativeWatcherRegistry(
normalizedPath => {
const nativeWatcher = new NativeConstructor(normalizedPath)
return nativeWatcher
}
)
this.live.set(normalizedPath, nativeWatcher)
const sub = nativeWatcher.onWillStop(() => {
this.live.delete(normalizedPath)
sub.dispose()
})
return nativeWatcher
}
)
}
if (setting === 'atom') {
initLocal(AtomNativeWatcher)
} else if (setting === 'experimental') {
//
} else if (setting === 'poll') {
//
} else {
initLocal(NSFWNativeWatcher)
}
this.isShuttingDown = false
}
useExperimentalWatcher () {
return this.setting === 'experimental' || this.setting === 'poll'
}
// Private: Create a {PathWatcher} tied to this global state. See {watchPath} for detailed arguments.
createWatcher (rootPath, options, eventCallback) {
const watcher = new PathWatcher(this.nativeRegistry, rootPath, options)
watcher.onDidChange(eventCallback)
return watcher
async createWatcher (rootPath, options, eventCallback) {
if (this.isShuttingDown) {
await this.constructor.transitionPromise
return PathWatcherManager.active().createWatcher(rootPath, options, eventCallback)
}
if (this.useExperimentalWatcher()) {
if (this.setting === 'poll') {
options.poll = true
}
const w = await watcher.watchPath(rootPath, options, eventCallback)
this.live.set(rootPath, w.native)
return w
}
const w = new PathWatcher(this.nativeRegistry, rootPath, options)
w.onDidChange(eventCallback)
await w.getStartPromise()
return w
}
// Private: Directly access the {NativeWatcherRegistry}.
getRegistry () {
if (this.useExperimentalWatcher()) {
return watcher.getRegistry()
}
return this.nativeRegistry
}
// Private: Sample watcher usage statistics. Only available for experimental watchers.
status () {
if (this.useExperimentalWatcher()) {
return watcher.status()
}
return {}
}
// Private: Return a {String} depicting the currently active native watchers.
print () {
if (this.useExperimentalWatcher()) {
return watcher.printWatchers()
}
return this.nativeRegistry.print()
}
@@ -590,8 +651,12 @@ class PathWatcherManager {
//
// Returns a {Promise} that resolves when all native watcher resources are disposed.
stopAllWatchers () {
if (this.useExperimentalWatcher()) {
return watcher.stopAllWatchers()
}
return Promise.all(
Array.from(this.live, watcher => watcher.stop())
Array.from(this.live, ([, w]) => w.stop())
)
}
}
@@ -636,19 +701,33 @@ class PathWatcherManager {
// ```
//
function watchPath (rootPath, options, eventCallback) {
const watcher = PathWatcherManager.instance().createWatcher(rootPath, options, eventCallback)
return watcher.getStartPromise().then(() => watcher)
return PathWatcherManager.active().createWatcher(rootPath, options, eventCallback)
}
// Private: Return a Promise that resolves when all {NativeWatcher} instances associated with a FileSystemManager
// have stopped listening. This is useful for `afterEach()` blocks in unit tests.
function stopAllWatchers () {
return PathWatcherManager.instance().stopAllWatchers()
return PathWatcherManager.active().stopAllWatchers()
}
// Private: Show the currently active native watchers.
function printWatchers () {
return PathWatcherManager.instance().print()
// Private: Show the currently active native watchers in a formatted {String}.
watchPath.printWatchers = function () {
return PathWatcherManager.active().print()
}
module.exports = {watchPath, stopAllWatchers, printWatchers}
// Private: Access the active {NativeWatcherRegistry}.
watchPath.getRegistry = function () {
return PathWatcherManager.active().getRegistry()
}
// Private: Sample usage statistics for the active watcher.
watchPath.status = function () {
return PathWatcherManager.active().status()
}
// Private: Configure @atom/watcher ("experimental") directly.
watchPath.configure = function (...args) {
return watcher.configure(...args)
}
module.exports = {watchPath, stopAllWatchers}

View File

@@ -266,14 +266,22 @@ class TextEditorComponent {
if (useScheduler === true) {
const scheduler = etch.getScheduler()
scheduler.readDocument(() => {
this.measureContentDuringUpdateSync()
const restartFrame = this.measureContentDuringUpdateSync()
scheduler.updateDocument(() => {
this.updateSyncAfterMeasuringContent()
if (restartFrame) {
this.updateSync(true)
} else {
this.updateSyncAfterMeasuringContent()
}
})
})
} else {
this.measureContentDuringUpdateSync()
this.updateSyncAfterMeasuringContent()
const restartFrame = this.measureContentDuringUpdateSync()
if (restartFrame) {
this.updateSync(false)
} else {
this.updateSyncAfterMeasuringContent()
}
}
this.updateScheduled = false
@@ -391,15 +399,16 @@ class TextEditorComponent {
this.measureHorizontalPositions()
this.updateAbsolutePositionedDecorations()
const isHorizontalScrollbarVisible = (
this.canScrollHorizontally() &&
this.getHorizontalScrollbarHeight() > 0
)
if (this.pendingAutoscroll) {
this.derivedDimensionsCache = {}
const {screenRange, options} = this.pendingAutoscroll
this.autoscrollHorizontally(screenRange, options)
const isHorizontalScrollbarVisible = (
this.canScrollHorizontally() &&
this.getHorizontalScrollbarHeight() > 0
)
if (!wasHorizontalScrollbarVisible && isHorizontalScrollbarVisible) {
this.autoscrollVertically(screenRange, options)
}
@@ -408,6 +417,8 @@ class TextEditorComponent {
this.linesToMeasure.clear()
this.measuredContent = true
return wasHorizontalScrollbarVisible !== isHorizontalScrollbarVisible
}
updateSyncAfterMeasuringContent () {
@@ -1520,15 +1531,11 @@ class TextEditorComponent {
let {wheelDeltaX, wheelDeltaY} = event
if (Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY)) {
wheelDeltaX = (Math.sign(wheelDeltaX) === 1)
? Math.max(1, wheelDeltaX * scrollSensitivity)
: Math.min(-1, wheelDeltaX * scrollSensitivity)
wheelDeltaX = wheelDeltaX * scrollSensitivity
wheelDeltaY = 0
} else {
wheelDeltaX = 0
wheelDeltaY = (Math.sign(wheelDeltaY) === 1)
? Math.max(1, wheelDeltaY * scrollSensitivity)
: Math.min(-1, wheelDeltaY * scrollSensitivity)
wheelDeltaY = wheelDeltaY * scrollSensitivity
}
if (this.getPlatform() !== 'darwin' && event.shiftKey) {

View File

@@ -205,7 +205,7 @@ class TextEditorRegistry {
// Returns a {String} scope name, or `null` if no override has been set
// for the given editor.
getGrammarOverride (editor) {
return editor.getBuffer().getLanguageMode().grammar.scopeName
return atom.grammars.getAssignedLanguageId(editor.getBuffer())
}
// Deprecated: Remove any grammar override that has been set for the given {TextEditor}.

View File

@@ -11,6 +11,7 @@ const Cursor = require('./cursor')
const Selection = require('./selection')
const NullGrammar = require('./null-grammar')
const TextMateLanguageMode = require('./text-mate-language-mode')
const ScopeDescriptor = require('./scope-descriptor')
const TextMateScopeSelector = require('first-mate').ScopeSelector
const GutterContainer = require('./gutter-container')
@@ -3655,7 +3656,10 @@ class TextEditor {
//
// Returns a {ScopeDescriptor}.
scopeDescriptorForBufferPosition (bufferPosition) {
return this.buffer.getLanguageMode().scopeDescriptorForPosition(bufferPosition)
const languageMode = this.buffer.getLanguageMode()
return languageMode.scopeDescriptorForPosition
? languageMode.scopeDescriptorForPosition(bufferPosition)
: new ScopeDescriptor({scopes: ['text']})
}
// Extended: Get the range in buffer coordinates of all tokens surrounding the

View File

@@ -1,5 +1,6 @@
const {Document} = require('tree-sitter')
const {Point, Range, Emitter} = require('atom')
const {Point, Range} = require('text-buffer')
const {Emitter, Disposable} = require('event-kit')
const ScopeDescriptor = require('./scope-descriptor')
const TokenizedLine = require('./tokenized-line')
const TextMateLanguageMode = require('./text-mate-language-mode')
@@ -279,10 +280,16 @@ class TreeSitterLanguageMode {
if (node) return new Range(node.startPosition, node.endPosition)
}
bufferRangeForScopeAtPosition (position) {
return this.getRangeForSyntaxNodeContainingRange(new Range(position, position))
}
/*
Section - Backward compatibility shims
*/
onDidTokenize (callback) { return new Disposable(() => {}) }
tokenizedLineForRow (row) {
return new TokenizedLine({
openScopes: [],
@@ -296,6 +303,7 @@ class TreeSitterLanguageMode {
}
scopeDescriptorForPosition (point) {
point = Point.fromObject(point)
const result = []
let node = this.document.rootNode.descendantForPosition(point)
@@ -413,7 +421,7 @@ class TreeSitterHighlightIterator {
this.pushCloseTag()
const {nextSibling} = this.currentNode
if (nextSibling) {
if (nextSibling && nextSibling.endIndex > this.currentIndex) {
this.currentNode = nextSibling
this.currentChildIndex++
if (this.currentIndex === nextSibling.startIndex) {
@@ -427,19 +435,8 @@ class TreeSitterHighlightIterator {
if (!this.currentNode) break
}
}
} else if (this.currentNode.startIndex < this.currentNode.endIndex) {
this.currentNode = this.currentNode.nextSibling
if (this.currentNode) {
this.currentChildIndex++
this.currentPosition = this.currentNode.startPosition
this.currentIndex = this.currentNode.startIndex
this.pushOpenTag()
this.descendLeft()
}
} else {
this.pushCloseTag()
this.currentNode = this.currentNode.parent
this.currentChildIndex = last(this.containingNodeChildIndices)
this.currentNode = this.currentNode.nextSibling
}
} while (this.closeTags.length === 0 && this.openTags.length === 0 && this.currentNode)
@@ -495,16 +492,22 @@ class TreeSitterHighlightIterator {
class TreeSitterTextBufferInput {
constructor (buffer) {
this.buffer = buffer
this.seek(0)
this.position = {row: 0, column: 0}
this.isBetweenCRLF = false
}
seek (characterIndex) {
this.position = this.buffer.positionForCharacterIndex(characterIndex)
seek (offset, position) {
this.position = position
this.isBetweenCRLF = this.position.column > this.buffer.lineLengthForRow(this.position.row)
}
read () {
const endPosition = this.buffer.clipPosition(this.position.traverse({row: 1000, column: 0}))
const text = this.buffer.getTextInRange([this.position, endPosition])
const endPosition = this.buffer.clipPosition(new Point(this.position.row + 1000, 0))
let text = this.buffer.getTextInRange([this.position, endPosition])
if (this.isBetweenCRLF) {
text = text.slice(1)
this.isBetweenCRLF = false
}
this.position = endPosition
return text
}

View File

@@ -56,10 +56,10 @@ class WorkspaceElement extends HTMLElement {
}
updateGlobalTextEditorStyleSheet () {
const styleSheetSource = `atom-text-editor {
font-size: ${this.config.get('editor.fontSize')}px;
font-family: ${this.config.get('editor.fontFamily')};
line-height: ${this.config.get('editor.lineHeight')};
const styleSheetSource = `atom-workspace {
--editor-font-size: ${this.config.get('editor.fontSize')}px;
--editor-font-family: ${this.config.get('editor.fontFamily')};
--editor-line-height: ${this.config.get('editor.lineHeight')};
}`
this.styleManager.addStyleSheet(styleSheetSource, {sourcePath: 'global-text-editor-styles', priority: -1})
}

View File

@@ -8,7 +8,7 @@
@ibeam-2x: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAAz0lEQVRIx2NgYGBY/R8I/vx5eelX3n82IJ9FxGf6tksvf/8FiTMQAcAGQMDvSwu09abffY8QYSAScNk45G198eX//yev73/4///701eh//kZSARckrNBRvz//+8+6ZohwCzjGNjdgQxkAg7B9WADeBjIBqtJCbhRA0YNoIkBSNmaPEMoNmA0FkYNoFKhapJ6FGyAH3nauaSmPfwI0v/3OukVi0CIZ+F25KrtYcx/CTIy0e+rC7R1Z4KMICVTQQ14feVXIbR695u14+Ir4gwAAD49E54wc1kWAAAAAElFTkSuQmCC');
.cursor-white() {
cursor: -webkit-image-set(@ibeam-1x 1x, @ibeam-2x 2x) 5 8, text;
cursor: -webkit-image-set(@ibeam-1x 1dppx, @ibeam-2x 2dppx) 5 8, text;
}
// Editors

View File

@@ -113,10 +113,16 @@ atom-dock {
// Promote to own layer, fixes rendering issue atom/atom#14915
will-change: transform;
&.right { left: 0; }
&.bottom { top: 0; }
&.left { right: 0; }
}
&.right {
left: 0;
}
&.bottom {
top: 0;
}
&.left {
right: 0;
}
}
// Hide the button.
&:not(.atom-dock-toggle-button-visible) {

View File

@@ -2,10 +2,17 @@
@import "octicon-utf-codes";
@import "octicon-mixins";
:root {
// Fixes specs
--editor-font-family: Menlo, Consolas, 'DejaVu Sans Mono', monospace;
}
atom-text-editor {
display: flex;
font-family: Menlo, Consolas, 'DejaVu Sans Mono', monospace;
cursor: text;
font-family: var(--editor-font-family);
font-size: var(--editor-font-size);
line-height: var(--editor-line-height);
.gutter-container {
width: min-content;

View File

@@ -81,5 +81,5 @@
// Other
@font-family: 'BlinkMacSystemFont', 'Lucida Grande', 'Segoe UI', Ubuntu, Cantarell, sans-serif;
@font-family: system-ui;
@use-custom-controls: true; // false uses native controls

23
stylelint.config.js Normal file
View File

@@ -0,0 +1,23 @@
const path = require('path');
module.exports = {
"extends": "stylelint-config-standard",
"ignoreFiles": [path.resolve(__dirname, "static", "atom.less")],
"rules": {
"color-hex-case": null, // TODO: enable?
"max-empty-lines": null, // TODO: enable?
"selector-type-no-unknown": null,
"function-comma-space-after": null, // TODO: enable?
"font-family-no-missing-generic-family-keyword": null, // needed for octicons (no sensible fallback)
"declaration-empty-line-before": null, // TODO: enable?
"declaration-block-trailing-semicolon": null, // TODO: enable
"no-descending-specificity": null,
"number-leading-zero": null, // TODO: enable?
"no-duplicate-selectors": null,
"selector-pseudo-element-colon-notation": null, // TODO: enable?
"selector-list-comma-newline-after": null, // TODO: enable?
"rule-empty-line-before": null, // TODO: enable?
"at-rule-empty-line-before": null, // TODO: enable?
"font-family-no-duplicate-names": null, // TODO: enable?
}
}