Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2893828
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
15 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/applications/settings/setting/PhabricatorEditorSetting.php b/src/applications/settings/setting/PhabricatorEditorSetting.php
index 6884217a3e..a0f1b43c95 100644
--- a/src/applications/settings/setting/PhabricatorEditorSetting.php
+++ b/src/applications/settings/setting/PhabricatorEditorSetting.php
@@ -1,54 +1,54 @@
<?php
final class PhabricatorEditorSetting
extends PhabricatorStringSetting {
const SETTINGKEY = 'editor';
public function getSettingName() {
return pht('Editor Link');
}
public function getSettingPanelKey() {
return PhabricatorExternalEditorSettingsPanel::PANELKEY;
}
protected function getSettingOrder() {
return 300;
}
protected function getControlInstructions() {
return pht(
"Many text editors can be configured as URI handlers for special ".
"protocols like `editor://`. If you have installed and configured ".
"such an editor, Phabricator can generate links that you can click ".
"to open files locally.".
"\n\n".
"Provide a URI pattern for building external editor URIs in your ".
"environment. For example, if you use TextMate on macOS, the pattern ".
- "for your machine look like this:".
+ "for your machine may look something like this:".
"\n\n".
"```name=\"Example: TextMate on macOS\"\n".
"%s\n".
"```\n".
"\n\n".
"For complete instructions on editor configuration, ".
"see **[[ %s | %s ]]**.".
"\n\n".
"See the tables below for a list of supported variables and protocols.",
- 'txmt://open/?url=file:///Users/alincoln/editor_links/%r/%f&line=%l',
+ 'txmt://open/?url=file:///Users/alincoln/editor_links/%n/%f&line=%l',
PhabricatorEnv::getDoclink('User Guide: Configuring an External Editor'),
pht('User Guide: Configuring an External Editor'));
}
public function validateTransactionValue($value) {
if (!strlen($value)) {
return;
}
id(new PhabricatorEditorURIEngine())
->setPattern($value)
->validatePattern();
}
}
diff --git a/src/docs/user/userguide/external_editor.diviner b/src/docs/user/userguide/external_editor.diviner
index 07aa9db290..ce39ad6610 100644
--- a/src/docs/user/userguide/external_editor.diviner
+++ b/src/docs/user/userguide/external_editor.diviner
@@ -1,44 +1,78 @@
@title User Guide: Configuring an External Editor
@group userguide
Setting up an external editor to integrate with Diffusion and Differential.
Overview
========
-You can configure a URI handler to allow you to open files from Differential
-and Diffusion in your preferred text editor.
+You can configure a URI handler to allow you to open files referenced in
+Differential and Diffusion in your preferred text editor on your local
+machine.
+
Configuring Editors
===================
To configure an external editor, go to {nav Settings > Application Settings >
External Editor} and set "Editor Link" to a URI pattern (see below). This
will enable an "Open in Editor" link in Differential, and an "Edit" button in
Diffusion.
-In general, you'll set this field to something like:
+In general, you'll set this field to something like this, although the
+particular pattern to use depends on your editor and environment:
```lang=uri
editor://open/?file=%f
```
+
+Mapping Repositories
+====================
+
+When you open a file in an external editor, Phabricator needs to be able to
+build a URI which includes the correct absolute path on disk to the local
+version of the file, including the repository directory.
+
+If all your repositories are named consistently in a single directory, you
+may be able to use the `%n` (repository short name) variable to do this.
+For example:
+
+```lang=uri
+editor://open/?file=/Users/alice/repositories/%n/%f
+```
+
+If your repositories aren't named consistently or aren't in a single location,
+you can build a local directory of symlinks which map a repositoriy identifier
+to the right location on disk:
+
+```
+/Users/alice/editor_links/ $ ls -l
+... search-service/ -> /Users/alice/backend/search/
+... site-templates/ -> /Users/alice/frontend/site/
+```
+
+Then use this directory in your editor URI:
+
+```lang=uri
+editor://open/?file=/Users/alice/editor_links/%n/%f
+```
+
+Instead of `%n` (repository short name), you can also use `%d` (repository ID)
+or `%p` (repository PHID). These identifiers are immutable and all repositories
+always have both identifiers, but they're less human-readable.
+
+
Configuring: TextMate on macOS
==============================
TextMate installs a `txmt://` handler by default, so it's easy to configure
this feature if you use TextMate.
-First, create a local directory with symlinks for each repository callsign. For
-example, if you're developing Phabricator, it might look like this:
-
- /Users/alincoln/editor_links/ $ ls -l
- ... ARC -> /Users/alincoln/workspace/arcanist/
- ... P -> /Users/alincoln/workspace/phabricator/
- ... PHU -> /Users/alincoln/workspace/libphutil/
-
-Then set your "Editor Link" to:
+First, identify the parent directory where your repositories are stored
+(for example, `/Users/alice/repositories/`). Then, configure your editor
+pattern like this:
```lang=uri
-txmt://open/?url=file:///Users/alincoln/editor_links/%r/%f&line=%l
+txmt://open/?url=file:///Users/alice/repositories/%n/%f&line=%l
```
diff --git a/src/infrastructure/editor/PhabricatorEditorURIEngine.php b/src/infrastructure/editor/PhabricatorEditorURIEngine.php
index 6abfa98677..bf0c1d1b4b 100644
--- a/src/infrastructure/editor/PhabricatorEditorURIEngine.php
+++ b/src/infrastructure/editor/PhabricatorEditorURIEngine.php
@@ -1,339 +1,354 @@
<?php
final class PhabricatorEditorURIEngine
extends Phobject {
private $viewer;
private $repository;
private $pattern;
private $rawTokens;
private $repositoryTokens;
public static function newForViewer(PhabricatorUser $viewer) {
if (!$viewer->isLoggedIn()) {
return null;
}
$pattern = $viewer->getUserSetting(PhabricatorEditorSetting::SETTINGKEY);
if (!strlen(trim($pattern))) {
return null;
}
$engine = id(new self())
->setViewer($viewer)
->setPattern($pattern);
// If there's a problem with the pattern,
try {
$engine->validatePattern();
} catch (PhabricatorEditorURIParserException $ex) {
return null;
}
return $engine;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function getRepository() {
return $this->repository;
}
public function setPattern($pattern) {
$this->pattern = $pattern;
return $this;
}
public function getPattern() {
return $this->pattern;
}
public function validatePattern() {
$this->getRawURITokens();
return true;
}
public function getURIForPath($path, $line) {
$tokens = $this->getURITokensForRepository();
$variables = array(
'f' => $this->escapeToken($path),
'l' => $this->escapeToken($line),
);
$tokens = $this->newTokensWithVariables($tokens, $variables);
return $this->newStringFromTokens($tokens);
}
public function getURITokensForRepository() {
if (!$this->repositoryTokens) {
$this->repositoryTokens = $this->newURITokensForRepository();
}
return $this->repositoryTokens;
}
public static function getVariableDefinitions() {
return array(
- '%' => array(
- 'name' => pht('Literal Percent Symbol'),
- 'example' => '%',
- ),
- 'r' => array(
- 'name' => pht('Repository Callsign'),
- 'example' => 'XYZ',
- ),
'f' => array(
'name' => pht('File Name'),
'example' => pht('path/to/source.c'),
),
'l' => array(
'name' => pht('Line Number'),
'example' => '777',
),
+ 'n' => array(
+ 'name' => pht('Repository Short Name'),
+ 'example' => 'arcanist',
+ ),
+ 'd' => array(
+ 'name' => pht('Repository ID'),
+ 'example' => '42',
+ ),
+ 'p' => array(
+ 'name' => pht('Repository PHID'),
+ 'example' => 'PHID-REPO-abcdefghijklmnopqrst',
+ ),
+ 'r' => array(
+ 'name' => pht('Repository Callsign'),
+ 'example' => 'XYZ',
+ ),
+ '%' => array(
+ 'name' => pht('Literal Percent Symbol'),
+ 'example' => '%',
+ ),
);
}
private function newURITokensForRepository() {
$tokens = $this->getRawURITokens();
$repository = $this->getRepository();
if (!$repository) {
throw new PhutilInvalidStateException('setRepository');
}
$variables = array(
'r' => $this->escapeToken($repository->getCallsign()),
+ 'n' => $this->escapeToken($repository->getRepositorySlug()),
+ 'd' => $this->escapeToken($repository->getID()),
+ 'p' => $this->escapeToken($repository->getPHID()),
);
return $this->newTokensWithVariables($tokens, $variables);
}
private function getRawURITokens() {
if (!$this->rawTokens) {
$this->rawTokens = $this->newRawURITokens();
}
return $this->rawTokens;
}
private function newRawURITokens() {
$raw_pattern = $this->getPattern();
$raw_tokens = self::newPatternTokens($raw_pattern);
$variable_definitions = self::getVariableDefinitions();
foreach ($raw_tokens as $token) {
if ($token['type'] !== 'variable') {
continue;
}
$value = $token['value'];
if (isset($variable_definitions[$value])) {
continue;
}
throw new PhabricatorEditorURIParserException(
pht(
'Editor pattern "%s" is invalid: the pattern contains an '.
'unrecognized variable ("%s"). Use "%%%%" to encode a literal '.
'percent symbol.',
$raw_pattern,
'%'.$value));
}
$variables = array(
'%' => '%',
);
$tokens = $this->newTokensWithVariables($raw_tokens, $variables);
$first_literal = null;
if ($tokens) {
foreach ($tokens as $token) {
if ($token['type'] === 'literal') {
$first_literal = $token['value'];
}
break;
}
if ($first_literal === null) {
throw new PhabricatorEditorURIParserException(
pht(
'Editor pattern "%s" is invalid: the pattern must begin with '.
'a valid editor protocol, but begins with a variable. This is '.
'very sneaky and also very forbidden.',
$raw_pattern));
}
}
$uri = new PhutilURI($first_literal);
$editor_protocol = $uri->getProtocol();
if (!$editor_protocol) {
throw new PhabricatorEditorURIParserException(
pht(
'Editor pattern "%s" is invalid: the pattern must begin with '.
'a valid editor protocol, but does not begin with a recognized '.
'protocol string.',
$raw_pattern));
}
$allowed_key = 'uri.allowed-editor-protocols';
$allowed_protocols = PhabricatorEnv::getEnvConfig($allowed_key);
if (empty($allowed_protocols[$editor_protocol])) {
throw new PhabricatorEditorURIParserException(
pht(
'Editor pattern "%s" is invalid: the pattern must begin with '.
'a valid editor protocol, but the protocol "%s://" is not allowed.',
$raw_pattern,
$editor_protocol));
}
return $tokens;
}
private function newTokensWithVariables(array $tokens, array $variables) {
// Replace all "variable" tokens that we have replacements for with
// the literal value.
foreach ($tokens as $key => $token) {
$type = $token['type'];
if ($type == 'variable') {
$variable = $token['value'];
if (isset($variables[$variable])) {
$tokens[$key] = array(
'type' => 'literal',
'value' => $variables[$variable],
);
}
}
}
// Now, merge sequences of adjacent "literal" tokens into a single token.
$last_literal = null;
foreach ($tokens as $key => $token) {
$is_literal = ($token['type'] === 'literal');
if (!$is_literal) {
$last_literal = null;
continue;
}
if ($last_literal !== null) {
$tokens[$key]['value'] =
$tokens[$last_literal]['value'].$token['value'];
unset($tokens[$last_literal]);
}
$last_literal = $key;
}
return $tokens;
}
private function escapeToken($token) {
// Paths are user controlled, so a clever user could potentially make
// editor links do surprising things with paths containing "/../".
// Find anything that looks like "/../" and mangle it.
$token = preg_replace('((^|/)\.\.(/|\z))', '\1dot-dot\2', $token);
return phutil_escape_uri($token);
}
private function newStringFromTokens(array $tokens) {
$result = array();
foreach ($tokens as $token) {
$token_type = $token['type'];
$token_value = $token['value'];
$is_literal = ($token_type === 'literal');
if (!$is_literal) {
throw new Exception(
pht(
'Editor pattern token list can not be converted into a string: '.
'it still contains a non-literal token ("%s", of type "%s").',
$token_value,
$token_type));
}
$result[] = $token_value;
}
$result = implode('', $result);
return $result;
}
public static function newPatternTokens($raw_pattern) {
$token_positions = array();
$len = strlen($raw_pattern);
for ($ii = 0; $ii < $len; $ii++) {
$c = $raw_pattern[$ii];
if ($c === '%') {
if (!isset($raw_pattern[$ii + 1])) {
throw new PhabricatorEditorURIParserException(
pht(
'Editor pattern "%s" is invalid: the final character in a '.
'pattern may not be an unencoded percent symbol ("%%"). '.
'Use "%%%%" to encode a literal percent symbol.',
$raw_pattern));
}
$token_positions[] = $ii;
$ii++;
}
}
// Add a final marker past the end of the string, so we'll collect any
// trailing literal bytes.
$token_positions[] = $len;
$tokens = array();
$cursor = 0;
foreach ($token_positions as $pos) {
$token_len = ($pos - $cursor);
if ($token_len > 0) {
$tokens[] = array(
'type' => 'literal',
'value' => substr($raw_pattern, $cursor, $token_len),
);
}
$cursor = $pos;
if ($cursor < $len) {
$tokens[] = array(
'type' => 'variable',
'value' => substr($raw_pattern, $cursor + 1, 1),
);
}
$cursor = $pos + 2;
}
return $tokens;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Jan 19, 19:07 (1 w, 3 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1127698
Default Alt Text
(15 KB)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment