diff --git a/README.md b/README.md index 0c10b1352..b0d8a6504 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/docs/focus/2018-02-12.md b/docs/focus/2018-02-12.md new file mode 100644 index 000000000..345543a9b --- /dev/null +++ b/docs/focus/2018-02-12.md @@ -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. diff --git a/docs/focus/2018-02-19.md b/docs/focus/2018-02-19.md new file mode 100644 index 000000000..9bc89628a --- /dev/null +++ b/docs/focus/2018-02-19.md @@ -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. diff --git a/package.json b/package.json index 990d20f7d..2ac3c966b 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/script/lib/generate-startup-snapshot.js b/script/lib/generate-startup-snapshot.js index 1078ab20e..8c33e5494 100644 --- a/script/lib/generate-startup-snapshot.js +++ b/script/lib/generate-startup-snapshot.js @@ -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') || diff --git a/script/lib/lint-less-paths.js b/script/lib/lint-less-paths.js index 84e6aa3ae..c46d847c3 100644 --- a/script/lib/lint-less-paths.js +++ b/script/lib/lint-less-paths.js @@ -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 + }) } diff --git a/script/package.json b/script/package.json index afc034df3..5946496ef 100644 --- a/script/package.json +++ b/script/package.json @@ -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" } diff --git a/spec/config-spec.js b/spec/config-spec.js index ba41e0891..67ade3434 100644 --- a/spec/config-spec.js +++ b/spec/config-spec.js @@ -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) diff --git a/spec/context-menu-manager-spec.coffee b/spec/context-menu-manager-spec.coffee index 035d56a61..70ab4b3e7 100644 --- a/spec/context-menu-manager-spec.coffee +++ b/spec/context-menu-manager-spec.coffee @@ -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"] + }] + }]) diff --git a/spec/grammar-registry-spec.js b/spec/grammar-registry-spec.js index e6d815f8d..93f83eb26 100644 --- a/spec/grammar-registry-spec.js +++ b/spec/grammar-registry-spec.js @@ -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) }) }) }) diff --git a/spec/menu-sort-helpers-spec.js b/spec/menu-sort-helpers-spec.js new file mode 100644 index 000000000..86f00b37e --- /dev/null +++ b/spec/menu-sort-helpers-spec.js @@ -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' } + ]) + }) + }) +}) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index de2b68bb5..6947de229 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -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}) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index a9aa80cd1..69be6be32 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -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') diff --git a/spec/tree-sitter-language-mode-spec.js b/spec/tree-sitter-language-mode-spec.js index 58a098dca..a788fac47 100644 --- a/spec/tree-sitter-language-mode-spec.js +++ b/spec/tree-sitter-language-mode-spec.js @@ -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', diff --git a/src/application-delegate.js b/src/application-delegate.js index f0be70a11..fcf8441b6 100644 --- a/src/application-delegate.js +++ b/src/application-delegate.js @@ -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) diff --git a/src/config-schema.js b/src/config-schema.js index 358cf8717..97a6d16f3 100644 --- a/src/config-schema.js +++ b/src/config-schema.js @@ -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: { diff --git a/src/config.js b/src/config.js index 27a50b9bb..5b321b59e 100644 --- a/src/config.js +++ b/src/config.js @@ -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, diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index 10a7a3bdb..9cff5497b 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -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 diff --git a/src/grammar-registry.js b/src/grammar-registry.js index b316bdbb0..20757fb0b 100644 --- a/src/grammar-registry.js +++ b/src/grammar-registry.js @@ -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) }) } diff --git a/src/main-process/atom-application.js b/src/main-process/atom-application.js index 38316497a..2907e886f 100644 --- a/src/main-process/atom-application.js +++ b/src/main-process/atom-application.js @@ -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. diff --git a/src/main-process/atom-window.js b/src/main-process/atom-window.js index 38fb11292..aa085d04a 100644 --- a/src/main-process/atom-window.js +++ b/src/main-process/atom-window.js @@ -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 () { diff --git a/src/menu-helpers.js b/src/menu-helpers.js index 398feb40e..12598764e 100644 --- a/src/menu-helpers.js +++ b/src/menu-helpers.js @@ -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)) diff --git a/src/menu-sort-helpers.js b/src/menu-sort-helpers.js new file mode 100644 index 000000000..259f8321e --- /dev/null +++ b/src/menu-sort-helpers.js @@ -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} diff --git a/src/package.js b/src/package.js index 8d5cbc3ca..bbcb0061f 100644 --- a/src/package.js +++ b/src/package.js @@ -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() } diff --git a/src/pane-container-element.coffee b/src/pane-container-element.coffee deleted file mode 100644 index 78e2fbad3..000000000 --- a/src/pane-container-element.coffee +++ /dev/null @@ -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 diff --git a/src/pane-container-element.js b/src/pane-container-element.js new file mode 100644 index 000000000..7a2a88463 --- /dev/null +++ b/src/pane-container-element.js @@ -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 +}) diff --git a/src/pane-element.coffee b/src/pane-element.coffee deleted file mode 100644 index d68b3b834..000000000 --- a/src/pane-element.coffee +++ /dev/null @@ -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 diff --git a/src/pane-element.js b/src/pane-element.js new file mode 100644 index 000000000..2c9f4eeef --- /dev/null +++ b/src/pane-element.js @@ -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 +}) diff --git a/src/path-watcher.js b/src/path-watcher.js index ff7e8fd56..72a798d06 100644 --- a/src/path-watcher.js +++ b/src/path-watcher.js @@ -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} diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 5d09ff50f..38851d88d 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -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) { diff --git a/src/text-editor-registry.js b/src/text-editor-registry.js index 9b802f5f8..132b24ffb 100644 --- a/src/text-editor-registry.js +++ b/src/text-editor-registry.js @@ -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}. diff --git a/src/text-editor.js b/src/text-editor.js index 32d3102c2..9bfa8ff3e 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -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 diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js index 945af9331..0d2fab8cf 100644 --- a/src/tree-sitter-language-mode.js +++ b/src/tree-sitter-language-mode.js @@ -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 } diff --git a/src/workspace-element.js b/src/workspace-element.js index c9a30af85..cd5c1b746 100644 --- a/src/workspace-element.js +++ b/src/workspace-element.js @@ -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}) } diff --git a/static/cursors.less b/static/cursors.less index 5cbfadef6..843dab2c6 100644 --- a/static/cursors.less +++ b/static/cursors.less @@ -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 diff --git a/static/docks.less b/static/docks.less index 283402e09..ccbb4e903 100644 --- a/static/docks.less +++ b/static/docks.less @@ -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) { diff --git a/static/text-editor.less b/static/text-editor.less index 21cba8482..99f198512 100644 --- a/static/text-editor.less +++ b/static/text-editor.less @@ -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; diff --git a/static/variables/ui-variables.less b/static/variables/ui-variables.less index 8ef4d48e7..7bea17e72 100644 --- a/static/variables/ui-variables.less +++ b/static/variables/ui-variables.less @@ -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 diff --git a/stylelint.config.js b/stylelint.config.js new file mode 100644 index 000000000..49caab46d --- /dev/null +++ b/stylelint.config.js @@ -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? + } +}