Hot Reloading w Pythonie

Hot Reloading w Pythonie
K9
  • Rejestracja:prawie 3 lata
  • Ostatnio:około 2 lata
  • Postów:15
17

Cześć wszystkim,
Jakbyscie byli zainteresowani tematem hot reloadingu to polecam moja paczkę reloadium.
https://github.com/reloadware/reloadium

Umożliwia ona modyfikację kodu w trakcie działania aplikacji.
example_small.gif
Napisałem też plugin do PyCharma
https://plugins.jetbrains.com/plugin/18509-reloadium

Jestem ciekaw co o tym myślicie.

veneficus
  • Rejestracja:ponad 5 lat
  • Ostatnio:21 dni
  • Postów:383
1

Hmm, ciekawe sprawdzę dziś, ale szczerze nie widzę zastosowania dla większych projektów.


We buy things we don't need with money we don't have to impress people we don't like.
WeiXiao
  • Rejestracja:około 9 lat
  • Ostatnio:4 minuty
  • Postów:5105
4

@veneficus

Wręcz przeciwnie, zmiana kodu w locie jest jedną z najużyteczniejszych rzeczy przy debugowaniu i im większy projekt, im więcej trzeba pozmieniać aby wprowadzić system w określony stan tym bardziej się taki feature przydaje

@Kwazar90:

A jak wygląda wsparcie dla przeskakiwania do różnych linii kodu? np. z 1 do 3, wykonanie 4, dopisanie 5 i jej wykonanie, a na końcu powrót do 1?

edytowany 2x, ostatnio: WeiXiao
veneficus
W sumie, może źle o tym myślałem, muszę przetestować :)
K9
  • Rejestracja:prawie 3 lata
  • Ostatnio:około 2 lata
  • Postów:15
1

@1a2b3c4d5e: Tak te funkcje sa wspierane. Dodatkowo jak uzytkownik sie pomyli mozna poprawic bledy jak ponizej:

image

WeiXiao
  • Rejestracja:około 9 lat
  • Ostatnio:4 minuty
  • Postów:5105
1

Mógłbyś tak high levelowo opisać jak to działa?

K9
  • Rejestracja:prawie 3 lata
  • Ostatnio:około 2 lata
  • Postów:15
3

@1a2b3c4d5e: Kod nowego modulu jest wykonywany w sandboxie po czym sa wykrywane i dodawane zmiany. Jezeli zmiana jest w trakcie debugowania jak pokazane powyzej to dodatkowo funkcja w ktorej jest debugger jest restartowana.

veneficus
  • Rejestracja:ponad 5 lat
  • Ostatnio:21 dni
  • Postów:383
1

Nie widzę listy wspieranych urządzeń na GH, Python 3.10, OSX, Pycharm professional, Nie ma wsparcia dla wszystkiego?
screenshot-20220416205237.png


We buy things we don't need with money we don't have to impress people we don't like.
edytowany 1x, ostatnio: veneficus
Zobacz pozostały 1 komentarz
veneficus
@jackweb: może coś w tym jest, w pracy muszę używać linuxa i mam dość walki z tym, że coś nie działa, czegoś brakuje itd/
JA
@veneficus: Ja to rozumiem:) sam pochodzę ze środowiska GNU/Linux (ponad 10 lat) i bym używał, gdyby nie to, że trzeba ciągle coś konfigurować itp. a w przypadku calli nie ma czasu na to, aby kompilować chromium z flagą --with-... pod jakiegoś nowego buga z mikrofonem
veneficus
@jackweb: Właśnie o tym mówię, denerwuje mnie brak czegoś czy potrzeby konfiguracji, dlatego OSX :)
JA
Skoro OSX, to masz stary system 🤪
veneficus
@jackweb: a przyzwyczaiłem się się od do mówienia OSX, standardowo miałem na myśli macOS i cudo w postaci procesorów w technologii ARM .
K9
  • Rejestracja:prawie 3 lata
  • Ostatnio:około 2 lata
  • Postów:15
0

@veneficus: Czy to jest OSX ARM64? Wszystkie systemy sa wspierane oprocz tego wlasnie. Aktualnie nie da sie zbudowac paczek na ten system na github actions.
I racja, musze to dodac na githubie.

