Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Signals (inter-instance communication) #825

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -286,16 +286,19 @@
((rule_set) @meta.selector.css
(#set! adjust.endBeforeFirstMatchOf "{"))

; Scope the inside of a media query block so that tooling doesn't mistake it
; for a property/value pair.
(keyword_query) @meta.media-query.css
((feature_query) @meta.media-query.css
(#set! adjust.startAfterFirstMatchOf "^\\(")
(#set! adjust.endBeforeFirstMatchOf "\\)$"))

(parenthesized_query) @meta.media-query.css


; META
; ====

[
(plain_value)
(integer_value)
(string_value)
] @meta.property-value.css

; `!important` starts out as an ERROR node as it's being typed, but we need it
; to be recognized as a possible property value for `autocomplete-css` to be
; able to complete it. This should match only when it comes at the end of a
Expand Down
45 changes: 23 additions & 22 deletions packages/language-gfm/lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,30 +38,31 @@ exports.activate = () => {
includeChildren: true
});

// Highlight inline HTML within paragraphs.
atom.grammars.addInjectionPoint('source.gfm.embedded', {
type: 'paragraph',
language(node) {
let html = node.descendantsOfType([
'html_open_tag',
'html_close_tag',
'html_self_closing_tag'
]);
if (html.length === 0) { return null; }
return 'html';
},
for (let nodeType of ['paragraph', 'table_cell']) {
atom.grammars.addInjectionPoint('source.gfm.embedded', {
type: nodeType,
language(node) {
let html = node.descendantsOfType([
'html_open_tag',
'html_close_tag',
'html_self_closing_tag'
]);
if (html.length === 0) { return null; }
return 'html';
},

content(node) {
let html = node.descendantsOfType([
'html_open_tag',
'html_close_tag',
'html_self_closing_tag'
]);
return html;
},
content(node) {
let html = node.descendantsOfType([
'html_open_tag',
'html_close_tag',
'html_self_closing_tag'
]);
return html;
},

includeChildren: true
});
includeChildren: true
});
}

// All code blocks of the form
//
Expand Down
6 changes: 4 additions & 2 deletions packages/language-javascript/grammars/ts/folds.scm
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@
; we want the folds to work a little differently so that collapsing the `if`
; fold doesn't interfere with our ability to collapse the `else` fold.
((if_statement
consequence: (statement_block) @fold)
(#set! fold.adjustToEndOfPreviousRow true))
consequence: (statement_block) @fold
alternative: (else_clause)
(#set! fold.adjustToEndOfPreviousRow true)
))

(else_clause (statement_block) @fold)

Expand Down
11 changes: 11 additions & 0 deletions packages/language-python/grammars/ts/highlights.scm
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@
; An entire decorator without arguments, like `@foo`.
(decorator "@" (identifier) .) @support.other.function.decorator.python

; A namespaced decorator, like the `@foo` in `@foo.bar`, with or without a
; function invocation.
((decorator [(attribute) (call)]) @support.other.function.decorator.python
(#set! adjust.endAfterFirstMatchOf "\\."))

; The "@" and "foo" together in a decorator with arguments, like `@foo(True)`.
((decorator "@" (call function: (identifier))) @support.other.function.decorator.python
(#set! adjust.endAt firstNamedChild.firstNamedChild.endPosition))
Expand Down Expand Up @@ -331,6 +336,10 @@
(assignment
left: (identifier) @variable.other.assignment.python)

; The "a" and "b" in `a, b = 2, 3`.
(assignment
left: (pattern_list
(identifier) @variable.other.assignment.python))

; OPERATORS
; =========
Expand Down Expand Up @@ -425,6 +434,8 @@
"," @punctuation.separator.parameters.comma.python
(#set! capture.final true))

(pattern_list "," @punctuation.separator.destructuring.comma.python)

(argument_list
"(" @punctuation.definition.arguments.begin.bracket.round.python
")" @punctuation.definition.arguments.end.bracket.round.python
Expand Down
6 changes: 5 additions & 1 deletion src/atom-environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const DeserializerManager = require('./deserializer-manager');
const ViewRegistry = require('./view-registry');
const NotificationManager = require('./notification-manager');
const Config = require('./config');
const SignalManager = require('./signal-manager');
const KeymapManager = require('./keymap-extensions');
const TooltipManager = require('./tooltip-manager');
const CommandRegistry = require('./command-registry');
Expand Down Expand Up @@ -105,6 +106,9 @@ class AtomEnvironment {
properties: _.clone(ConfigSchema)
});

/** @type {SignalManager} */
this.signal = new SignalManager();

/** @type {KeymapManager} */
this.keymaps = new KeymapManager({
notificationManager: this.notifications
Expand Down Expand Up @@ -1746,7 +1750,7 @@ or use Pane::saveItemAs for programmatic saving.`);
}

resolveProxy(url) {
return new Promise((resolve, reject) => {
return new Promise((resolve) => {
const requestId = this.nextProxyRequestId++;
const disposable = this.applicationDelegate.onDidResolveProxy(
(id, proxy) => {
Expand Down
10 changes: 7 additions & 3 deletions src/main-process/atom-application.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const {
shell,
screen
} = require('electron');
const signalBroker = require('./signal-broker');
const { CompositeDisposable, Disposable } = require('event-kit');
const crypto = require('crypto');
const fs = require('fs-plus');
Expand Down Expand Up @@ -263,6 +264,9 @@ module.exports = class AtomApplication extends EventEmitter {

const result = await this.launch(options);

signalBroker.start();
this.disposable.add(() => signalBroker.stop());

StartupTime.addMarker('main-process:atom-application:initialize:end');

return result;
Expand Down Expand Up @@ -372,7 +376,7 @@ module.exports = class AtomApplication extends EventEmitter {
if (this.getAllWindows().length === 0) {
console.log("Quitting.");
app.quit();
};
}
} else if (
(pathsToOpen && pathsToOpen.length > 0) ||
(foldersToOpen && foldersToOpen.length > 0)
Expand Down Expand Up @@ -651,7 +655,7 @@ module.exports = class AtomApplication extends EventEmitter {
const window = this.focusedWindow();
if (window) window.minimize();
});
this.on('application:zoom', function() {
this.on('application:zoom', function () {
const window = this.focusedWindow();
if (window) window.maximize();
});
Expand Down Expand Up @@ -1681,7 +1685,7 @@ module.exports = class AtomApplication extends EventEmitter {

const timeoutInSeconds = Number.parseFloat(timeout);
if (!Number.isNaN(timeoutInSeconds)) {
const timeoutHandler = function() {
const timeoutHandler = function () {
console.log(
`The test suite has timed out because it has been running for more than ${timeoutInSeconds} seconds.`
);
Expand Down
49 changes: 49 additions & 0 deletions src/main-process/signal-broker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const { ipcMain } = require('electron');

// The easiest way to send a message from the main Electron process to a
// renderer process is via a `WebContents` instance. That instance is sent to
// use as part of the event metadata whenever a renderer process sends a
// message to the main process. So we'll keep track of each of these instances
// in a set. A new window is responsible for sending us a message upon
// initialization so that we can keep track of it.
const instances = new Set;
let initialized = false;

function start() {
if (initialized) return;

ipcMain.on('signal-register', (event) => {
// The first time we hear from a window, we'll add it to our instance list.
instances.add(event.sender);
});

ipcMain.on('signal-message', (event, bundle, { includeSelf = false } = {}) => {
let { sender } = event;

for (let instance of instances) {
if (sender === instance && !includeSelf) continue;
// We could require instances to unregister themselves when they close,
// but it's just as easy for us to do a quick check before we send a
// message. If this instance is stale, we can remove it from the list.
if (instance?.isDestroyed()) {
instances.delete(instance);
continue;
}

try {
instance.send('signal-message-reply', bundle);
} catch (err) {
instances.delete(instance);
}
}
});
initialized = true;
}

function stop() {
ipcMain.removeHandler('signal-register');
ipcMain.removeHandler('signal-message');
initialized = false;
}

module.exports = { start, stop };
101 changes: 101 additions & 0 deletions src/signal-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
const { Emitter } = require('event-kit');
const { ipcRenderer } = require('electron');

// Used to send messages between different Pulsar windows.
//
// Every Pulsar window is a new instance of a web page at its core. Hence each
// one is isolated and has no easy way to communicate with other windows.
//
// To make this possible, `SignalManager` uses the main process as a broker
// that forwards messages from one window to all windows.
//
// ## Usage
//
// Suppose you need to share data between two windows in a reactive way. For
// instance, if someone changes a value in one window, another Pulsar window
// needs to know about it. You could use `atom.config` for this, but that'd
// result in changes to the user's `config.cson`, which may not be appropriate.
//
// Instead, you can send a signal from one window to all the others:
//
// ```js
// atom.signal.send('my-package', { data: someData });
// ```
//
// By default, all windows _other than_ the window that sent the message will
// receive this message in the `my-package` channel.
//
// If _all_ windows should react to a message, even the window that sent it,
// you can use an `includeSelf` option:
//
// ```js
// atom.signal.send(
// 'my-package',
// { data: someData },
// { includeSelf: true }
// );
// ```
//
// To have windows act on that message, you can set up a listener:
//
// ```js
// atom.signal.onMessage('my-package', (message) => {
// synchronize(message.data);
// });
// ```
//

class SignalManager {
constructor(storage = window.localStorage) {
this.clear();
this.storage = storage;
}

clear() {
this.emitter = new Emitter();
ipcRenderer.send('signal-register');
ipcRenderer.on('signal-message-reply', (event, bundle) => {
console.warn('Received message:', bundle);
let { channel, message } = bundle;
this.emitter.emit(`message-${channel}`, message);
this.emitter.emit('message', bundle);
});
}

destroy() {
ipcRenderer.removeAllListeners('signal-message-reply');
}

// Add a listener for messages on a given channel. If `channel` is `null`,
// your callback will be called when a message is sent on any channel.
//
// * `channel` (optional) {String} name of the channel to listen on, or
// `null` to listen on all channels.
// * `callback` {Function} to call when a message is received on the given
// channel. If listening on one channel, argument will be the value sent.
// If listening on all channels, argument will be an object with keys
// `channel` and `message`.
onMessage(channel, callback) {
if (channel) {
return this.emitter.on(`message-${channel}`, callback);
} else {
return this.emitter.on(`message`, callback);
}
}

// Send a message on a given channel.
//
// * `channel` {String} name of the channel to send on.
// * `message` the message to be sent. Can be any serializable value or object.
// * `options` (optional) {Object}
// * `includeSelf` (optional) {Boolean} whether to send the message to the
// same window that originated it. Defaults to `false`, in which case
// only the non-originating windows receive the message. If `true`, all
// windows receive the message.
send(channel, message, { includeSelf = false } = {}) {
let bundle = { channel, message, includeSelf };
ipcRenderer.send('signal-message', bundle);
}
}

module.exports = SignalManager;
6 changes: 6 additions & 0 deletions src/wasm-tree-sitter-language-mode.js
Original file line number Diff line number Diff line change
Expand Up @@ -3852,6 +3852,12 @@ class LanguageLayer {
range = range.union(new Range(earliest, latest));
}

// Why do we have to do this explicitly? Because `descendantsOfType` will
// incorrectly return nodes if the range runs from (0, 0) to (0, 0). All
// other empty ranges seem not to have this problem. Upon cursory
// inspection, this bug doesn't seem to be limited to `web-tree-sitter`.
if (range.isEmpty()) { return; }

// Now that we've enlarged the range, we might have more existing injection
// markers to consider. But check for containment rather than intersection
// so that we don't have to enlarge it again.
Expand Down
Loading