21
$\begingroup$

[Feature added in version 13.1]

Is there some way I can change Mathematica keyboard behavior so I could select a block of text/code, press tab, and the selection would be indented to right? Similarly, I would like shift + tab to produce indenting to the left.

If it were possible to get this behavior just for multi-line string text, or just in code cells, that would be great, too.

$\endgroup$
3
  • 3
    $\begingroup$ Normal mma cells don't have a clear notion of whitespace.. Tab works in code cells, but not for selected blocks, just as the first char of the line. If you're asking such questions, it's usually time to switch to WorkBench. Once I did my life became much easier:) For code cells this would still be very useful though (+1). $\endgroup$
    – Ajasja
    Commented Jan 6, 2014 at 5:40
  • $\begingroup$ Hi @Ajasja. All my current important code is inside code cells or in packages. I tried workbench, but did not get used to it. I love the new autocomplete interface, collapse and expand code parts and the code execution in place for test, and it's not nice in Workbench. But maybe I have to try Workbench again. $\endgroup$
    – Murta
    Commented Jan 6, 2014 at 10:34
  • $\begingroup$ Thanks for the accept. So, is my solution really usable for you? It is certainly not refined, and needs some more work to be robust and general. $\endgroup$ Commented Feb 22, 2014 at 16:19

4 Answers 4

10
$\begingroup$

The following solution seems to work reasonably well, at least on a few examples I have tested. It will be in the spirit of the one I gave to a rather similar earlier question you asked. I wasn't able to make the Tab key work, instead I bound the indenting to the CTRL+ ` combination - but in practice this is almost as easy as pressing Tab key. Also, the following is only a solution for indenting to the right. Indenting to the left is likely also possible, but might be a bit trickier to implement.

Here is the code. This is a generic function to construct a self-overwriting cell:

ClearAll[generateAutoOverwriteCell];
generateAutoOverwriteCell[
   boxes : Except[_?OptionQ] : "", 
   type_String: "Input", 
   opts___?OptionQ
] :=
Module[{},
   SelectionMove[EvaluationNotebook[], All, Cell, AutoScroll -> False];
   SelectionMove[EvaluationNotebook[], Previous, Cell, AutoScroll -> False]; 
   NotebookWrite[EvaluationNotebook[], Cell[BoxData[boxes], type, opts], All]; 
   SelectionMove[EvaluationNotebook[], All, CellContents, AutoScroll -> False]; 
   SelectionMove[EvaluationNotebook[], Previous, Character];
]

This is a relatively simple "formatter" for the selected part of your code:

ClearAll[process, $newlinePattern, $inner];

$newlinePattern = ("\n"|"\[IndentingNewLine]");

process[r_RowBox/;FreeQ[r,$newlinePattern]]:=RowBox[{"\t",r}];

process[x_]:=process[x,False];

process[RowBox[conts_List],flag_]:= RowBox[process[conts,flag]];