veneficus
Tak, dokladnie tak :( Planujesz dodanie w przyszłości jakoś też na ARM?
K9
Tak, planuje dodac jak dodadza virtual environment na github actions. Inaczej nie mam jak zbudowac na ta platforme.
Riddle
Administrator
  • Rejestracja:ponad 14 lat
  • Ostatnio:minuta
  • Lokalizacja:Laska, z Polski
  • Postów:10045
2

A masz jakiś sensowny przykład kiedy to by miało sens? Jakiś scenariusz?

WeiXiao
  • Rejestracja:około 9 lat
  • Ostatnio:4 minuty
  • Postów:5105
0

@TomRiddle:

Puszczam kod który wykonuje SQLa i poleciał wyjątek, ale jako że nie chce mi się przygotowywać danych, ani aby odpalał się wcześniejszy kod jeszcze raz

Kopiuj
void DoSomething(a,b,c)
{
	try
	{
		sql("SELEKT *", a,b,c);
	}
	catch (Exception ex)
	{
		
	}
}

to w obsłudze wyjątku dodałem linijkę która wykonuje tą samą funkcję jeszcze raz i przekazuje tam parametry

Kopiuj
void DoSomething(a,b,c)
{
	try
	{
		sql("SELEKT *", a,b,c);
	}
	catch (Exception ex)
	{
		DoSomething(a,b,c);
	}
}

i teraz gdy jestem tu

Kopiuj
void DoSomething(a,b,c)
{
	try
	{ <------------------- jestem tu
		sql("SELEKT *", a,b,c);
	}
	catch (Exception ex)
	{
		DoSomething(a,b,c);
	}
}

to zmieniam to zapytanie, puszczam niech wykona

Kopiuj
void DoSomething(a,b,c)
{
	try
	{
		sql("SELECT *", a,b,c);
	}  <------------------- jestem tu
	catch (Exception ex)
	{
		DoSomething(a,b,c);
	}
}

i teraz usuwam to co dodałem

Kopiuj
void DoSomething(a,b,c)
{
	try
	{
		sql("SELEKT *", a,b,c);
	}  <------------------- jestem tu
	catch (Exception ex)
	{
 
	}
}
edytowany 1x, ostatnio: WeiXiao
Riddle
Administrator
  • Rejestracja:ponad 14 lat
  • Ostatnio:minuta
  • Lokalizacja:Laska, z Polski
  • Postów:10045
1

Niby pomysł spoko, aczkolwiek trochr nie kumam czemu miałbym nie zeminic kodu i nie zrobić restart. Chyba tylko tym że mam już breakpoint we frameie, ale nawigacja po stacku wywołań jak ktoś umie w debugger to nie jest jakiś super trudna.

Także dla mnie pomysł fajny, jeśli faktycznie to by działało bez zarzutu i nie robiło jakichś cyrków, to spoko. Fajny bajer. Ale nie widzę co by to narzędzie umiało czego restart sesji by nie umiał, trochę bardziej wygodne tylko.

Shalom
  • Rejestracja:około 21 lat
  • Ostatnio:prawie 3 lata
  • Lokalizacja:Space: the final frontier
  • Postów:26433
3

@TomRiddle bo np. dojście do sytuacji w danym miejscu kosztuje dużo czasu ;) Np. próbujesz striggerować jakiś bardzo specyficzny bug gdzie setup jest skomplikowany i restart oznacza że stracisz kolejne pół godziny (ot powiedzmy jakiś heisenbug wynikający z concurrency który pojawia się raz na pół godziny mielenia requestów)


"Nie brookliński most, ale przemienić w jasny, nowy dzień najsmutniejszą noc - to jest dopiero coś!"
edytowany 1x, ostatnio: Shalom
Riddle
Administrator
  • Rejestracja:ponad 14 lat
  • Ostatnio:minuta
  • Lokalizacja:Laska, z Polski
  • Postów:10045
1
Shalom napisał(a):

@TomRiddle bo np. dojście do sytuacji w danym miejscu kosztuje dużo czasu ;) Np. próbujesz striggerować jakiś bardzo specyficzny bug gdzie setup jest skomplikowany i restart oznacza że stracisz kolejne pół godziny (ot powiedzmy jakiś heisenbug wynikający z concurrency który pojawia się raz na pół godziny mielenia requestów)

A nawet jeśli już miałbyś już znaleźć takiego buga, to czemu miałbyś nie użyć wtedy "watch" z debuggera albo "evaluate expression"? Co takiego ma zaprezentowane rozwiązanie, czego już nie mają debuggery - poza tym, że może to jest kapkę lepszy interfejs, że w kodzie zamiast w evaluate?

A poza tym, to nie kupuję takich akcji że żeby zreprodukować buga na prawdę musisz mielić coś 30 minut, chyba że w na prawdę mega podziwnionych przypadkach, dla których ja znalazłbym inne określenie (wink, wink, bad architecture). Bo jeśli ktoś szuka błędów z concurrency przy użyciu debuggera to moim zdaniem już na starcie jesteś spalony.

Nie mówię że coś zaprezentowane przez autora jest głupie - bo to jest fajna zabawka. Ale nie widzę co takiego można przy jej użyciu zrobić czego nie można z istniejącymi już narzędziami.

