mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-09-20 15:37:46 +03:00
Merge branch 'master' into wl-build-on-node-7
This commit is contained in:
commit
69d12d917b
@ -1 +1 @@
|
||||
2.7.12
|
||||
2.7.13
|
||||
|
@ -19,7 +19,9 @@ install:
|
||||
- npm install -g npm
|
||||
- script/build --create-debian-package --create-rpm-package --compress-artifacts
|
||||
|
||||
script: script/test
|
||||
script:
|
||||
- script/lint
|
||||
- script/test
|
||||
|
||||
cache:
|
||||
directories:
|
||||
|
@ -27,6 +27,7 @@ build_script:
|
||||
- script\build.cmd --code-sign --create-windows-installer --compress-artifacts
|
||||
|
||||
test_script:
|
||||
- script\lint.cmd
|
||||
- script\test.cmd
|
||||
|
||||
deploy: off
|
||||
|
@ -26,7 +26,7 @@ export default async function ({test}) {
|
||||
console.log(text.length / 1024)
|
||||
|
||||
let t0 = window.performance.now()
|
||||
const buffer = new TextBuffer(text)
|
||||
const buffer = new TextBuffer({text})
|
||||
const editor = new TextEditor({buffer, largeFileMode: true})
|
||||
atom.workspace.getActivePane().activateItem(editor)
|
||||
let t1 = window.performance.now()
|
||||
|
97
benchmarks/text-editor-long-lines.bench.js
Normal file
97
benchmarks/text-editor-long-lines.bench.js
Normal file
@ -0,0 +1,97 @@
|
||||
/** @babel */
|
||||
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import {TextEditor, TextBuffer} from 'atom'
|
||||
|
||||
const SIZES_IN_KB = [
|
||||
512,
|
||||
1024,
|
||||
2048
|
||||
]
|
||||
const REPEATED_TEXT = fs.readFileSync(path.join(__dirname, '..', 'spec', 'fixtures', 'sample.js'), 'utf8').replace(/\n/g, '')
|
||||
const TEXT = REPEATED_TEXT.repeat(Math.ceil(SIZES_IN_KB[SIZES_IN_KB.length - 1] * 1024 / REPEATED_TEXT.length))
|
||||
|
||||
export default async function ({test}) {
|
||||
const data = []
|
||||
|
||||
const workspaceElement = atom.views.getView(atom.workspace)
|
||||
document.body.appendChild(workspaceElement)
|
||||
|
||||
atom.packages.loadPackages()
|
||||
await atom.packages.activate()
|
||||
|
||||
console.log(atom.getLoadSettings().resourcePath);
|
||||
|
||||
for (let pane of atom.workspace.getPanes()) {
|
||||
pane.destroy()
|
||||
}
|
||||
|
||||
for (const sizeInKB of SIZES_IN_KB) {
|
||||
const text = TEXT.slice(0, sizeInKB * 1024)
|
||||
console.log(text.length / 1024)
|
||||
|
||||
let t0 = window.performance.now()
|
||||
const buffer = new TextBuffer({text})
|
||||
const editor = new TextEditor({buffer, largeFileMode: true})
|
||||
editor.setGrammar(atom.grammars.grammarForScopeName('source.js'))
|
||||
atom.workspace.getActivePane().activateItem(editor)
|
||||
let t1 = window.performance.now()
|
||||
|
||||
data.push({
|
||||
name: 'Opening a large single-line file',
|
||||
x: sizeInKB,
|
||||
duration: t1 - t0
|
||||
})
|
||||
|
||||
const tickDurations = []
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await timeout(50)
|
||||
t0 = window.performance.now()
|
||||
await timeout(0)
|
||||
t1 = window.performance.now()
|
||||
tickDurations[i] = t1 - t0
|
||||
}
|
||||
|
||||
data.push({
|
||||
name: 'Max time event loop was blocked after opening a large single-line file',
|
||||
x: sizeInKB,
|
||||
duration: Math.max(...tickDurations)
|
||||
})
|
||||
|
||||
t0 = window.performance.now()
|
||||
editor.setCursorScreenPosition(editor.element.screenPositionForPixelPosition({
|
||||
top: 100,
|
||||
left: 30
|
||||
}))
|
||||
t1 = window.performance.now()
|
||||
|
||||
data.push({
|
||||
name: 'Clicking the editor after opening a large single-line file',
|
||||
x: sizeInKB,
|
||||
duration: t1 - t0
|
||||
})
|
||||
|
||||
t0 = window.performance.now()
|
||||
editor.element.setScrollTop(editor.element.getScrollTop() + 100)
|
||||
t1 = window.performance.now()
|
||||
|
||||
data.push({
|
||||
name: 'Scrolling down after opening a large single-line file',
|
||||
x: sizeInKB,
|
||||
duration: t1 - t0
|
||||
})
|
||||
|
||||
editor.destroy()
|
||||
buffer.destroy()
|
||||
await timeout(10000)
|
||||
}
|
||||
|
||||
workspaceElement.remove()
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
function timeout (duration) {
|
||||
return new Promise((resolve) => setTimeout(resolve, duration))
|
||||
}
|
@ -7,7 +7,7 @@ Ubuntu LTS 12.04 64-bit is the recommended platform.
|
||||
* OS with 64-bit or 32-bit architecture
|
||||
* C++11 toolchain
|
||||
* Git
|
||||
* Node.js 4.4.x or later (we recommend installing it via [nvm](https://github.com/creationix/nvm))
|
||||
* Node.js 6.x (we recommend installing it via [nvm](https://github.com/creationix/nvm))
|
||||
* npm 3.10.x or later (run `npm install -g npm`)
|
||||
* Ensure node-gyp uses python2 (run `npm config set python /usr/bin/python2 -g`, use `sudo` if you didn't install node via nvm)
|
||||
* Development headers for [GNOME Keyring](https://wiki.gnome.org/Projects/GnomeKeyring).
|
||||
|
@ -3,7 +3,7 @@
|
||||
## Requirements
|
||||
|
||||
* macOS 10.8 or later
|
||||
* Node.js 4.4.x or later (we recommend installing it via [nvm](https://github.com/creationix/nvm))
|
||||
* Node.js 6.x (we recommend installing it via [nvm](https://github.com/creationix/nvm))
|
||||
* npm 3.10.x or later (run `npm install -g npm`)
|
||||
* Command Line Tools for [Xcode](https://developer.apple.com/xcode/downloads/) (run `xcode-select --install` to install)
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
## Requirements
|
||||
|
||||
* Node.js 4.4.x or later (the architecture of node available to the build system will determine whether you build 32-bit or 64-bit Atom)
|
||||
* Node.js 6.x (the architecture of node available to the build system will determine whether you build 32-bit or 64-bit Atom)
|
||||
* Python v2.7.x
|
||||
* The python.exe must be available at `%SystemDrive%\Python27\python.exe`. If it is installed elsewhere create a symbolic link to the directory containing the python.exe using: `mklink /d %SystemDrive%\Python27 D:\elsewhere\Python27`
|
||||
* 7zip (7z.exe available from the command line) - for creating distribution zip files
|
||||
|
102
package.json
102
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "atom",
|
||||
"productName": "Atom",
|
||||
"version": "1.15.0-dev",
|
||||
"version": "1.16.0-dev",
|
||||
"description": "A hackable text editor for the 21st Century.",
|
||||
"main": "./src/main-process/main.js",
|
||||
"repository": {
|
||||
@ -15,10 +15,21 @@
|
||||
"electronVersion": "1.3.13",
|
||||
"dependencies": {
|
||||
"async": "0.2.6",
|
||||
"atom-keymap": "7.1.19",
|
||||
"atom-select-list": "0.0.6",
|
||||
"atom-keymap": "7.1.20",
|
||||
"atom-select-list": "0.0.12",
|
||||
"atom-ui": "0.4.1",
|
||||
"babel-core": "5.8.38",
|
||||
"babel-core": "6.22.1",
|
||||
"babel-plugin-add-module-exports": "0.2.1",
|
||||
"babel-plugin-transform-async-to-generator": "6.22.0",
|
||||
"babel-plugin-transform-class-properties": "6.23.0",
|
||||
"babel-plugin-transform-decorators-legacy": "1.3.4",
|
||||
"babel-plugin-transform-do-expressions": "6.22.0",
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "6.23.0",
|
||||
"babel-plugin-transform-export-extensions": "6.22.0",
|
||||
"babel-plugin-transform-flow-strip-types": "6.22.0",
|
||||
"babel-plugin-transform-function-bind": "6.22.0",
|
||||
"babel-plugin-transform-object-rest-spread": "6.23.0",
|
||||
"babel-plugin-transform-react-jsx": "6.23.0",
|
||||
"cached-run-in-this-context": "0.4.1",
|
||||
"chai": "3.5.0",
|
||||
"chart.js": "^2.3.0",
|
||||
@ -65,7 +76,7 @@
|
||||
"sinon": "1.17.4",
|
||||
"source-map-support": "^0.3.2",
|
||||
"temp": "0.8.1",
|
||||
"text-buffer": "10.2.5",
|
||||
"text-buffer": "10.3.12",
|
||||
"typescript-simple": "1.0.0",
|
||||
"underscore-plus": "^1.6.6",
|
||||
"winreg": "^1.2.1",
|
||||
@ -82,85 +93,86 @@
|
||||
"one-light-ui": "1.9.1",
|
||||
"one-dark-syntax": "1.7.1",
|
||||
"one-light-syntax": "1.7.1",
|
||||
"solarized-dark-syntax": "1.1.1",
|
||||
"solarized-light-syntax": "1.1.1",
|
||||
"solarized-dark-syntax": "1.1.2",
|
||||
"solarized-light-syntax": "1.1.2",
|
||||
"about": "1.7.2",
|
||||
"archive-view": "0.62.2",
|
||||
"autocomplete-atom-api": "0.10.0",
|
||||
"autocomplete-css": "0.14.2",
|
||||
"autocomplete-css": "0.15.0",
|
||||
"autocomplete-html": "0.7.2",
|
||||
"autocomplete-plus": "2.34.2",
|
||||
"autocomplete-snippets": "1.11.0",
|
||||
"autoflow": "0.29.0",
|
||||
"autosave": "0.24.0",
|
||||
"background-tips": "0.26.1",
|
||||
"bookmarks": "0.44.0",
|
||||
"bracket-matcher": "0.85.2",
|
||||
"command-palette": "0.40.0",
|
||||
"deprecation-cop": "0.56.1",
|
||||
"bookmarks": "0.44.1",
|
||||
"bracket-matcher": "0.85.3",
|
||||
"command-palette": "0.40.2",
|
||||
"dalek": "0.2.0",
|
||||
"deprecation-cop": "0.56.2",
|
||||
"dev-live-reload": "0.47.0",
|
||||
"encoding-selector": "0.23.0",
|
||||
"exception-reporting": "0.40.2",
|
||||
"find-and-replace": "0.206.0",
|
||||
"encoding-selector": "0.23.1",
|
||||
"exception-reporting": "0.41.1",
|
||||
"find-and-replace": "0.206.3",
|
||||
"fuzzy-finder": "1.4.1",
|
||||
"git-diff": "1.3.0",
|
||||
"git-diff": "1.3.1",
|
||||
"go-to-line": "0.32.0",
|
||||
"grammar-selector": "0.49.0",
|
||||
"image-view": "0.60.0",
|
||||
"incompatible-packages": "0.26.1",
|
||||
"keybinding-resolver": "0.36.0",
|
||||
"line-ending-selector": "0.6.0",
|
||||
"grammar-selector": "0.49.2",
|
||||
"image-view": "0.61.0",
|
||||
"incompatible-packages": "0.27.0",
|
||||
"keybinding-resolver": "0.36.1",
|
||||
"line-ending-selector": "0.6.1",
|
||||
"link": "0.31.2",
|
||||
"markdown-preview": "0.159.3",
|
||||
"metrics": "1.1.3",
|
||||
"markdown-preview": "0.159.7",
|
||||
"metrics": "1.2.1",
|
||||
"notifications": "0.66.2",
|
||||
"open-on-github": "1.2.1",
|
||||
"package-generator": "1.1.0",
|
||||
"settings-view": "0.246.0",
|
||||
"settings-view": "0.247.0",
|
||||
"snippets": "1.0.5",
|
||||
"spell-check": "0.70.2",
|
||||
"spell-check": "0.71.0",
|
||||
"status-bar": "1.8.1",
|
||||
"styleguide": "0.49.0",
|
||||
"styleguide": "0.49.2",
|
||||
"symbols-view": "0.114.0",
|
||||
"tabs": "0.104.1",
|
||||
"timecop": "0.34.0",
|
||||
"tree-view": "0.213.2",
|
||||
"timecop": "0.36.0",
|
||||
"tree-view": "0.214.1",
|
||||
"update-package-dependencies": "0.10.0",
|
||||
"welcome": "0.36.0",
|
||||
"whitespace": "0.36.1",
|
||||
"wrap-guide": "0.39.0",
|
||||
"language-c": "0.54.1",
|
||||
"language-clojure": "0.22.1",
|
||||
"language-coffee-script": "0.48.2",
|
||||
"language-csharp": "0.14.1",
|
||||
"welcome": "0.36.1",
|
||||
"whitespace": "0.36.2",
|
||||
"wrap-guide": "0.39.1",
|
||||
"language-c": "0.56.0",
|
||||
"language-clojure": "0.22.2",
|
||||
"language-coffee-script": "0.48.4",
|
||||
"language-csharp": "0.14.2",
|
||||
"language-css": "0.42.0",
|
||||
"language-gfm": "0.88.0",
|
||||
"language-git": "0.19.0",
|
||||
"language-go": "0.43.1",
|
||||
"language-html": "0.47.1",
|
||||
"language-html": "0.47.2",
|
||||
"language-hyperlink": "0.16.1",
|
||||
"language-java": "0.25.0",
|
||||
"language-javascript": "0.125.1",
|
||||
"language-java": "0.26.0",
|
||||
"language-javascript": "0.126.0",
|
||||
"language-json": "0.18.3",
|
||||
"language-less": "0.30.1",
|
||||
"language-make": "0.22.3",
|
||||
"language-mustache": "0.13.1",
|
||||
"language-objective-c": "0.15.1",
|
||||
"language-perl": "0.37.0",
|
||||
"language-php": "0.37.3",
|
||||
"language-php": "0.37.4",
|
||||
"language-property-list": "0.9.0",
|
||||
"language-python": "0.45.1",
|
||||
"language-ruby": "0.70.4",
|
||||
"language-ruby-on-rails": "0.25.1",
|
||||
"language-python": "0.45.2",
|
||||
"language-ruby": "0.70.5",
|
||||
"language-ruby-on-rails": "0.25.2",
|
||||
"language-sass": "0.57.1",
|
||||
"language-shellscript": "0.25.0",
|
||||
"language-source": "0.9.0",
|
||||
"language-sql": "0.25.2",
|
||||
"language-sql": "0.25.3",
|
||||
"language-text": "0.7.1",
|
||||
"language-todo": "0.29.1",
|
||||
"language-toml": "0.18.1",
|
||||
"language-xml": "0.34.15",
|
||||
"language-yaml": "0.27.2"
|
||||
"language-xml": "0.34.16",
|
||||
"language-yaml": "0.28.0"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -1,3 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
"$(dirname "$0")/../app/apm/apm.sh" "$@"
|
||||
"$(dirname "$0")/../app/apm/bin/apm" "$@"
|
||||
|
@ -5,16 +5,17 @@ const path = require('path')
|
||||
const spawnSync = require('./spawn-sync')
|
||||
|
||||
module.exports = function (packagedAppPath) {
|
||||
if (!process.env.ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL) {
|
||||
if (!process.env.ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL && !process.env.ATOM_MAC_CODE_SIGNING_CERT_PATH) {
|
||||
console.log('Skipping code signing because the ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL environment variable is not defined'.gray)
|
||||
return
|
||||
}
|
||||
|
||||
// This declaration is outside of the try block because it is also used for the finally block
|
||||
const certPath = path.join(os.tmpdir(), 'mac.p12')
|
||||
try {
|
||||
let certPath = process.env.ATOM_MAC_CODE_SIGNING_CERT_PATH;
|
||||
if (!certPath) {
|
||||
certPath = path.join(os.tmpdir(), 'mac.p12')
|
||||
downloadFileFromGithub(process.env.ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL, certPath)
|
||||
|
||||
}
|
||||
try {
|
||||
console.log(`Unlocking keychain ${process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN}`)
|
||||
const unlockArgs = ['unlock-keychain']
|
||||
// For signing on local workstations, password could be entered interactively
|
||||
@ -32,6 +33,18 @@ module.exports = function (packagedAppPath) {
|
||||
'-T', '/usr/bin/codesign'
|
||||
])
|
||||
|
||||
|
||||
console.log('Running incantation to suppress dialog when signing on macOS Sierra')
|
||||
try {
|
||||
spawnSync('security', [
|
||||
'set-key-partition-list', '-S', 'apple-tool:,apple:', '-s',
|
||||
'-k', process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN_PASSWORD,
|
||||
process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN
|
||||
])
|
||||
} catch (e) {
|
||||
console.log('Incantation failed... maybe this isn\'t Sierra?');
|
||||
}
|
||||
|
||||
console.log(`Code-signing application at ${packagedAppPath}`)
|
||||
spawnSync('codesign', [
|
||||
'--deep', '--force', '--verbose',
|
||||
@ -39,7 +52,9 @@ module.exports = function (packagedAppPath) {
|
||||
'--sign', 'Developer ID Application: GitHub', packagedAppPath
|
||||
], {stdio: 'inherit'})
|
||||
} finally {
|
||||
console.log(`Deleting certificate at ${certPath}`)
|
||||
fs.removeSync(certPath)
|
||||
if (!process.env.ATOM_MAC_CODE_SIGNING_CERT_PATH) {
|
||||
console.log(`Deleting certificate at ${certPath}`)
|
||||
fs.removeSync(certPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,15 +18,19 @@ module.exports = function (packagedAppPath, codeSign) {
|
||||
iconUrl: `https://raw.githubusercontent.com/atom/atom/master/resources/app-icons/${CONFIG.channel}/atom.ico`,
|
||||
loadingGif: path.join(CONFIG.repositoryRootPath, 'resources', 'win', 'loading.gif'),
|
||||
outputDirectory: CONFIG.buildOutputPath,
|
||||
remoteReleases: `https://atom.io/api/updates${archSuffix}`,
|
||||
remoteReleases: `https://atom.io/api/updates${archSuffix}?version=${CONFIG.appMetadata.version}`,
|
||||
setupIcon: path.join(CONFIG.repositoryRootPath, 'resources', 'app-icons', CONFIG.channel, 'atom.ico')
|
||||
}
|
||||
|
||||
const certPath = path.join(os.tmpdir(), 'win.p12')
|
||||
const signing = codeSign && process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL
|
||||
const signing = codeSign && (process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL || process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH)
|
||||
let certPath = process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH;
|
||||
|
||||
if (signing) {
|
||||
downloadFileFromGithub(process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL, certPath)
|
||||
if (!certPath) {
|
||||
certPath = path.join(os.tmpdir(), 'win.p12')
|
||||
downloadFileFromGithub(process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL, certPath)
|
||||
}
|
||||
|
||||
var signParams = []
|
||||
signParams.push(`/f ${certPath}`) // Signing cert file
|
||||
signParams.push(`/p ${process.env.ATOM_WIN_CODE_SIGNING_CERT_PASSWORD}`) // Signing cert password
|
||||
@ -39,7 +43,7 @@ module.exports = function (packagedAppPath, codeSign) {
|
||||
}
|
||||
|
||||
const cleanUp = function () {
|
||||
if (fs.existsSync(certPath)) {
|
||||
if (fs.existsSync(certPath) && !process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH) {
|
||||
console.log(`Deleting certificate at ${certPath}`)
|
||||
fs.removeSync(certPath)
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
"description": "Atom build scripts",
|
||||
"dependencies": {
|
||||
"async": "2.0.1",
|
||||
"babel-core": "5.8.38",
|
||||
"coffeelint": "1.15.7",
|
||||
"colors": "1.1.2",
|
||||
"csslint": "1.0.2",
|
||||
|
@ -142,6 +142,11 @@ describe "AtomEnvironment", ->
|
||||
atom.assert(false, "a == b", (e) -> error = e)
|
||||
expect(error).toBe errors[0]
|
||||
|
||||
describe "if passed metadata", ->
|
||||
it "assigns the metadata on the assertion failure's error object", ->
|
||||
atom.assert(false, "a == b", {foo: 'bar'})
|
||||
expect(errors[0].metadata).toEqual {foo: 'bar'}
|
||||
|
||||
describe "if the condition is true", ->
|
||||
it "does nothing", ->
|
||||
result = atom.assert(true, "a == b")
|
||||
|
@ -1,4 +1,5 @@
|
||||
path = require 'path'
|
||||
process = require 'process'
|
||||
_ = require 'underscore-plus'
|
||||
grim = require 'grim'
|
||||
marked = require 'marked'
|
||||
@ -20,12 +21,15 @@ formatStackTrace = (spec, message='', stackTrace) ->
|
||||
lines.shift() if message.trim() is errorMatch?[1]?.trim()
|
||||
|
||||
for line, index in lines
|
||||
# Remove prefix of lines matching: at [object Object].<anonymous> (path:1:2)
|
||||
prefixMatch = line.match(/at \[object Object\]\.<anonymous> \(([^)]+)\)/)
|
||||
# Remove prefix of lines matching: at .<anonymous> (path:1:2)
|
||||
prefixMatch = line.match(/at \.<anonymous> \(([^)]+)\)/)
|
||||
line = "at #{prefixMatch[1]}" if prefixMatch
|
||||
|
||||
# Relativize locations to spec directory
|
||||
lines[index] = line.replace("at #{spec.specDirectory}#{path.sep}", 'at ')
|
||||
if process.platform is 'win32'
|
||||
line = line.replace('file:///', '').replace(///#{path.posix.sep}///g, path.win32.sep)
|
||||
line = line.replace("at #{spec.specDirectory}#{path.sep}", 'at ')
|
||||
lines[index] = line.replace("(#{spec.specDirectory}#{path.sep}", '(') # at step (path:1:2)
|
||||
|
||||
lines = lines.map (line) -> line.trim()
|
||||
lines.join('\n').trim()
|
||||
|
@ -1,13 +1,14 @@
|
||||
DecorationManager = require '../src/decoration-manager'
|
||||
|
||||
describe "DecorationManager", ->
|
||||
[decorationManager, buffer, defaultMarkerLayer] = []
|
||||
[decorationManager, buffer, displayLayer, markerLayer1, markerLayer2] = []
|
||||
|
||||
beforeEach ->
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
displayLayer = buffer.addDisplayLayer()
|
||||
defaultMarkerLayer = displayLayer.addMarkerLayer()
|
||||
decorationManager = new DecorationManager(displayLayer, defaultMarkerLayer)
|
||||
markerLayer1 = displayLayer.addMarkerLayer()
|
||||
markerLayer2 = displayLayer.addMarkerLayer()
|
||||
decorationManager = new DecorationManager(displayLayer)
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-javascript')
|
||||
@ -17,38 +18,53 @@ describe "DecorationManager", ->
|
||||
buffer.release()
|
||||
|
||||
describe "decorations", ->
|
||||
[marker, decoration, decorationProperties] = []
|
||||
[layer1Marker, layer2Marker, layer1MarkerDecoration, layer2MarkerDecoration, decorationProperties] = []
|
||||
beforeEach ->
|
||||
marker = defaultMarkerLayer.markBufferRange([[2, 13], [3, 15]])
|
||||
layer1Marker = markerLayer1.markBufferRange([[2, 13], [3, 15]])
|
||||
decorationProperties = {type: 'line-number', class: 'one'}
|
||||
decoration = decorationManager.decorateMarker(marker, decorationProperties)
|
||||
layer1MarkerDecoration = decorationManager.decorateMarker(layer1Marker, decorationProperties)
|
||||
layer2Marker = markerLayer2.markBufferRange([[2, 13], [3, 15]])
|
||||
layer2MarkerDecoration = decorationManager.decorateMarker(layer2Marker, decorationProperties)
|
||||
|
||||
it "can add decorations associated with markers and remove them", ->
|
||||
expect(decoration).toBeDefined()
|
||||
expect(decoration.getProperties()).toBe decorationProperties
|
||||
expect(decorationManager.decorationForId(decoration.id)).toBe decoration
|
||||
expect(decorationManager.decorationsForScreenRowRange(2, 3)[marker.id][0]).toBe decoration
|
||||
expect(layer1MarkerDecoration).toBeDefined()
|
||||
expect(layer1MarkerDecoration.getProperties()).toBe decorationProperties
|
||||
expect(decorationManager.decorationForId(layer1MarkerDecoration.id)).toBe layer1MarkerDecoration
|
||||
expect(decorationManager.decorationsForScreenRowRange(2, 3)).toEqual {
|
||||
"#{layer1Marker.id}": [layer1MarkerDecoration],
|
||||
"#{layer2Marker.id}": [layer2MarkerDecoration]
|
||||
}
|
||||
|
||||
decoration.destroy()
|
||||
expect(decorationManager.decorationsForScreenRowRange(2, 3)[marker.id]).not.toBeDefined()
|
||||
expect(decorationManager.decorationForId(decoration.id)).not.toBeDefined()
|
||||
layer1MarkerDecoration.destroy()
|
||||
expect(decorationManager.decorationsForScreenRowRange(2, 3)[layer1Marker.id]).not.toBeDefined()
|
||||
expect(decorationManager.decorationForId(layer1MarkerDecoration.id)).not.toBeDefined()
|
||||
layer2MarkerDecoration.destroy()
|
||||
expect(decorationManager.decorationsForScreenRowRange(2, 3)[layer2Marker.id]).not.toBeDefined()
|
||||
expect(decorationManager.decorationForId(layer2MarkerDecoration.id)).not.toBeDefined()
|
||||
|
||||
it "will not fail if the decoration is removed twice", ->
|
||||
decoration.destroy()
|
||||
decoration.destroy()
|
||||
expect(decorationManager.decorationForId(decoration.id)).not.toBeDefined()
|
||||
layer1MarkerDecoration.destroy()
|
||||
layer1MarkerDecoration.destroy()
|
||||
expect(decorationManager.decorationForId(layer1MarkerDecoration.id)).not.toBeDefined()
|
||||
|
||||
it "does not allow destroyed markers to be decorated", ->
|
||||
marker.destroy()
|
||||
layer1Marker.destroy()
|
||||
expect(->
|
||||
decorationManager.decorateMarker(marker, {type: 'overlay', item: document.createElement('div')})
|
||||
decorationManager.decorateMarker(layer1Marker, {type: 'overlay', item: document.createElement('div')})
|
||||
).toThrow("Cannot decorate a destroyed marker")
|
||||
expect(decorationManager.getOverlayDecorations()).toEqual []
|
||||
|
||||
it "does not allow destroyed marker layers to be decorated", ->
|
||||
layer = displayLayer.addMarkerLayer()
|
||||
layer.destroy()
|
||||
expect(->
|
||||
decorationManager.decorateMarkerLayer(layer, {type: 'highlight'})
|
||||
).toThrow("Cannot decorate a destroyed marker layer")
|
||||
|
||||
describe "when a decoration is updated via Decoration::update()", ->
|
||||
it "emits an 'updated' event containing the new and old params", ->
|
||||
decoration.onDidChangeProperties updatedSpy = jasmine.createSpy()
|
||||
decoration.setProperties type: 'line-number', class: 'two'
|
||||
layer1MarkerDecoration.onDidChangeProperties updatedSpy = jasmine.createSpy()
|
||||
layer1MarkerDecoration.setProperties type: 'line-number', class: 'two'
|
||||
|
||||
{oldProperties, newProperties} = updatedSpy.mostRecentCall.args[0]
|
||||
expect(oldProperties).toEqual decorationProperties
|
||||
@ -56,29 +72,29 @@ describe "DecorationManager", ->
|
||||
|
||||
describe "::getDecorations(properties)", ->
|
||||
it "returns decorations matching the given optional properties", ->
|
||||
expect(decorationManager.getDecorations()).toEqual [decoration]
|
||||
expect(decorationManager.getDecorations()).toEqual [layer1MarkerDecoration, layer2MarkerDecoration]
|
||||
expect(decorationManager.getDecorations(class: 'two').length).toEqual 0
|
||||
expect(decorationManager.getDecorations(class: 'one').length).toEqual 1
|
||||
expect(decorationManager.getDecorations(class: 'one').length).toEqual 2
|
||||
|
||||
describe "::decorateMarker", ->
|
||||
describe "when decorating gutters", ->
|
||||
[marker] = []
|
||||
[layer1Marker] = []
|
||||
|
||||
beforeEach ->
|
||||
marker = defaultMarkerLayer.markBufferRange([[1, 0], [1, 0]])
|
||||
layer1Marker = markerLayer1.markBufferRange([[1, 0], [1, 0]])
|
||||
|
||||
it "creates a decoration that is both of 'line-number' and 'gutter' type when called with the 'line-number' type", ->
|
||||
decorationProperties = {type: 'line-number', class: 'one'}
|
||||
decoration = decorationManager.decorateMarker(marker, decorationProperties)
|
||||
expect(decoration.isType('line-number')).toBe true
|
||||
expect(decoration.isType('gutter')).toBe true
|
||||
expect(decoration.getProperties().gutterName).toBe 'line-number'
|
||||
expect(decoration.getProperties().class).toBe 'one'
|
||||
layer1MarkerDecoration = decorationManager.decorateMarker(layer1Marker, decorationProperties)
|
||||
expect(layer1MarkerDecoration.isType('line-number')).toBe true
|
||||
expect(layer1MarkerDecoration.isType('gutter')).toBe true
|
||||
expect(layer1MarkerDecoration.getProperties().gutterName).toBe 'line-number'
|
||||
expect(layer1MarkerDecoration.getProperties().class).toBe 'one'
|
||||
|
||||
it "creates a decoration that is only of 'gutter' type if called with the 'gutter' type and a 'gutterName'", ->
|
||||
decorationProperties = {type: 'gutter', gutterName: 'test-gutter', class: 'one'}
|
||||
decoration = decorationManager.decorateMarker(marker, decorationProperties)
|
||||
expect(decoration.isType('gutter')).toBe true
|
||||
expect(decoration.isType('line-number')).toBe false
|
||||
expect(decoration.getProperties().gutterName).toBe 'test-gutter'
|
||||
expect(decoration.getProperties().class).toBe 'one'
|
||||
layer1MarkerDecoration = decorationManager.decorateMarker(layer1Marker, decorationProperties)
|
||||
expect(layer1MarkerDecoration.isType('gutter')).toBe true
|
||||
expect(layer1MarkerDecoration.isType('line-number')).toBe false
|
||||
expect(layer1MarkerDecoration.getProperties().gutterName).toBe 'test-gutter'
|
||||
expect(layer1MarkerDecoration.getProperties().class).toBe 'one'
|
||||
|
@ -28,6 +28,15 @@ describe "DefaultDirectoryProvider", ->
|
||||
directory = provider.directoryForURISync(nonNormalizedPath)
|
||||
expect(directory.getPath()).toEqual tmp
|
||||
|
||||
it "normalizes disk drive letter in path on #win32", ->
|
||||
provider = new DefaultDirectoryProvider()
|
||||
nonNormalizedPath = tmp[0].toLowerCase()+tmp.slice(1)
|
||||
expect(tmp).not.toMatch /^[a-z]:/
|
||||
expect(nonNormalizedPath).toMatch /^[a-z]:/
|
||||
|
||||
directory = provider.directoryForURISync(nonNormalizedPath)
|
||||
expect(directory.getPath()).toEqual tmp
|
||||
|
||||
it "creates a Directory for its parent dir when passed a file", ->
|
||||
provider = new DefaultDirectoryProvider()
|
||||
file = path.join(tmp, "example.txt")
|
||||
|
@ -1,60 +0,0 @@
|
||||
DOMElementPool = require '../src/dom-element-pool'
|
||||
{contains} = require 'underscore-plus'
|
||||
|
||||
describe "DOMElementPool", ->
|
||||
domElementPool = null
|
||||
|
||||
beforeEach ->
|
||||
domElementPool = new DOMElementPool
|
||||
|
||||
it "builds DOM nodes, recycling them when they are freed", ->
|
||||
[div, span1, span2, span3, span4, span5, textNode] = elements = [
|
||||
domElementPool.buildElement("div")
|
||||
domElementPool.buildElement("span")
|
||||
domElementPool.buildElement("span")
|
||||
domElementPool.buildElement("span")
|
||||
domElementPool.buildElement("span")
|
||||
domElementPool.buildElement("span")
|
||||
domElementPool.buildText("Hello world!")
|
||||
]
|
||||
|
||||
div.appendChild(span1)
|
||||
span1.appendChild(span2)
|
||||
div.appendChild(span3)
|
||||
span3.appendChild(span4)
|
||||
span4.appendChild(textNode)
|
||||
|
||||
domElementPool.freeElementAndDescendants(div)
|
||||
domElementPool.freeElementAndDescendants(span5)
|
||||
|
||||
expect(contains(elements, domElementPool.buildElement("div"))).toBe(true)
|
||||
expect(contains(elements, domElementPool.buildElement("span"))).toBe(true)
|
||||
expect(contains(elements, domElementPool.buildElement("span"))).toBe(true)
|
||||
expect(contains(elements, domElementPool.buildElement("span"))).toBe(true)
|
||||
expect(contains(elements, domElementPool.buildElement("span"))).toBe(true)
|
||||
expect(contains(elements, domElementPool.buildElement("span"))).toBe(true)
|
||||
expect(contains(elements, domElementPool.buildText("another text"))).toBe(true)
|
||||
|
||||
expect(contains(elements, domElementPool.buildElement("div"))).toBe(false)
|
||||
expect(contains(elements, domElementPool.buildElement("span"))).toBe(false)
|
||||
expect(contains(elements, domElementPool.buildText("unexisting"))).toBe(false)
|
||||
|
||||
it "forgets free nodes after being cleared", ->
|
||||
span = domElementPool.buildElement("span")
|
||||
div = domElementPool.buildElement("div")
|
||||
domElementPool.freeElementAndDescendants(span)
|
||||
domElementPool.freeElementAndDescendants(div)
|
||||
|
||||
domElementPool.clear()
|
||||
|
||||
expect(domElementPool.buildElement("span")).not.toBe(span)
|
||||
expect(domElementPool.buildElement("div")).not.toBe(div)
|
||||
|
||||
it "throws an error when trying to free the same node twice", ->
|
||||
div = domElementPool.buildElement("div")
|
||||
domElementPool.freeElementAndDescendants(div)
|
||||
expect(-> domElementPool.freeElementAndDescendants(div)).toThrow()
|
||||
|
||||
it "throws an error when trying to free an invalid element", ->
|
||||
expect(-> domElementPool.freeElementAndDescendants(null)).toThrow()
|
||||
expect(-> domElementPool.freeElementAndDescendants(undefined)).toThrow()
|
112
spec/dom-element-pool-spec.js
Normal file
112
spec/dom-element-pool-spec.js
Normal file
@ -0,0 +1,112 @@
|
||||
const DOMElementPool = require ('../src/dom-element-pool')
|
||||
|
||||
describe('DOMElementPool', function () {
|
||||
let domElementPool
|
||||
|
||||
beforeEach(() => { domElementPool = new DOMElementPool() })
|
||||
|
||||
it('builds DOM nodes, recycling them when they are freed', function () {
|
||||
let elements
|
||||
const [div, span1, span2, span3, span4, span5, textNode] = Array.from(elements = [
|
||||
domElementPool.buildElement('div', 'foo'),
|
||||
domElementPool.buildElement('span'),
|
||||
domElementPool.buildElement('span'),
|
||||
domElementPool.buildElement('span'),
|
||||
domElementPool.buildElement('span'),
|
||||
domElementPool.buildElement('span'),
|
||||
domElementPool.buildText('Hello world!')
|
||||
])
|
||||
|
||||
expect(div.className).toBe('foo')
|
||||
div.textContent = 'testing'
|
||||
div.style.backgroundColor = 'red'
|
||||
div.dataset.foo = 'bar'
|
||||
|
||||
expect(textNode.textContent).toBe('Hello world!')
|
||||
|
||||
div.appendChild(span1)
|
||||
span1.appendChild(span2)
|
||||
div.appendChild(span3)
|
||||
span3.appendChild(span4)
|
||||
span4.appendChild(textNode)
|
||||
|
||||
domElementPool.freeElementAndDescendants(div)
|
||||
domElementPool.freeElementAndDescendants(span5)
|
||||
|
||||
expect(elements.includes(domElementPool.buildElement('div'))).toBe(true)
|
||||
expect(elements.includes(domElementPool.buildElement('span'))).toBe(true)
|
||||
expect(elements.includes(domElementPool.buildElement('span'))).toBe(true)
|
||||
expect(elements.includes(domElementPool.buildElement('span'))).toBe(true)
|
||||
expect(elements.includes(domElementPool.buildElement('span'))).toBe(true)
|
||||
expect(elements.includes(domElementPool.buildElement('span'))).toBe(true)
|
||||
expect(elements.includes(domElementPool.buildText('another text'))).toBe(true)
|
||||
|
||||
expect(elements.includes(domElementPool.buildElement('div'))).toBe(false)
|
||||
expect(elements.includes(domElementPool.buildElement('span'))).toBe(false)
|
||||
expect(elements.includes(domElementPool.buildText('unexisting'))).toBe(false)
|
||||
|
||||
expect(div.className).toBe('')
|
||||
expect(div.textContent).toBe('')
|
||||
expect(div.style.backgroundColor).toBe('')
|
||||
expect(div.dataset.foo).toBeUndefined()
|
||||
|
||||
expect(textNode.textContent).toBe('another text')
|
||||
})
|
||||
|
||||
it('forgets free nodes after being cleared', function () {
|
||||
const span = domElementPool.buildElement('span')
|
||||
const div = domElementPool.buildElement('div')
|
||||
domElementPool.freeElementAndDescendants(span)
|
||||
domElementPool.freeElementAndDescendants(div)
|
||||
|
||||
domElementPool.clear()
|
||||
|
||||
expect(domElementPool.buildElement('span')).not.toBe(span)
|
||||
expect(domElementPool.buildElement('div')).not.toBe(div)
|
||||
})
|
||||
|
||||
it('does not attempt to free nodes that were not created by the pool', () => {
|
||||
let assertionFailure
|
||||
atom.onDidFailAssertion((error) => assertionFailure = error)
|
||||
|
||||
const foreignDiv = document.createElement('div')
|
||||
const div = domElementPool.buildElement('div')
|
||||
div.appendChild(foreignDiv)
|
||||
domElementPool.freeElementAndDescendants(div)
|
||||
const span = domElementPool.buildElement('span')
|
||||
span.appendChild(foreignDiv)
|
||||
domElementPool.freeElementAndDescendants(span)
|
||||
|
||||
expect(assertionFailure).toBeUndefined()
|
||||
})
|
||||
|
||||
it('fails an assertion when freeing the same element twice', function () {
|
||||
let assertionFailure
|
||||
atom.onDidFailAssertion((error) => assertionFailure = error)
|
||||
|
||||
const div = domElementPool.buildElement('div')
|
||||
div.textContent = 'testing'
|
||||
domElementPool.freeElementAndDescendants(div)
|
||||
expect(assertionFailure).toBeUndefined()
|
||||
domElementPool.freeElementAndDescendants(div)
|
||||
expect(assertionFailure.message).toBe('Assertion failed: The element has already been freed!')
|
||||
expect(assertionFailure.metadata.content).toBe('<div>testing</div>')
|
||||
})
|
||||
|
||||
it('fails an assertion when freeing the same text node twice', function () {
|
||||
let assertionFailure
|
||||
atom.onDidFailAssertion((error) => assertionFailure = error)
|
||||
|
||||
const node = domElementPool.buildText('testing')
|
||||
domElementPool.freeElementAndDescendants(node)
|
||||
expect(assertionFailure).toBeUndefined()
|
||||
domElementPool.freeElementAndDescendants(node)
|
||||
expect(assertionFailure.message).toBe('Assertion failed: The element has already been freed!')
|
||||
expect(assertionFailure.metadata.content).toBe('testing')
|
||||
})
|
||||
|
||||
it('throws an error when trying to free an invalid element', function () {
|
||||
expect(() => domElementPool.freeElementAndDescendants(null)).toThrow()
|
||||
expect(() => domElementPool.freeElementAndDescendants(undefined)).toThrow()
|
||||
})
|
||||
})
|
@ -196,7 +196,9 @@ describe('AtomApplication', function () {
|
||||
it('persists window state based on the project directories', async function () {
|
||||
const tempDirPath = makeTempDir()
|
||||
const atomApplication = buildAtomApplication()
|
||||
const window1 = atomApplication.launch(parseCommandLine([path.join(tempDirPath, 'new-file')]))
|
||||
const nonExistentFilePath = path.join(tempDirPath, 'new-file')
|
||||
|
||||
const window1 = atomApplication.launch(parseCommandLine([nonExistentFilePath]))
|
||||
await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
|
||||
atom.workspace.observeActivePaneItem(function (textEditor) {
|
||||
if (textEditor) {
|
||||
@ -205,17 +207,31 @@ describe('AtomApplication', function () {
|
||||
}
|
||||
})
|
||||
})
|
||||
await window1.saveState()
|
||||
window1.close()
|
||||
await window1.closedPromise
|
||||
|
||||
const window2 = atomApplication.launch(parseCommandLine([path.join(tempDirPath)]))
|
||||
// Restore unsaved state when opening the directory itself
|
||||
const window2 = atomApplication.launch(parseCommandLine([tempDirPath]))
|
||||
await window2.loadedPromise
|
||||
const window2Text = await evalInWebContents(window2.browserWindow.webContents, function (sendBackToMainProcess) {
|
||||
atom.workspace.observeActivePaneItem(function (textEditor) {
|
||||
if (textEditor) sendBackToMainProcess(textEditor.getText())
|
||||
})
|
||||
const textEditor = atom.workspace.getActiveTextEditor()
|
||||
textEditor.moveToBottom()
|
||||
textEditor.insertText(' How are you?')
|
||||
sendBackToMainProcess(textEditor.getText())
|
||||
})
|
||||
assert.equal(window2Text, 'Hello World! How are you?')
|
||||
await window2.saveState()
|
||||
window2.close()
|
||||
await window2.closedPromise
|
||||
|
||||
assert.equal(window2Text, 'Hello World!')
|
||||
// Restore unsaved state when opening a path to a non-existent file in the directory
|
||||
const window3 = atomApplication.launch(parseCommandLine([path.join(tempDirPath, 'another-non-existent-file')]))
|
||||
await window3.loadedPromise
|
||||
const window3Texts = await evalInWebContents(window3.browserWindow.webContents, function (sendBackToMainProcess, nonExistentFilePath) {
|
||||
sendBackToMainProcess(atom.workspace.getTextEditors().map(editor => editor.getText()))
|
||||
})
|
||||
assert.include(window3Texts, 'Hello World! How are you?')
|
||||
})
|
||||
|
||||
it('shows all directories in the tree view when multiple directory paths are passed to Atom', async function () {
|
||||
@ -260,7 +276,7 @@ describe('AtomApplication', function () {
|
||||
})
|
||||
assert.equal(window1EditorTitle, 'untitled')
|
||||
|
||||
const window2 = atomApplication.launch(parseCommandLine([]))
|
||||
const window2 = atomApplication.openWithOptions(parseCommandLine([]))
|
||||
await focusWindow(window2)
|
||||
const window2EditorTitle = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
|
||||
sendBackToMainProcess(atom.workspace.getActiveTextEditor().getTitle())
|
||||
@ -472,7 +488,7 @@ describe('AtomApplication', function () {
|
||||
}
|
||||
|
||||
let channelIdCounter = 0
|
||||
function evalInWebContents (webContents, source) {
|
||||
function evalInWebContents (webContents, source, ...args) {
|
||||
const channelId = 'eval-result-' + channelIdCounter++
|
||||
return new Promise(function (resolve) {
|
||||
electron.ipcMain.on(channelId, receiveResult)
|
||||
|
@ -112,7 +112,7 @@ describe("FileRecoveryService", () => {
|
||||
const mockWindow = {}
|
||||
const filePath = temp.path()
|
||||
fs.writeFileSync(filePath, "content")
|
||||
fs.chmodSync(filePath, 0444)
|
||||
fs.chmodSync(filePath, 0o444)
|
||||
|
||||
let logs = []
|
||||
this.stub(console, 'log', (message) => logs.push(message))
|
||||
|
@ -614,3 +614,7 @@ describe "Project", ->
|
||||
|
||||
randomPath = path.join("some", "random", "path")
|
||||
expect(atom.project.contains(randomPath)).toBe false
|
||||
|
||||
describe ".resolvePath(uri)", ->
|
||||
it "normalizes disk drive letter in passed path on #win32", ->
|
||||
expect(atom.project.resolvePath("d:\\file.txt")).toEqual "D:\\file.txt"
|
||||
|
@ -1747,11 +1747,13 @@ describe('TextEditorComponent', function () {
|
||||
})
|
||||
|
||||
describe('block decorations rendering', function () {
|
||||
let markerLayer
|
||||
|
||||
function createBlockDecorationBeforeScreenRow(screenRow, {className}) {
|
||||
let item = document.createElement("div")
|
||||
item.className = className || ""
|
||||
let blockDecoration = editor.decorateMarker(
|
||||
editor.markScreenPosition([screenRow, 0], {invalidate: "never"}),
|
||||
markerLayer.markScreenPosition([screenRow, 0], {invalidate: "never"}),
|
||||
{type: "block", item: item, position: "before"}
|
||||
)
|
||||
return [item, blockDecoration]
|
||||
@ -1761,13 +1763,14 @@ describe('TextEditorComponent', function () {
|
||||
let item = document.createElement("div")
|
||||
item.className = className || ""
|
||||
let blockDecoration = editor.decorateMarker(
|
||||
editor.markScreenPosition([screenRow, 0], {invalidate: "never"}),
|
||||
markerLayer.markScreenPosition([screenRow, 0], {invalidate: "never"}),
|
||||
{type: "block", item: item, position: "after"}
|
||||
)
|
||||
return [item, blockDecoration]
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
markerLayer = editor.addMarkerLayer()
|
||||
wrapperNode.style.height = 5 * lineHeightInPixels + 'px'
|
||||
editor.update({autoHeight: false})
|
||||
component.measureDimensions()
|
||||
|
@ -886,8 +886,12 @@ describe "Workspace", ->
|
||||
|
||||
describe "document.title", ->
|
||||
describe "when there is no item open", ->
|
||||
it "sets the title to 'untitled'", ->
|
||||
expect(document.title).toMatch ///^untitled///
|
||||
it "sets the title to the project path", ->
|
||||
expect(document.title).toMatch escapeStringRegex(fs.tildify(atom.project.getPaths()[0]))
|
||||
|
||||
it "sets the title to 'untitled' if there is no project path", ->
|
||||
atom.project.setPaths([])
|
||||
expect(document.title).toMatch /^untitled/
|
||||
|
||||
describe "when the active pane item's path is not inside a project path", ->
|
||||
beforeEach ->
|
||||
@ -948,10 +952,10 @@ describe "Workspace", ->
|
||||
expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{pathEscaped}///
|
||||
|
||||
describe "when the last pane item is removed", ->
|
||||
it "updates the title to be untitled", ->
|
||||
it "updates the title to the project's first path", ->
|
||||
atom.workspace.getActivePane().destroy()
|
||||
expect(atom.workspace.getActivePaneItem()).toBeUndefined()
|
||||
expect(document.title).toMatch ///^untitled///
|
||||
expect(document.title).toMatch escapeStringRegex(fs.tildify(atom.project.getPaths()[0]))
|
||||
|
||||
describe "when an inactive pane's item changes", ->
|
||||
it "does not update the title", ->
|
||||
|
@ -2,10 +2,12 @@ _ = require 'underscore-plus'
|
||||
{screen, ipcRenderer, remote, shell, webFrame} = require 'electron'
|
||||
ipcHelpers = require './ipc-helpers'
|
||||
{Disposable} = require 'event-kit'
|
||||
{getWindowLoadSettings, setWindowLoadSettings} = require './window-load-settings-helpers'
|
||||
getWindowLoadSettings = require './get-window-load-settings'
|
||||
|
||||
module.exports =
|
||||
class ApplicationDelegate
|
||||
getWindowLoadSettings: -> getWindowLoadSettings()
|
||||
|
||||
open: (params) ->
|
||||
ipcRenderer.send('open', params)
|
||||
|
||||
@ -109,10 +111,7 @@ class ApplicationDelegate
|
||||
ipcRenderer.send("add-recent-document", filename)
|
||||
|
||||
setRepresentedDirectoryPaths: (paths) ->
|
||||
loadSettings = getWindowLoadSettings()
|
||||
loadSettings['initialPaths'] = paths
|
||||
setWindowLoadSettings(loadSettings)
|
||||
ipcRenderer.send("did-change-paths")
|
||||
ipcHelpers.call('window-method', 'setRepresentedDirectoryPaths', paths)
|
||||
|
||||
setAutoHideWindowMenuBar: (autoHide) ->
|
||||
ipcHelpers.call('window-method', 'setAutoHideMenuBar', autoHide)
|
||||
@ -149,13 +148,9 @@ class ApplicationDelegate
|
||||
showMessageDialog: (params) ->
|
||||
|
||||
showSaveDialog: (params) ->
|
||||
if _.isString(params)
|
||||
params = defaultPath: params
|
||||
else
|
||||
params = _.clone(params)
|
||||
params.title ?= 'Save File'
|
||||
params.defaultPath ?= getWindowLoadSettings().initialPaths[0]
|
||||
remote.dialog.showSaveDialog remote.getCurrentWindow(), params
|
||||
if typeof params is 'string'
|
||||
params = {defaultPath: params}
|
||||
@getCurrentWindow().showSaveDialog(params)
|
||||
|
||||
playBeepSound: ->
|
||||
shell.beep()
|
||||
|
@ -11,7 +11,6 @@ Model = require './model'
|
||||
WindowEventHandler = require './window-event-handler'
|
||||
StateStore = require './state-store'
|
||||
StorageFolder = require './storage-folder'
|
||||
{getWindowLoadSettings} = require './window-load-settings-helpers'
|
||||
registerDefaultCommands = require './register-default-commands'
|
||||
{updateProcessEnv} = require './update-process-env'
|
||||
|
||||
@ -458,7 +457,7 @@ class AtomEnvironment extends Model
|
||||
#
|
||||
# Returns an {Object} containing all the load setting key/value pairs.
|
||||
getLoadSettings: ->
|
||||
getWindowLoadSettings()
|
||||
@applicationDelegate.getWindowLoadSettings()
|
||||
|
||||
###
|
||||
Section: Managing The Atom Window
|
||||
@ -695,8 +694,14 @@ class AtomEnvironment extends Model
|
||||
@deserialize(state) if state?
|
||||
@deserializeTimings.atom = Date.now() - startTime
|
||||
|
||||
if process.platform is 'darwin' and @config.get('core.useCustomTitleBar')
|
||||
if process.platform is 'darwin' and @config.get('core.titleBar') is 'custom'
|
||||
@workspace.addHeaderPanel({item: new TitleBar({@workspace, @themes, @applicationDelegate})})
|
||||
@document.body.classList.add('custom-title-bar')
|
||||
if process.platform is 'darwin' and @config.get('core.titleBar') is 'custom-inset'
|
||||
@workspace.addHeaderPanel({item: new TitleBar({@workspace, @themes, @applicationDelegate})})
|
||||
@document.body.classList.add('custom-inset-title-bar')
|
||||
if process.platform is 'darwin' and @config.get('core.titleBar') is 'hidden'
|
||||
@document.body.classList.add('hidden-title-bar')
|
||||
|
||||
@document.body.appendChild(@views.getView(@workspace))
|
||||
@backgroundStylesheet?.remove()
|
||||
@ -822,12 +827,17 @@ class AtomEnvironment extends Model
|
||||
Section: Private
|
||||
###
|
||||
|
||||
assert: (condition, message, callback) ->
|
||||
assert: (condition, message, callbackOrMetadata) ->
|
||||
return true if condition
|
||||
|
||||
error = new Error("Assertion failed: #{message}")
|
||||
Error.captureStackTrace(error, @assert)
|
||||
callback?(error)
|
||||
|
||||
if callbackOrMetadata?
|
||||
if typeof callbackOrMetadata is 'function'
|
||||
callbackOrMetadata?(error)
|
||||
else
|
||||
error.metadata = callbackOrMetadata
|
||||
|
||||
@emitter.emit 'did-fail-assertion', error
|
||||
|
||||
|
20
src/babel.js
20
src/babel.js
@ -6,6 +6,7 @@ var defaultOptions = require('../static/babelrc.json')
|
||||
|
||||
var babel = null
|
||||
var babelVersionDirectory = null
|
||||
var options = null
|
||||
|
||||
var PREFIXES = [
|
||||
'/** @babel */',
|
||||
@ -47,16 +48,27 @@ exports.compile = function (sourceCode, filePath) {
|
||||
var noop = function () {}
|
||||
Logger.prototype.debug = noop
|
||||
Logger.prototype.verbose = noop
|
||||
|
||||
options = {ast: false, babelrc: false}
|
||||
for (var key in defaultOptions) {
|
||||
if (key === 'plugins') {
|
||||
const plugins = []
|
||||
for (const [pluginName, pluginOptions] of defaultOptions[key]) {
|
||||
plugins.push([require.resolve(`babel-plugin-${pluginName}`), pluginOptions])
|
||||
}
|
||||
options[key] = plugins
|
||||
} else {
|
||||
options[key] = defaultOptions[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
filePath = 'file:///' + path.resolve(filePath).replace(/\\/g, '/')
|
||||
}
|
||||
|
||||
var options = {filename: filePath}
|
||||
for (var key in defaultOptions) {
|
||||
options[key] = defaultOptions[key]
|
||||
}
|
||||
options.filename = filePath
|
||||
|
||||
return babel.transform(sourceCode, options).code
|
||||
}
|
||||
|
||||
|
@ -68,6 +68,12 @@ const configSchema = {
|
||||
default: true,
|
||||
description: 'Trigger the system\'s beep sound when certain actions cannot be executed or there are no results.'
|
||||
},
|
||||
closeDeletedFileTabs: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
title: 'Close Deleted File Tabs',
|
||||
description: 'Close corresponding editors when a file is deleted outside Atom.'
|
||||
},
|
||||
destroyEmptyPanes: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
@ -492,10 +498,11 @@ if (['win32', 'linux'].includes(process.platform)) {
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
configSchema.core.properties.useCustomTitleBar = {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Use custom, theme-aware title bar.<br>Note: This currently does not include a proxy icon.<br>This setting will require a relaunch of Atom to take effect.'
|
||||
configSchema.core.properties.titleBar = {
|
||||
type: 'string',
|
||||
default: 'native',
|
||||
enum: ['native', 'custom', 'custom-inset', 'hidden'],
|
||||
description: 'Experimental: A `custom` title bar adapts to theme colors. Choosing `custom-inset` adds a bit more padding. The title bar can also be completely `hidden`.<br>Note: Switching to a custom or hidden title bar will compromise some functionality.<br>This setting will require a relaunch of Atom to take effect.'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -336,6 +336,31 @@ ScopeDescriptor = require './scope-descriptor'
|
||||
# order: 2
|
||||
# ```
|
||||
#
|
||||
# ## Manipulating values outside your configuration schema
|
||||
#
|
||||
# It is possible to manipulate(`get`, `set`, `observe` etc) values that do not
|
||||
# appear in your configuration schema. For example, if the config schema of the
|
||||
# package 'some-package' is
|
||||
#
|
||||
# ```coffee
|
||||
# config:
|
||||
# someSetting:
|
||||
# type: 'boolean'
|
||||
# default: false
|
||||
# ```
|
||||
#
|
||||
# You can still do the following
|
||||
#
|
||||
# ```coffee
|
||||
# let otherSetting = atom.config.get('some-package.otherSetting')
|
||||
# atom.config.set('some-package.stillAnotherSetting', otherSetting * 5)
|
||||
# ```
|
||||
#
|
||||
# In other words, if a function asks for a `key-path`, that path doesn't have to
|
||||
# be described in the config schema for the package or any package. However, as
|
||||
# highlighted in the best practices section, you are advised against doing the
|
||||
# above.
|
||||
#
|
||||
# ## Best practices
|
||||
#
|
||||
# * Don't depend on (or write to) configuration keys outside of your keypath.
|
||||
|
@ -8,7 +8,7 @@ class DecorationManager extends Model
|
||||
didUpdateDecorationsEventScheduled: false
|
||||
updatedSynchronously: false
|
||||
|
||||
constructor: (@displayLayer, @defaultMarkerLayer) ->
|
||||
constructor: (@displayLayer) ->
|
||||
super
|
||||
|
||||
@emitter = new Emitter
|
||||
@ -71,9 +71,11 @@ class DecorationManager extends Model
|
||||
|
||||
decorationsForScreenRowRange: (startScreenRow, endScreenRow) ->
|
||||
decorationsByMarkerId = {}
|
||||
for marker in @defaultMarkerLayer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow])
|
||||
if decorations = @decorationsByMarkerId[marker.id]
|
||||
decorationsByMarkerId[marker.id] = decorations
|
||||
for layerId of @decorationCountsByLayerId
|
||||
layer = @displayLayer.getMarkerLayer(layerId)
|
||||
for marker in layer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow])
|
||||
if decorations = @decorationsByMarkerId[marker.id]
|
||||
decorationsByMarkerId[marker.id] = decorations
|
||||
decorationsByMarkerId
|
||||
|
||||
decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) ->
|
||||
@ -104,7 +106,14 @@ class DecorationManager extends Model
|
||||
decorationsState
|
||||
|
||||
decorateMarker: (marker, decorationParams) ->
|
||||
throw new Error("Cannot decorate a destroyed marker") if marker.isDestroyed()
|
||||
if marker.isDestroyed()
|
||||
error = new Error("Cannot decorate a destroyed marker")
|
||||
error.metadata = {markerLayerIsDestroyed: marker.layer.isDestroyed()}
|
||||
if marker.destroyStackTrace?
|
||||
error.metadata.destroyStackTrace = marker.destroyStackTrace
|
||||
if marker.bufferMarker?.destroyStackTrace?
|
||||
error.metadata.destroyStackTrace = marker.bufferMarker?.destroyStackTrace
|
||||
throw error
|
||||
marker = @displayLayer.getMarkerLayer(marker.layer.id).getMarker(marker.id)
|
||||
decoration = new Decoration(marker, this, decorationParams)
|
||||
@decorationsByMarkerId[marker.id] ?= []
|
||||
@ -117,6 +126,7 @@ class DecorationManager extends Model
|
||||
decoration
|
||||
|
||||
decorateMarkerLayer: (markerLayer, decorationParams) ->
|
||||
throw new Error("Cannot decorate a destroyed marker layer") if markerLayer.isDestroyed()
|
||||
decoration = new LayerDecoration(markerLayer, this, decorationParams)
|
||||
@layerDecorationsByMarkerLayerId[markerLayer.id] ?= []
|
||||
@layerDecorationsByMarkerLayerId[markerLayer.id].push(decoration)
|
||||
|
@ -15,7 +15,7 @@ class DefaultDirectoryProvider
|
||||
# * {Directory} if the given URI is compatible with this provider.
|
||||
# * `null` if the given URI is not compatibile with this provider.
|
||||
directoryForURISync: (uri) ->
|
||||
normalizedPath = path.normalize(uri)
|
||||
normalizedPath = @normalizePath(uri)
|
||||
{host} = url.parse(uri)
|
||||
directoryPath = if host
|
||||
uri
|
||||
@ -42,3 +42,17 @@ class DefaultDirectoryProvider
|
||||
# * `null` if the given URI is not compatibile with this provider.
|
||||
directoryForURI: (uri) ->
|
||||
Promise.resolve(@directoryForURISync(uri))
|
||||
|
||||
# Public: Normalizes path.
|
||||
#
|
||||
# * `uri` {String} The path that should be normalized.
|
||||
#
|
||||
# Returns a {String} with normalized path.
|
||||
normalizePath: (uri) ->
|
||||
# Normalize disk drive letter on Windows to avoid opening two buffers for the same file
|
||||
pathWithNormalizedDiskDriveLetter =
|
||||
if process.platform is 'win32' and matchData = uri.match(/^([a-z]):/)
|
||||
"#{matchData[1].toUpperCase()}#{uri.slice(1)}"
|
||||
else
|
||||
uri
|
||||
path.normalize(pathWithNormalizedDiskDriveLetter)
|
||||
|
@ -1,55 +0,0 @@
|
||||
module.exports =
|
||||
class DOMElementPool
|
||||
constructor: ->
|
||||
@freeElementsByTagName = {}
|
||||
@freedElements = new Set
|
||||
|
||||
clear: ->
|
||||
@freedElements.clear()
|
||||
for tagName, freeElements of @freeElementsByTagName
|
||||
freeElements.length = 0
|
||||
return
|
||||
|
||||
build: (tagName, factory, reset) ->
|
||||
element = @freeElementsByTagName[tagName]?.pop()
|
||||
element ?= factory()
|
||||
reset(element)
|
||||
@freedElements.delete(element)
|
||||
element
|
||||
|
||||
buildElement: (tagName, className) ->
|
||||
factory = -> document.createElement(tagName)
|
||||
reset = (element) ->
|
||||
delete element.dataset[dataId] for dataId of element.dataset
|
||||
element.removeAttribute("style")
|
||||
if className?
|
||||
element.className = className
|
||||
else
|
||||
element.removeAttribute("class")
|
||||
@build(tagName, factory, reset)
|
||||
|
||||
buildText: (textContent) ->
|
||||
factory = -> document.createTextNode(textContent)
|
||||
reset = (element) -> element.textContent = textContent
|
||||
@build("#text", factory, reset)
|
||||
|
||||
freeElementAndDescendants: (element) ->
|
||||
@free(element)
|
||||
@freeDescendants(element)
|
||||
|
||||
freeDescendants: (element) ->
|
||||
for descendant in element.childNodes by -1
|
||||
@free(descendant)
|
||||
@freeDescendants(descendant)
|
||||
return
|
||||
|
||||
free: (element) ->
|
||||
throw new Error("The element cannot be null or undefined.") unless element?
|
||||
throw new Error("The element has already been freed!") if @freedElements.has(element)
|
||||
|
||||
tagName = element.nodeName.toLowerCase()
|
||||
@freeElementsByTagName[tagName] ?= []
|
||||
@freeElementsByTagName[tagName].push(element)
|
||||
@freedElements.add(element)
|
||||
|
||||
element.remove()
|
89
src/dom-element-pool.js
Normal file
89
src/dom-element-pool.js
Normal file
@ -0,0 +1,89 @@
|
||||
module.exports =
|
||||
class DOMElementPool {
|
||||
constructor () {
|
||||
this.managedElements = new Set()
|
||||
this.freeElementsByTagName = new Map()
|
||||
this.freedElements = new Set()
|
||||
}
|
||||
|
||||
clear () {
|
||||
this.managedElements.clear()
|
||||
this.freedElements.clear()
|
||||
this.freeElementsByTagName.clear()
|
||||
}
|
||||
|
||||
buildElement (tagName, className) {
|
||||
const elements = this.freeElementsByTagName.get(tagName)
|
||||
let element = elements ? elements.pop() : null
|
||||
if (element) {
|
||||
for (let dataId in element.dataset) { delete element.dataset[dataId] }
|
||||
element.removeAttribute('style')
|
||||
if (className) {
|
||||
element.className = className
|
||||
} else {
|
||||
element.removeAttribute('class')
|
||||
}
|
||||
while (element.firstChild) {
|
||||
element.removeChild(element.firstChild)
|
||||
}
|
||||
this.freedElements.delete(element)
|
||||
} else {
|
||||
element = document.createElement(tagName)
|
||||
if (className) {
|
||||
element.className = className
|
||||
}
|
||||
this.managedElements.add(element)
|
||||
}
|
||||
return element
|
||||
}
|
||||
|
||||
buildText (textContent) {
|
||||
const elements = this.freeElementsByTagName.get('#text')
|
||||
let element = elements ? elements.pop() : null
|
||||
if (element) {
|
||||
element.textContent = textContent
|
||||
this.freedElements.delete(element)
|
||||
} else {
|
||||
element = document.createTextNode(textContent)
|
||||
this.managedElements.add(element)
|
||||
}
|
||||
return element
|
||||
}
|
||||
|
||||
freeElementAndDescendants (element) {
|
||||
this.free(element)
|
||||
element.remove()
|
||||
}
|
||||
|
||||
freeDescendants (element) {
|
||||
while (element.firstChild) {
|
||||
this.free(element.firstChild)
|
||||
element.removeChild(element.firstChild)
|
||||
}
|
||||
}
|
||||
|
||||
free (element) {
|
||||
if (element == null) { throw new Error('The element cannot be null or undefined.') }
|
||||
if (!this.managedElements.has(element)) return
|
||||
if (this.freedElements.has(element)) {
|
||||
atom.assert(false, 'The element has already been freed!', {
|
||||
content: element instanceof window.Text ? element.textContent : element.outerHTML
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const tagName = element.nodeName.toLowerCase()
|
||||
let elements = this.freeElementsByTagName.get(tagName)
|
||||
if (!elements) {
|
||||
elements = []
|
||||
this.freeElementsByTagName.set(tagName, elements)
|
||||
}
|
||||
elements.push(element)
|
||||
this.freedElements.add(element)
|
||||
|
||||
for (let i = element.childNodes.length - 1; i >= 0; i--) {
|
||||
const descendant = element.childNodes[i]
|
||||
this.free(descendant)
|
||||
}
|
||||
}
|
||||
}
|
10
src/get-window-load-settings.js
Normal file
10
src/get-window-load-settings.js
Normal file
@ -0,0 +1,10 @@
|
||||
const {remote} = require('electron')
|
||||
|
||||
let windowLoadSettings = null
|
||||
|
||||
module.exports = () => {
|
||||
if (!windowLoadSettings) {
|
||||
windowLoadSettings = remote.getCurrentWindow().loadSettings
|
||||
}
|
||||
return windowLoadSettings
|
||||
}
|
@ -3,7 +3,7 @@ module.exports = ({blobStore}) ->
|
||||
{updateProcessEnv} = require('./update-process-env')
|
||||
path = require 'path'
|
||||
require './window'
|
||||
{getWindowLoadSettings} = require './window-load-settings-helpers'
|
||||
getWindowLoadSettings = require './get-window-load-settings'
|
||||
{ipcRenderer} = require 'electron'
|
||||
{resourcePath, devMode, env} = getWindowLoadSettings()
|
||||
require './electron-shims'
|
||||
|
@ -6,7 +6,7 @@ import ipcHelpers from './ipc-helpers'
|
||||
import util from 'util'
|
||||
|
||||
export default async function () {
|
||||
const {getWindowLoadSettings} = require('./window-load-settings-helpers')
|
||||
const getWindowLoadSettings = require('./get-window-load-settings')
|
||||
const {test, headless, resourcePath, benchmarkPaths} = getWindowLoadSettings()
|
||||
try {
|
||||
const Clipboard = require('../src/clipboard')
|
||||
|
@ -18,7 +18,7 @@ module.exports = ({blobStore}) ->
|
||||
try
|
||||
path = require 'path'
|
||||
{ipcRenderer} = require 'electron'
|
||||
{getWindowLoadSettings} = require './window-load-settings-helpers'
|
||||
getWindowLoadSettings = require './get-window-load-settings'
|
||||
CompileCache = require './compile-cache'
|
||||
AtomEnvironment = require '../src/atom-environment'
|
||||
ApplicationDelegate = require '../src/application-delegate'
|
||||
|
@ -84,7 +84,13 @@ class AtomApplication
|
||||
initialize: (options) ->
|
||||
global.atomApplication = this
|
||||
|
||||
@config.onDidChange 'core.useCustomTitleBar', @promptForRestart.bind(this)
|
||||
# DEPRECATED: This can be removed at some point (added in 1.13)
|
||||
# It converts `useCustomTitleBar: true` to `titleBar: "custom"`
|
||||
if process.platform is 'darwin' and @config.get('core.useCustomTitleBar')
|
||||
@config.unset('core.useCustomTitleBar')
|
||||
@config.set('core.titleBar', 'custom')
|
||||
|
||||
@config.onDidChange 'core.titleBar', @promptForRestart.bind(this)
|
||||
|
||||
@autoUpdateManager = new AutoUpdateManager(
|
||||
@version, options.test or options.benchmark or options.benchmarkTest, @resourcePath, @config
|
||||
@ -587,8 +593,7 @@ class AtomApplication
|
||||
states = []
|
||||
for window in @windows
|
||||
unless window.isSpec
|
||||
if loadSettings = window.getLoadSettings()
|
||||
states.push(initialPaths: loadSettings.initialPaths)
|
||||
states.push({initialPaths: window.representedDirectoryPaths})
|
||||
if states.length > 0 or allowEmpty
|
||||
@storageFolder.storeSync('application.json', states)
|
||||
|
||||
|
@ -43,12 +43,16 @@ class AtomWindow
|
||||
if process.platform is 'linux'
|
||||
options.icon = @constructor.iconPath
|
||||
|
||||
if @shouldHideTitleBar()
|
||||
if @shouldAddCustomTitleBar()
|
||||
options.titleBarStyle = 'hidden'
|
||||
|
||||
@browserWindow = new BrowserWindow options
|
||||
@atomApplication.addWindow(this)
|
||||
if @shouldAddCustomInsetTitleBar()
|
||||
options.titleBarStyle = 'hidden-inset'
|
||||
|
||||
if @shouldHideTitleBar()
|
||||
options.frame = false
|
||||
|
||||
@browserWindow = new BrowserWindow(options)
|
||||
@handleEvents()
|
||||
|
||||
loadSettings = Object.assign({}, settings)
|
||||
@ -60,11 +64,15 @@ class AtomWindow
|
||||
loadSettings.clearWindowState ?= false
|
||||
loadSettings.initialPaths ?=
|
||||
for {pathToOpen} in locationsToOpen when pathToOpen
|
||||
if fs.statSyncNoException(pathToOpen).isFile?()
|
||||
path.dirname(pathToOpen)
|
||||
else
|
||||
stat = fs.statSyncNoException(pathToOpen) or null
|
||||
if stat?.isDirectory()
|
||||
pathToOpen
|
||||
|
||||
else
|
||||
parentDirectory = path.dirname(pathToOpen)
|
||||
if stat?.isFile() or fs.existsSync(parentDirectory)
|
||||
parentDirectory
|
||||
else
|
||||
pathToOpen
|
||||
loadSettings.initialPaths.sort()
|
||||
|
||||
# Only send to the first non-spec window created
|
||||
@ -72,33 +80,31 @@ class AtomWindow
|
||||
@constructor.includeShellLoadTime = false
|
||||
loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime
|
||||
|
||||
@representedDirectoryPaths = loadSettings.initialPaths
|
||||
@env = loadSettings.env if loadSettings.env?
|
||||
|
||||
@browserWindow.loadSettings = loadSettings
|
||||
|
||||
@browserWindow.on 'window:loaded', =>
|
||||
@emit 'window:loaded'
|
||||
@resolveLoadedPromise()
|
||||
|
||||
@setLoadSettings(loadSettings)
|
||||
@env = loadSettings.env if loadSettings.env?
|
||||
@browserWindow.loadURL url.format
|
||||
protocol: 'file'
|
||||
pathname: "#{@resourcePath}/static/index.html"
|
||||
slashes: true
|
||||
|
||||
@browserWindow.showSaveDialog = @showSaveDialog.bind(this)
|
||||
|
||||
@browserWindow.focusOnWebView() if @isSpec
|
||||
@browserWindow.temporaryState = {windowDimensions} if windowDimensions?
|
||||
|
||||
hasPathToOpen = not (locationsToOpen.length is 1 and not locationsToOpen[0].pathToOpen?)
|
||||
@openLocations(locationsToOpen) if hasPathToOpen and not @isSpecWindow()
|
||||
|
||||
setLoadSettings: (loadSettings) ->
|
||||
@browserWindow.loadURL url.format
|
||||
protocol: 'file'
|
||||
pathname: "#{@resourcePath}/static/index.html"
|
||||
slashes: true
|
||||
hash: encodeURIComponent(JSON.stringify(loadSettings))
|
||||
@atomApplication.addWindow(this)
|
||||
|
||||
getLoadSettings: ->
|
||||
if @browserWindow.webContents? and not @browserWindow.webContents.isLoading()
|
||||
hash = url.parse(@browserWindow.webContents.getURL()).hash.substr(1)
|
||||
JSON.parse(decodeURIComponent(hash))
|
||||
|
||||
hasProjectPath: -> @getLoadSettings().initialPaths?.length > 0
|
||||
hasProjectPath: -> @representedDirectoryPaths.length > 0
|
||||
|
||||
setupContextMenu: ->
|
||||
ContextMenu = require './context-menu'
|
||||
@ -112,7 +118,7 @@ class AtomWindow
|
||||
true
|
||||
|
||||
containsPath: (pathToCheck) ->
|
||||
@getLoadSettings()?.initialPaths?.some (projectPath) ->
|
||||
@representedDirectoryPaths.some (projectPath) ->
|
||||
if not projectPath
|
||||
false
|
||||
else if not pathToCheck
|
||||
@ -226,10 +232,20 @@ class AtomWindow
|
||||
[width, height] = @browserWindow.getSize()
|
||||
{x, y, width, height}
|
||||
|
||||
shouldAddCustomTitleBar: ->
|
||||
not @isSpec and
|
||||
process.platform is 'darwin' and
|
||||
@atomApplication.config.get('core.titleBar') is 'custom'
|
||||
|
||||
shouldAddCustomInsetTitleBar: ->
|
||||
not @isSpec and
|
||||
process.platform is 'darwin' and
|
||||
@atomApplication.config.get('core.titleBar') is 'custom-inset'
|
||||
|
||||
shouldHideTitleBar: ->
|
||||
not @isSpec and
|
||||
process.platform is 'darwin' and
|
||||
@atomApplication.config.get('core.useCustomTitleBar')
|
||||
@atomApplication.config.get('core.titleBar') is 'hidden'
|
||||
|
||||
close: -> @browserWindow.close()
|
||||
|
||||
@ -265,6 +281,13 @@ class AtomWindow
|
||||
@saveState().then => @browserWindow.reload()
|
||||
@loadedPromise
|
||||
|
||||
showSaveDialog: (params) ->
|
||||
params = Object.assign({
|
||||
title: 'Save File',
|
||||
defaultPath: @representedDirectoryPaths[0]
|
||||
}, params)
|
||||
dialog.showSaveDialog(this, params)
|
||||
|
||||
toggleDevTools: -> @browserWindow.toggleDevTools()
|
||||
|
||||
openDevTools: -> @browserWindow.openDevTools()
|
||||
@ -275,4 +298,8 @@ class AtomWindow
|
||||
|
||||
setRepresentedFilename: (representedFilename) -> @browserWindow.setRepresentedFilename(representedFilename)
|
||||
|
||||
setRepresentedDirectoryPaths: (@representedDirectoryPaths) ->
|
||||
@representedDirectoryPaths.sort()
|
||||
@atomApplication.saveState()
|
||||
|
||||
copy: -> @browserWindow.copy()
|
||||
|
@ -22,7 +22,7 @@ class AutoUpdateManager
|
||||
setupAutoUpdater: ->
|
||||
if process.platform is 'win32'
|
||||
archSuffix = if process.arch is 'ia32' then '' else '-' + process.arch
|
||||
@feedUrl = "https://atom.io/api/updates#{archSuffix}"
|
||||
@feedUrl = "https://atom.io/api/updates#{archSuffix}?version=#{@version}"
|
||||
autoUpdater = require './auto-updater-win32'
|
||||
else
|
||||
@feedUrl = "https://atom.io/api/updates?version=#{@version}"
|
||||
|
@ -106,14 +106,14 @@ module.exports = function parseCommandLine (processArgs) {
|
||||
|
||||
if (args['resource-path']) {
|
||||
devMode = true
|
||||
resourcePath = args['resource-path']
|
||||
devResourcePath = args['resource-path']
|
||||
}
|
||||
|
||||
if (test) {
|
||||
devMode = true
|
||||
}
|
||||
|
||||
if (devMode && !resourcePath) {
|
||||
if (devMode) {
|
||||
resourcePath = devResourcePath
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,6 @@ class Project extends Model
|
||||
constructor: ({@notificationManager, packageManager, config, @applicationDelegate}) ->
|
||||
@emitter = new Emitter
|
||||
@buffers = []
|
||||
@paths = []
|
||||
@rootDirectories = []
|
||||
@repositories = []
|
||||
@directoryProviders = []
|
||||
@ -32,7 +31,9 @@ class Project extends Model
|
||||
|
||||
destroyed: ->
|
||||
buffer.destroy() for buffer in @buffers
|
||||
@setPaths([])
|
||||
repository?.destroy() for repository in @repositories
|
||||
@rootDirectories = []
|
||||
@repositories = []
|
||||
|
||||
reset: (packageManager) ->
|
||||
@emitter.dispose()
|
||||
@ -62,6 +63,9 @@ class Project extends Model
|
||||
fs.closeSync(fs.openSync(bufferState.filePath, 'r'))
|
||||
catch error
|
||||
return unless error.code is 'ENOENT'
|
||||
unless bufferState.shouldDestroyOnFileDelete?
|
||||
bufferState.shouldDestroyOnFileDelete =
|
||||
-> atom.config.get('core.closeDeletedFileTabs')
|
||||
TextBuffer.deserialize(bufferState)
|
||||
|
||||
@subscribeToBuffer(buffer) for buffer in @buffers
|
||||
@ -205,7 +209,7 @@ class Project extends Model
|
||||
removePath: (projectPath) ->
|
||||
# The projectPath may be a URI, in which case it should not be normalized.
|
||||
unless projectPath in @getPaths()
|
||||
projectPath = path.normalize(projectPath)
|
||||
projectPath = @defaultDirectoryProvider.normalizePath(projectPath)
|
||||
|
||||
indexToRemove = null
|
||||
for directory, i in @rootDirectories
|
||||
@ -233,11 +237,10 @@ class Project extends Model
|
||||
uri
|
||||
else
|
||||
if fs.isAbsolute(uri)
|
||||
path.normalize(fs.resolveHome(uri))
|
||||
|
||||
@defaultDirectoryProvider.normalizePath(fs.resolveHome(uri))
|
||||
# TODO: what should we do here when there are multiple directories?
|
||||
else if projectPath = @getPaths()[0]
|
||||
path.normalize(fs.resolveHome(path.join(projectPath, uri)))
|
||||
@defaultDirectoryProvider.normalizePath(fs.resolveHome(path.join(projectPath, uri)))
|
||||
else
|
||||
undefined
|
||||
|
||||
@ -360,9 +363,14 @@ class Project extends Model
|
||||
else
|
||||
@buildBuffer(absoluteFilePath)
|
||||
|
||||
shouldDestroyBufferOnFileDelete: ->
|
||||
atom.config.get('core.closeDeletedFileTabs')
|
||||
|
||||
# Still needed when deserializing a tokenized buffer
|
||||
buildBufferSync: (absoluteFilePath) ->
|
||||
buffer = new TextBuffer({filePath: absoluteFilePath})
|
||||
buffer = new TextBuffer({
|
||||
filePath: absoluteFilePath
|
||||
shouldDestroyOnFileDelete: @shouldDestroyBufferOnFileDelete})
|
||||
@addBuffer(buffer)
|
||||
buffer.loadSync()
|
||||
buffer
|
||||
@ -374,7 +382,9 @@ class Project extends Model
|
||||
#
|
||||
# Returns a {Promise} that resolves to the {TextBuffer}.
|
||||
buildBuffer: (absoluteFilePath) ->
|
||||
buffer = new TextBuffer({filePath: absoluteFilePath})
|
||||
buffer = new TextBuffer({
|
||||
filePath: absoluteFilePath
|
||||
shouldDestroyOnFileDelete: @shouldDestroyBufferOnFileDelete})
|
||||
@addBuffer(buffer)
|
||||
buffer.load()
|
||||
.then((buffer) -> buffer)
|
||||
|
@ -108,7 +108,10 @@ class TextEditorElement extends HTMLElement
|
||||
|
||||
buildModel: ->
|
||||
@setModel(@workspace.buildTextEditor(
|
||||
buffer: new TextBuffer(@textContent)
|
||||
buffer: new TextBuffer({
|
||||
text: @textContent
|
||||
shouldDestroyOnFileDelete:
|
||||
-> atom.config.get('core.closeDeletedFileTabs')})
|
||||
softWrapped: false
|
||||
tabLength: 2
|
||||
softTabs: true
|
||||
|
@ -622,6 +622,18 @@ class TextEditorPresenter
|
||||
return unless @scrollTop? and @lineHeight?
|
||||
|
||||
@startRow = Math.max(0, @lineTopIndex.rowForPixelPosition(@scrollTop))
|
||||
atom.assert(
|
||||
Number.isFinite(@startRow),
|
||||
'Invalid start row',
|
||||
(error) =>
|
||||
error.metadata = {
|
||||
startRow: @startRow?.toString(),
|
||||
scrollTop: @scrollTop?.toString(),
|
||||
scrollHeight: @scrollHeight?.toString(),
|
||||
clientHeight: @clientHeight?.toString(),
|
||||
lineHeight: @lineHeight?.toString()
|
||||
}
|
||||
)
|
||||
|
||||
updateEndRow: ->
|
||||
return unless @scrollTop? and @lineHeight? and @height?
|
||||
|
@ -16,12 +16,11 @@ TextEditorElement = require './text-editor-element'
|
||||
{isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require './text-utils'
|
||||
|
||||
ZERO_WIDTH_NBSP = '\ufeff'
|
||||
MAX_SCREEN_LINE_LENGTH = 500
|
||||
|
||||
# Essential: This class represents all essential editing state for a single
|
||||
# {TextBuffer}, including cursor and selection positions, folds, and soft wraps.
|
||||
# If you're manipulating the state of an editor, use this class. If you're
|
||||
# interested in the visual appearance of editors, use {TextEditorElement}
|
||||
# instead.
|
||||
# If you're manipulating the state of an editor, use this class.
|
||||
#
|
||||
# A single {TextBuffer} can belong to multiple editors. For example, if the
|
||||
# same file is open in two different panes, Atom creates a separate editor for
|
||||
@ -162,7 +161,8 @@ class TextEditor extends Model
|
||||
@softWrapAtPreferredLineLength ?= false
|
||||
@preferredLineLength ?= 80
|
||||
|
||||
@buffer ?= new TextBuffer
|
||||
@buffer ?= new TextBuffer({shouldDestroyOnFileDelete: ->
|
||||
atom.config.get('core.closeDeletedFileTabs')})
|
||||
@tokenizedBuffer ?= new TokenizedBuffer({
|
||||
grammar, tabLength, @buffer, @largeFileMode, @assert
|
||||
})
|
||||
@ -193,8 +193,9 @@ class TextEditor extends Model
|
||||
@displayLayer.setTextDecorationLayer(@tokenizedBuffer)
|
||||
@defaultMarkerLayer = @displayLayer.addMarkerLayer()
|
||||
@selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true, persistent: true)
|
||||
@selectionsMarkerLayer.trackDestructionInOnDidCreateMarkerCallbacks = true
|
||||
|
||||
@decorationManager = new DecorationManager(@displayLayer, @defaultMarkerLayer)
|
||||
@decorationManager = new DecorationManager(@displayLayer)
|
||||
@decorateMarkerLayer(@displayLayer.foldsMarkerLayer, {type: 'line-number', class: 'folded'})
|
||||
|
||||
for marker in @selectionsMarkerLayer.getMarkers()
|
||||
@ -2965,7 +2966,7 @@ class TextEditor extends Model
|
||||
else
|
||||
@getEditorWidthInChars()
|
||||
else
|
||||
Infinity
|
||||
MAX_SCREEN_LINE_LENGTH
|
||||
|
||||
###
|
||||
Section: Indentation
|
||||
|
@ -8,6 +8,8 @@ ScopeDescriptor = require './scope-descriptor'
|
||||
TokenizedBufferIterator = require './tokenized-buffer-iterator'
|
||||
NullGrammar = require './null-grammar'
|
||||
|
||||
MAX_LINE_LENGTH_TO_TOKENIZE = 500
|
||||
|
||||
module.exports =
|
||||
class TokenizedBuffer extends Model
|
||||
grammar: null
|
||||
@ -251,6 +253,8 @@ class TokenizedBuffer extends Model
|
||||
|
||||
buildTokenizedLineForRowWithText: (row, text, ruleStack = @stackForRow(row - 1), openScopes = @openScopesForRow(row)) ->
|
||||
lineEnding = @buffer.lineEndingForRow(row)
|
||||
if text.length > MAX_LINE_LENGTH_TO_TOKENIZE
|
||||
text = text.slice(0, MAX_LINE_LENGTH_TO_TOKENIZE)
|
||||
{tags, ruleStack} = @grammar.tokenizeLine(text, ruleStack, row is 0, false)
|
||||
new TokenizedLine({openScopes, text, tags, ruleStack, lineEnding, @tokenIterator})
|
||||
|
||||
|
@ -1,8 +0,0 @@
|
||||
windowLoadSettings = null
|
||||
|
||||
exports.getWindowLoadSettings = ->
|
||||
windowLoadSettings ?= JSON.parse(window.decodeURIComponent(window.location.hash.substr(1)))
|
||||
|
||||
exports.setWindowLoadSettings = (settings) ->
|
||||
windowLoadSettings = settings
|
||||
location.hash = encodeURIComponent(JSON.stringify(settings))
|
@ -182,7 +182,7 @@ class Workspace extends Model
|
||||
projectPath = _.find projectPaths, (projectPath) ->
|
||||
itemPath is projectPath or itemPath?.startsWith(projectPath + path.sep)
|
||||
itemTitle ?= "untitled"
|
||||
projectPath ?= if itemPath then path.dirname(itemPath) else null
|
||||
projectPath ?= if itemPath then path.dirname(itemPath) else projectPaths[0]
|
||||
if projectPath?
|
||||
projectPath = fs.tildify(projectPath)
|
||||
|
||||
|
@ -1,7 +1,16 @@
|
||||
{
|
||||
"breakConfig": true,
|
||||
"sourceMap": "inline",
|
||||
"blacklist": ["es6.forOf", "useStrict"],
|
||||
"optional": ["asyncToGenerator"],
|
||||
"stage": 0
|
||||
"plugins": [
|
||||
["add-module-exports", {}],
|
||||
["transform-async-to-generator", {}],
|
||||
["transform-decorators-legacy", {}],
|
||||
["transform-class-properties", {}],
|
||||
["transform-es2015-modules-commonjs", {"strictMode": false}],
|
||||
["transform-export-extensions", {}],
|
||||
["transform-do-expressions", {}],
|
||||
["transform-function-bind", {}],
|
||||
["transform-object-rest-spread", {}],
|
||||
["transform-flow-strip-types", {}],
|
||||
["transform-react-jsx", {}]
|
||||
]
|
||||
}
|
||||
|
@ -2,9 +2,8 @@
|
||||
var path = require('path')
|
||||
var FileSystemBlobStore = require('../src/file-system-blob-store')
|
||||
var NativeCompileCache = require('../src/native-compile-cache')
|
||||
var getWindowLoadSettings = require('../src/get-window-load-settings')
|
||||
|
||||
var loadSettings = null
|
||||
var loadSettingsError = null
|
||||
var blobStore = null
|
||||
|
||||
window.onload = function () {
|
||||
@ -25,20 +24,16 @@
|
||||
// Normalize to make sure drive letter case is consistent on Windows
|
||||
process.resourcesPath = path.normalize(process.resourcesPath)
|
||||
|
||||
if (loadSettingsError) {
|
||||
throw loadSettingsError
|
||||
}
|
||||
|
||||
var devMode = loadSettings.devMode || !loadSettings.resourcePath.startsWith(process.resourcesPath + path.sep)
|
||||
var devMode = getWindowLoadSettings().devMode || !getWindowLoadSettings().resourcePath.startsWith(process.resourcesPath + path.sep)
|
||||
|
||||
if (devMode) {
|
||||
setupDeprecatedPackages()
|
||||
}
|
||||
|
||||
if (loadSettings.profileStartup) {
|
||||
profileStartup(loadSettings, Date.now() - startTime)
|
||||
if (getWindowLoadSettings().profileStartup) {
|
||||
profileStartup(Date.now() - startTime)
|
||||
} else {
|
||||
setupWindow(loadSettings)
|
||||
setupWindow()
|
||||
setLoadTime(Date.now() - startTime)
|
||||
}
|
||||
} catch (error) {
|
||||
@ -61,23 +56,23 @@
|
||||
console.error(error.stack || error)
|
||||
}
|
||||
|
||||
function setupWindow (loadSettings) {
|
||||
function setupWindow () {
|
||||
var CompileCache = require('../src/compile-cache')
|
||||
CompileCache.setAtomHomeDirectory(process.env.ATOM_HOME)
|
||||
|
||||
var ModuleCache = require('../src/module-cache')
|
||||
ModuleCache.register(loadSettings)
|
||||
ModuleCache.add(loadSettings.resourcePath)
|
||||
ModuleCache.register(getWindowLoadSettings())
|
||||
ModuleCache.add(getWindowLoadSettings().resourcePath)
|
||||
|
||||
// By explicitly passing the app version here, we could save the call
|
||||
// of "require('remote').require('app').getVersion()".
|
||||
var startCrashReporter = require('../src/crash-reporter-start')
|
||||
startCrashReporter({_version: loadSettings.appVersion})
|
||||
startCrashReporter({_version: getWindowLoadSettings().appVersion})
|
||||
|
||||
setupVmCompatibility()
|
||||
setupCsonCache(CompileCache.getCacheDirectory())
|
||||
|
||||
var initialize = require(loadSettings.windowInitializationScript)
|
||||
var initialize = require(getWindowLoadSettings().windowInitializationScript)
|
||||
return initialize({blobStore: blobStore}).then(function () {
|
||||
require('electron').ipcRenderer.send('window-command', 'window:loaded')
|
||||
})
|
||||
@ -105,11 +100,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
function profileStartup (loadSettings, initialTime) {
|
||||
function profileStartup (initialTime) {
|
||||
function profile () {
|
||||
console.profile('startup')
|
||||
var startTime = Date.now()
|
||||
setupWindow(loadSettings).then(function () {
|
||||
setupWindow().then(function () {
|
||||
setLoadTime(Date.now() - startTime + initialTime)
|
||||
console.profileEnd('startup')
|
||||
console.log('Switch to the Profiles tab to view the created startup profile')
|
||||
@ -125,16 +120,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function parseLoadSettings () {
|
||||
var rawLoadSettings = decodeURIComponent(window.location.hash.substr(1))
|
||||
try {
|
||||
loadSettings = JSON.parse(rawLoadSettings)
|
||||
} catch (error) {
|
||||
console.error('Failed to parse load settings: ' + rawLoadSettings)
|
||||
loadSettingsError = error
|
||||
}
|
||||
}
|
||||
|
||||
var setupAtomHome = function () {
|
||||
if (process.env.ATOM_HOME) {
|
||||
return
|
||||
@ -143,11 +128,10 @@
|
||||
// Ensure ATOM_HOME is always set before anything else is required
|
||||
// This is because of a difference in Linux not inherited between browser and render processes
|
||||
// https://github.com/atom/atom/issues/5412
|
||||
if (loadSettings && loadSettings.atomHome) {
|
||||
process.env.ATOM_HOME = loadSettings.atomHome
|
||||
if (getWindowLoadSettings() && getWindowLoadSettings().atomHome) {
|
||||
process.env.ATOM_HOME = getWindowLoadSettings().atomHome
|
||||
}
|
||||
}
|
||||
|
||||
parseLoadSettings()
|
||||
setupAtomHome()
|
||||
})()
|
||||
|
@ -1,30 +1,32 @@
|
||||
@import "ui-variables";
|
||||
|
||||
@title-bar-height: 22px;
|
||||
@traffic-lights-width: 68px;
|
||||
|
||||
@inset-title-bar-height: 38px;
|
||||
@inset-traffic-lights-width: 78px;
|
||||
|
||||
@title-bar-text-size: 13px;
|
||||
@title-bar-height: 23px;
|
||||
@title-bar-background-color: @base-background-color;
|
||||
@title-bar-border-color: @base-border-color;
|
||||
|
||||
body.fullscreen .title-bar {
|
||||
margin-top: -@title-bar-height;
|
||||
}
|
||||
|
||||
// Title Bar -------------------------------
|
||||
|
||||
.title-bar {
|
||||
height: @title-bar-height;
|
||||
transition: margin-top 160ms;
|
||||
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
overflow: hidden;
|
||||
box-sizing: content-box;
|
||||
font-size: @title-bar-text-size;
|
||||
background-color: @title-bar-background-color;
|
||||
border-bottom: 1px solid @title-bar-border-color;
|
||||
transition: margin-top 160ms;
|
||||
-webkit-user-select: none;
|
||||
-webkit-app-region: drag;
|
||||
|
||||
padding: 0 70px;
|
||||
overflow: hidden;
|
||||
|
||||
.title {
|
||||
flex: 0 1 auto;
|
||||
overflow: hidden;
|
||||
@ -32,10 +34,54 @@ body.fullscreen .title-bar {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
background-color: @title-bar-background-color;
|
||||
border-bottom: 1px solid @title-bar-border-color;
|
||||
|
||||
.is-blurred & {
|
||||
color: @text-color-subtle;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Custom -------------------------------
|
||||
|
||||
.custom-title-bar {
|
||||
.title-bar {
|
||||
height: @title-bar-height;
|
||||
padding-left: @traffic-lights-width;
|
||||
padding-right: @traffic-lights-width;
|
||||
}
|
||||
|
||||
&.fullscreen .title-bar {
|
||||
margin-top: -@title-bar-height; // hide title bar in fullscreen mode
|
||||
}
|
||||
|
||||
atom-panel.modal {
|
||||
top: @title-bar-height; // Move modals down
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Custom Inset -------------------------------
|
||||
|
||||
.custom-inset-title-bar {
|
||||
.title-bar {
|
||||
height: @inset-title-bar-height;
|
||||
padding-left: @inset-traffic-lights-width;
|
||||
padding-right: @inset-traffic-lights-width;
|
||||
}
|
||||
|
||||
&.fullscreen .title-bar {
|
||||
margin-top: -@inset-title-bar-height; // hide title bar in fullscreen mode
|
||||
}
|
||||
|
||||
atom-panel.modal {
|
||||
top: @inset-title-bar-height; // Move modals down
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Hidden -------------------------------
|
||||
|
||||
.hidden-title-bar {
|
||||
.status-bar {
|
||||
-webkit-app-region: drag; // Enable dragging
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user