Page MenuHomePhorge

No OneTemporary

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

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)

Event Timeline