edytowany 1x, ostatnio: Riddle
randomize111
  • Rejestracja:prawie 4 lata
  • Ostatnio:prawie 2 lata
  • Postów:137
1

Nie istnieje już coś takiego? https://github.com/breuleux/jurigged
I zgadzam się z @TomRiddle - fajna zabawka, ale nigdy nie słyszałem zbytniego zapotrzebowania na coś takiego w Pythonie.

WeiXiao
  • Rejestracja:około 9 lat
  • Ostatnio:4 minuty
  • Postów:5105
1

@TomRiddle:

A nawet jeśli już miałbyś już znaleźć takiego buga, to czemu miałbyś nie użyć wtedy "watch" z debuggera albo "evaluate expression"?

Ale przecież to zrobisz raz, a kod zmieniony już będzie się cały czas wykonywał

Weź na warsztat np. pętlę - i watchem oraz eval expr. zmienisz jedną iterację, a zmieniając kod w locie zmienisz jej działanie na stałe.

Riddle
Administrator
  • Rejestracja:ponad 14 lat
  • Ostatnio:minuta
  • Lokalizacja:Laska, z Polski
  • Postów:10045
1
1a2b3c4d5e napisał(a):

Ale przecież to zrobisz raz, a kod zmieniony już będzie się cały czas wykonywał

Weź na warsztat np. pętlę - i watchem oraz eval expr. zmienisz jedną iterację, a zmieniając kod w locie zmienisz jej działanie na stałe.

Mógłbyś wziąć pętlę do evaluate'a

WeiXiao
  • Rejestracja:około 9 lat
  • Ostatnio:4 minuty
  • Postów:5105
0

@TomRiddle:

dafq? weź to pokaż

ja w vs w eval expr. zazwyczaj proste rzeczy evaluatowałem, nie wiem czemu miałbym tam pętle np. forka wrzucić

edytowany 6x, ostatnio: WeiXiao
Riddle
Administrator
  • Rejestracja:ponad 14 lat
  • Ostatnio:minuta
  • Lokalizacja:Laska, z Polski
  • Postów:10045
0
1a2b3c4d5e napisał(a):

@TomRiddle:

dafq? weź to pokaż

ja w vs w eval expr. zazwyczaj proste rzeczy evaluatowałem, nie wiem czemu miałbym tam pętle wrzucić

vs.

1a2b3c4d5e napisał(a):

Weź na warsztat np. pętlę - i watchem oraz eval expr. zmienisz jedną iterację, a zmieniając kod w locie zmienisz jej działanie na stałe.

WeiXiao
  • Rejestracja:około 9 lat
  • Ostatnio:4 minuty
  • Postów:5105
1

masz kod:

Kopiuj
for (int i=0; i<asd.Length; i++)
{
  DoStuff();
}

stawiasz break pointa

Kopiuj
for (int i=0; i<asd.Length; i++)
{ <-------- BP
  DoStuff();
}

zmieniasz body

Kopiuj
for (int i=0; i<asd.Length; i++)
{ 
  DoStuffDifferently();
}

proszę, o to Edit&Continue, jakbyś to chciał uzyskać (w miare sensownie) przy eval expr? nie twierdzę że się nie da, ale po co?

edytowany 3x, ostatnio: WeiXiao
K9
  • Rejestracja:prawie 3 lata
  • Ostatnio:około 2 lata
  • Postów:15
4

@TomRiddle: Przy wiekszych projektach czas startupu jest wysoki i restartowanie calego procesu jest po prostu czasochlonne (20s run, 50s debug aplikacja django z poprzedniej pracy).
Przy takim startupie koszt naprawy malego bledu (literowka np) jest duzy.

Inny scenariusz to zreprodukowanie bledu albo wprowadzenie interpretera w dana funkcje moze trwac minuty albo i wiecej.

Oczywiscie mozesz kombinowac z evaluate, ale jak to wczesniej inny ludzie opisali nie bedzie to dzialac w wiekszosci przypadkow. No i tez to jest na okolo bo musisz i tak pozniej do kodu przekleic wiec podwojna robota plus mozliwosc pomylki. Dodatkowa nie da sie debugowac kodu w oknie evaluate.

Tak samo jak nie musisz korzystac z narzedzia zwanego debuggerem tylko printy wrzucac w kod tak samo mozesz kombinowac na okolo. Mozna ale po co ;)

@randomize111: Ta biblioteka wspiera tylko podstawowe przypadki plus nie da sie modyfikowac w trakcie debugowania i z tego co widze autor juz nad nia nie pracuje.

Pyxis
  • Rejestracja:ponad 7 lat
  • Ostatnio:około 2 godziny
1

Na tym forum nie idzie dogodzić ludziom :)