process[{left__,sep:$newlinePattern, right___}, flag_]:=
  {
     RowBox[{If[!TrueQ[$inner],"\t",Sequence@@{}],Sequence@@#}] & [
	     Block[{$inner = True},process[{left},False]]
     ]
     ,
     sep
     ,
     Sequence @@ Block[{$inner = False},process[{right}, True]]
  };

process[{args__}/;FreeQ[{args},$newlinePattern],True]:={RowBox[{"\t",args}]};

process[x_,_]:=x/.block:{__,$newlinePattern, ___}:>process[block,False];

This is the key action rule, which I bound to the CTRL+ ` combination:

ClearAll[selectionTabRule]
selectionTabRule = 
   {"KeyDown", "`"} :>
      With[{nb = InputNotebook[]}, 
        If[MemberQ[CurrentValue["ModifierKeys"], "Control"],
           With[{sel = NotebookRead[nb]},
              NotebookWrite[nb, process[sel]]
           ],
           (* else *)
           NotebookWrite[nb, "`"]
        ]
      ];

Finally, here is the short-cut to construct such cells:

ClearAll[incell];
incell := generateAutoOverwriteCell[CellEventActions -> {selectionTabRule}];

Now, if you type incell into a new cell and evaluate, you will get a new cell with desired behavior. If you then copy and paste some code into this cell, you can start playing with it. Select the piece of code you'd like to move to the right, and press CTRL+ `. The code you select should be a complete expression or a sequence of expressions.

I am sure there are bugs and limitations in this simple solution. It is more in the spirit of showing how things might work, than being a complete solution here. I just find it interesting to explore the possibilities the FrontEnd can give us.

$\endgroup$
8
$\begingroup$

Later is better then never. 8 years after my original question, now in version 13.1 we have tab and shift + tab indentation in a native way, with no gambiarra needed. Cool!

$\endgroup$
3
  • $\begingroup$ "13.2.0 for Linux x86 (64-bit) (December 7, 2022)" this is not working for me. Can any Linux user confirm this indeed works for them? (It might be a me problem. I use modified hotkeys that I moved from earlier versions.) $\endgroup$
    – Kvothe
    Commented Feb 23, 2023 at 16:55
  • $\begingroup$ I have "Item[KeyEvent["Tab", Modifiers -> {Shift}], "MovePreviousPlaceHolder"]" in what I believe to be my unmoddified 13.2 KeyEventTranslations.tr file. That does not sound like it is indenting. Do you have KeyEvent for indenting? If so could you share it, perhaps it will work if I simply add it. $\endgroup$
    – Kvothe
    Commented Feb 23, 2023 at 17:00
  • $\begingroup$ Oops I probably misunderstood what you meant. While selecting multiple lines it seems that Tab natively indents multiple lines. (I am not sure what Shift +Tab is supposed to do but it doesn't do anything Indent related for me probably due to the KeyEvent mentioned in my comment above.) $\endgroup$
    – Kvothe
    Commented Feb 23, 2023 at 17:20
6
$\begingroup$

I do much the same as Leonid, except I generally first convert the \[IndentingNewLine] structure into the appropriate tabified block:

indentingNewLineReplace[r : RowBox[data_]] :=
  RowBox@
   Replace[data, {
     "{" :>
      CompoundExpression[
       $indentationUnbalancedBrackets["{"]++,
       "{"
       ],
     "}" :>
      CompoundExpression[
       $indentationUnbalancedBrackets["{"] =
        Max@{$indentationUnbalancedBrackets["{"] - 1, 0},
       "}"
       ],
     "[" :>
      CompoundExpression[
       $indentationUnbalancedBrackets["["]++,
       "["
       ],
     "]" :>
      CompoundExpression[
       $indentationUnbalancedBrackets["["] =
        Max@{$indentationUnbalancedBrackets["["] - 1, 0},
       "]"
       ],
     "(" :>
      CompoundExpression[
       $indentationUnbalancedBrackets["("]++,
       "("
       ],
     ")" :>
      CompoundExpression[
       $indentationUnbalancedBrackets["("] =
        Max@{$indentationUnbalancedBrackets["("] - 1, 0},
       ")"
       ],
     r2_RowBox :>
      indentingNewLineReplace[r2],
     $indentingNewLine :>
      CompoundExpression[
       Map[
        Which[
          $indentationUnbalancedBrackets[#] > \
$intentationPreviousLevels[#],
          $indentationLevel[#]++,
          $indentationUnbalancedBrackets[#] < \
$intentationPreviousLevels[#],
          $indentationLevel[#] =
           Max@{$indentationLevel[#] - 1, 0}
          ] &,
        Keys@$indentationLevel],
       $intentationPreviousLevels = $indentationUnbalancedBrackets,
       "\n" <>
        If[Total@$indentationLevel > 0,
         StringRepeat["\t", Total@$indentationLevel],
         ""
         ]
       ]
     },
    1];

IndentingNewLineReplace[r : RowBox[data_]] :=
  Block[{
    $indentationUnbalancedBrackets =
     <|"[" -> 0, "{" -> 0, "(" -> 0|>,
    $intentationPreviousLevels =
     <|"[" -> 0, "{" -> 0, "(" -> 0|>,
    $indentationLevel =
     <|"[" -> 0, "{" -> 0, "(" -> 0|>
    },
   indentingNewLineReplace[r]
   ];
IndentingNewLineReplace[s_String] :=
  s;

IndentationReplace[nb_: Automatic] :=

  With[{inputNotebook = Replace[nb, Automatic :> InputNotebook[]]},
   With[{selection = IndentationSelection@inputNotebook},
    With[{write = IndentingNewLineReplace@selection},
     NotebookWrite[inputNotebook, write,
      If[MatchQ[write, _String?(StringMatchQ[Whitespace])],
       After,
       All]]
     ]
    ]
   ];

Then I do a standard simple recursive replacement:

IndentationSelection[inputNotebook_] :=

  Replace[NotebookRead@inputNotebook, {
    Cell[BoxData[d_] | d_String, ___] :>
     CompoundExpression[
      SelectionMove[First@SelectedCells[], All, CellContents],
      d]
    }];

indentationAddTabsRecursiveCall[RowBox[d : {___}]] :=
  RowBox@
   Replace[d, {
     r_RowBox :>
      indentationAddTabsRecursiveCall[r],
     s_String?(StringMatchQ[$indentingNewLine ~~ ___]) :>

      StringInsert[StringDrop[s, 1], "\n\t", 1],
     s_String?(StringMatchQ["\n" ~~ ___]) :>

      StringInsert[s, "\t", 2]
     },
    1];
indentationAddTabs[sel_] :=
  Replace[
   sel, {
    {} :> "\t",
    _String :>
     StringReplace[sel, {
       "\n" :> "\n\t",
       StartOfString :> "\t"
       }],
    _ :>
     Replace[indentationAddTabsRecursiveCall[sel],
      RowBox[{data___}] :>
       RowBox[{"\t", data}]
      ]
    }];

IndentationIncrease[nb_: Automatic] :=

  With[{inputNotebook = Replace[nb, Automatic :> InputNotebook[]]},
   With[{write = 
      indentationAddTabs@IndentationSelection@inputNotebook},
    NotebookWrite[inputNotebook, write,
     If[MatchQ[write, _String?(StringMatchQ[Whitespace])],
      After,
      All]]
    ]
   ];

And then you can put this def in a stylesheet or attach it to a cell / notebook:

IndentationEvent[] :=
  If[AllTrue[
    {"OptionKey", "ShiftKey"},
    CurrentValue[EvaluationNotebook[], #] &
    ],
   IndentationRestore[],
   Which[
    Not@FreeQ[NotebookRead@EvaluationNotebook[], $indentingNewLine],
    IndentationReplace[],
    CurrentValue["OptionKey"],
    IndentationDecrease[],
    True,
    IndentationIncrease[]
    ]
   ];

{
 {"KeyDown", "\t"} :>
  Quiet@Check[
    Needs["BTools`"];
    IndentationEvent[],
    SetAttributes[EvaluationCell[], CellEventActions -> None]
    ],
 PassEventsDown -> False
 }

It's a lot of code, but it is generally quite robust in my experience. The only standard issue I have is that the indenting-newline to tab replacer doesn't appropriately pick up special indenting characters like = and ->. It only catches the block indents.

$\endgroup$
5
$\begingroup$

Here is a much more modest try for blocks of code. :) You could expand it to cover text like @LeonidShifrin has shown. It could also be reversed to unindent.

SetOptions[$FrontEnd, 
 FrontEndEventActions -> {
  {"KeyDown", "\t"} :> 
   NotebookWrite[
    InputNotebook[], 
    Insert[
     NotebookRead[InputNotebook[]] /. 
      "\[IndentingNewLine]" -> 
       Sequence["\[IndentingNewLine]", "\t"],
     "\t", {1, 1}]]}]

Select the whole lines (at least make sure you have the beginning of each line, including the first) and press tab:

a
b
c

... a

... b

... c

$\endgroup$
9
  • $\begingroup$ This is nice. Is it possible to retain basic tabbing (inserting a tab character) when there's no selection? $\endgroup$ Commented May 27, 2014 at 4:17
  • $\begingroup$ @RicoPicone after :> check to see if CurrentValue["SelectionData"] === $Failed (= nothing is selected), then decide what to do based on that. (Too cryptic?) $\endgroup$
    – mfvonh
    Commented May 27, 2014 at 4:19
  • $\begingroup$ Thanks, I was able to get that working. However, I noticed that under many circumstances, the block-tabbing fails. Even when selecting entire lines, often only the first line is indented. Sometimes the other lines are also indented, but one line from the selection is not (like the second line). I was unable to detect rules for these occurrences, and I suspect some extra hidden characters are running around that I can't see (is there any way to show them?). $\endgroup$ Commented May 27, 2014 at 15:28
  • $\begingroup$ @RicoPicone Yeah, that code won't work unless you have manually inserted line breaks (it's just adding a tab at the beginning of the selection and after every line break). Maybe what you want is changing LineIndent. Press Ctrl+Shift+O, then type LineIndent in the "Lookup" box. You can change it for individual cells or the whole notebook that way. Or you can do it via the StyleSheet. $\endgroup$
    – mfvonh
    Commented May 27, 2014 at 16:51
  • $\begingroup$ @RicoPicone Also, I am just repeating others here, but you may consider Wolfram Workbench or (in my opinion much better) halirutan's MMA plugin for IDEA. I find it much easier to code in an IDE and use notebooks for testing/etc. $\endgroup$
    – mfvonh
    Commented May 27, 2014 at 16:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.