Shalom
Nie ma takiej rzeczy która by się wszystkim podobała ;) Ludzie mają różne opinie, tak już jest.
Marcin Marcin
Jeszcze się taki nie urodził co by każdemu dogodził
SL
  • Rejestracja:około 7 lat
  • Ostatnio:około 5 godzin
  • Postów:857
0

@TomRiddle: Zgaduję, że takoe flow z reloadem może być całkiem produktywne, jak ktoś dużo eksperymentuje i np. co chwile rozkłada na czynniki pierwsze zewnętrzne liby. Wada jaką widzę to konieczność nauki takiego podejścia, żeby wszystko działało efektywnie

Marcin Marcin
  • Rejestracja:prawie 6 lat
  • Ostatnio:18 dni
  • Postów:610
0

@Kwazar90: Brakuje dokumentacji w formie diagramu / opisu wygenerowanej w standardzie Sphinx
GitHub ma wbudowane Action które może utworzyć taką dokumentacje w kontenerze i wrzucić artefakt na serwer następnie wyświetlić jako GitHub pages

Chyba że kierujesz się moim przesłaniem:
Nie ma dokumentacji, wiedza jest w ludziach


Fan moderatora somekind
K9
  • Rejestracja:prawie 3 lata
  • Ostatnio:około 2 lata
  • Postów:15
0
from_ruby_to_python
  • Rejestracja:ponad 5 lat
  • Ostatnio:ponad rok
  • Postów:8
0

@Kwazar90: Ej - świetne to jest !

masochista
  • Rejestracja:około 6 lat
  • Ostatnio:około 3 godziny
  • Lokalizacja:Warszawa
  • Postów:77
0

@Kwazar90 Fajne, szkoda że py2.7 nie jest wspierany :(

KamilAdam
A python 2.x już nie przestał być ofecjalnie wspierany przez twórców pythona?
randomize111
@KamilAdam: tak, na początku 2020 był koniec supportu dla Pythona 2
veneficus
czas przejść na 3+
masochista
Oj ponarzekać już sobie nie można :D Oficjalnie nie jest już wspierany, ale nieoficjalnie wiele narzędzi nadal wspiera tą wersję bo projektów, które stoją na pythonie 2.x jest nadal od cholery :P
K9
  • Rejestracja:prawie 3 lata
  • Ostatnio:około 2 lata
  • Postów:15
2

Prawdopodobnie nawet by się nie dało na python 2.7 tego zaimplementować, ciężko było na python 3.6 też.
Dodatkowo bez type hintingu kod byłby całkowicie nieczytelny.

Zobacz pozostałe 117 komentarzy
Riddle
@randomize111: Noi to właśnie jest dynamiczne typowanie! Jeśli nie zmienisz type-hinta, to to jest dynamic-typing w najczystszej postaci. Ale są tacy (wink, wink, @Kwazar90) co by zmienili type-hint, żeby pasował do wszystkich typów, robiąc static-typing. Poza tym, są też tacy co powiedzieliby że ignorując i nie update'ując type-hintu robisz spaghetti, z czym się w sumie zgadzam bo jak go ignorujesz, to po co ten type-hint w sumie.
stivens
Ale jak trzeba miec niepoukladane w glowie, ze by majac funkcje def foo(x: Player) nagle gdzies w srodku napisac x = "DUPA". Jakby nie mozna bylo jak czlowiek napisac dupa = "DUPA". No ja nie rozumiem po co komu to nadpisywanie typu :) Macie ograniczona liczbe zmiennych (w sensie nazw dla nich) i trzeba reuzywac juz te istniejace czy co?
stivens
Polecam tez doczytac np. o polimorfizmie parametrycznym (start tutaj: https://en.wikipedia.org/wiki/Parametric_polymorphism)
stivens
Swoja droga to taki wysoce ogolny kod to jest raczej domena bibliotek a nie kodu biznesowego. Bo jak piszesz np. biblioteke standardowa jezyka programowania to robisz np. class List[T] { def add(elem: T): Unit = ??? }. Ale jak masz np. sklep internetowy to w koszyku bedzie lista (mapa/slownik/whatever/...) ktora bedzie miala bardzo zawezony typ. Bedzie trzymac elementy typu StoreItem (modulo nazwa). Dorzucenie do takiego koszyka jakiegos np. stringa nie ma absolutnie zadnego sensu. To po co to dynamiczne typowanie?
Riddle
Jak dla mnie to możesz na psa mówić kot, i odwrotnie. Możesz też nazywać dynamic static, a static dynamic. Chciałem pomoc wyjść z błędu, ale widzę że niektórzy wolą słodkie kłamstwo.
WeiXiao
  • Rejestracja:około 9 lat
  • Ostatnio:4 minuty
  • Postów:5105
1

A zapomniałem - gratki głównej HNa

Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)